如何避免API设计中的“参数太多”问题?

时间:2011-06-04 21:01:08

标签: c# data-structures immutability

我有这个API函数:

public ResultEnum DoSomeAction(string a, string b, DateTime c, OtherEnum d, 
     string e, string f, out Guid code)

我不喜欢它。因为参数顺序变得不必要的重要。添加新字段变得更加困难。很难看到传递的是什么。将方法重构为较小的部分更加困难,因为它会产生另一个传递子函数中所有参数的开销。代码更难阅读。

我提出了一个最明显的想法:让一个对象封装数据并传递它,而不是逐个传递每个参数。以下是我提出的建议:

public class DoSomeActionParameters
{
    public string A;
    public string B;
    public DateTime C;
    public OtherEnum D;
    public string E;
    public string F;        
}

这将我的API声明减少为:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

尼斯。看起来很无辜,但我们实际上引入了一个巨大的变化:我们引入了可变性。因为我们以前一直在做的事实上是传递一个匿名的不可变对象:堆栈上的函数参数。现在我们创建了一个非常可变的新类。我们创建了操纵来电者状态的功能。太糟糕了。现在我希望我的对象不可变,我该怎么做?

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }        

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, 
     string e, string f)
    {
        this.A = a;
        this.B = b;
        // ... tears erased the text here
    }
}

正如您所看到的,我实际上重新创建了我的原始问题:参数太多了。很明显,这不是要走的路。我该怎么办?实现这种不变性的最后一个选择是使用像这样的“只读”结构:

public struct DoSomeActionParameters
{
    public readonly string A;
    public readonly string B;
    public readonly DateTime C;
    public readonly OtherEnum D;
    public readonly string E;
    public readonly string F;        
}

这允许我们避免具有太多参数的构造函数并实现不变性。实际上它修复了所有问题(参数排序等)。然而:

那时我感到困惑并决定写下这个问题:C#中最简单的方法是避免“太多参数”问题而不引入可变性?是否有可能为此目的使用readonly结构,但没有错误的API设计?

澄清:

  • 请假设没有违反单一责任原则。在我原来的情况下,该函数只是将给定的参数写入单个DB记录。
  • 我不是在寻找给定函数的特定解决方案。我正在寻求解决这些问题的一般方法。我特别感兴趣的是解决“太多参数”问题,而不会引入可变性或糟糕的设计。

更新

这里提供的答案有不同的优点/缺点。因此,我想将其转换为社区维基。我认为代码示例和优点/缺点的每个答案都可以为将来的类似问题提供一个很好的指导。我现在正试图找出如何做到这一点。

13 个答案:

答案 0 :(得分:80)

使用构建器和特定于域的语言样式API - Fluent Interface的组合。 API更加冗长,但是通过智能感知,它可以非常快速地输入并且易于理解。

public class Param
{
        public string A { get; private set; }
        public string B { get; private set; }
        public string C { get; private set; }


  public class Builder
  {
        private string a;
        private string b;
        private string c;

        public Builder WithA(string value)
        {
              a = value;
              return this;
        }

        public Builder WithB(string value)
        {
              b = value;
              return this;
        }

        public Builder WithC(string value)
        {
              c = value;
              return this;
        }

        public Param Build()
        {
              return new Param { A = a, B = b, C = c };
        }
  }


  DoSomeAction(new Param.Builder()
        .WithA("a")
        .WithB("b")
        .WithC("c")
        .Build());

答案 1 :(得分:21)

框架中包含的一种风格通常就像将相关参数分组到相关类中一样(但又有可变性的问题):

var request = new HttpWebRequest(a, b);
var service = new RestService(request, c, d, e);
var client = new RestClient(service, f, g);
var resource = client.RequestRestResource(); // O params after 3 objects

答案 2 :(得分:10)

你所拥有的是一个非常明确的迹象,表明有问题的类违反了Single Responsibility Principle,因为它有太多的依赖关系。寻找将这些依赖项重构为Facade Dependencies的集群的方法。

答案 3 :(得分:10)

只需将参数数据结构从class更改为struct,即可开始使用。

public struct DoSomeActionParameters 
{
   public string A;
   public string B;
   public DateTime C;
   public OtherEnum D;
   public string E;
   public string F;
}

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code) 

该方法现在将获得自己的结构副本。方法无法观察对参数变量所做的更改,并且调用方无法观察到方法对变量的更改。隔离是在没有不可变性的情况下实现的。

优点:

  • 最容易实施
  • 基础机制中行为的最小变化

缺点:

  • 不变性不明显,需要开发人员注意。
  • 不必要的复制以保持不变性
  • 占用堆栈空间

答案 4 :(得分:6)

如何在数据类中创建构建器类。数据类将所有setter设置为private,只有构建器才能设置它们。

public class DoSomeActionParameters
    {
        public string A { get; private set; }
        public string B  { get; private set; }
        public DateTime C { get; private set; }
        public OtherEnum D  { get; private set; }
        public string E  { get; private set; }
        public string F  { get; private set; }

        public class Builder
        {
            DoSomeActionParameters obj = new DoSomeActionParameters();

            public string A
            {
                set { obj.A = value; }
            }
            public string B
            {
                set { obj.B = value; }
            }
            public DateTime C
            {
                set { obj.C = value; }
            }
            public OtherEnum D
            {
                set { obj.D = value; }
            }
            public string E
            {
                set { obj.E = value; }
            }
            public string F
            {
                set { obj.F = value; }
            }

            public DoSomeActionParameters Build()
            {
                return obj;
            }
        }
    }

    public class Example
    {

        private void DoSth()
        {
            var data = new DoSomeActionParameters.Builder()
            {
                A = "",
                B = "",
                C = DateTime.Now,
                D = testc,
                E = "",
                F = ""
            }.Build();
        }
    }

答案 5 :(得分:6)

为什么不制作一个强制不变性的界面(即只有吸气剂)?

它本质上是您的第一个解决方案,但您强制该函数使用该接口来访问参数。

public interface IDoSomeActionParameters
{
    string A { get; }
    string B { get; }
    DateTime C { get; }
    OtherEnum D { get; }
    string E { get; }
    string F { get; }              
}

public class DoSomeActionParameters: IDoSomeActionParameters
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }        
}

,函数声明变为:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

优点:

  • 没有像struct解决方案
  • 这样的堆栈空间问题
  • 使用语言语义的自然解决方案
  • 不变性很明显
  • 灵活(消费者可以根据需要使用不同的课程)

缺点:

  • 一些重复的工作(两个不同实体中的相同声明)
  • 开发人员必须猜测DoSomeActionParameters是一个可以映射到IDoSomeActionParameters的类

答案 6 :(得分:6)

我不是C#程序员,但我相信 C#支持命名参数:( F#和C#在很大程度上是特征兼容的那种东西) 它确实: http://msdn.microsoft.com/en-us/library/dd264739.aspx#Y342

所以调用原始代码变为:

public ResultEnum DoSomeAction( 
 e:"bar", 
 a: "foo", 
 c: today(), 
 b:"sad", 
 d: Red,
 f:"penguins")

这不会占用你的对象创造空间/思想 并且拥有所有的好处,因为你根本没有改变在供应系统中发生的事情。 您甚至不需要重新编码任何内容来指示参数名为

编辑: 这是我发现的关于它的艺术品。 http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ 我应该提到C#4.0支持命名参数,3.0没有

答案 7 :(得分:3)

我知道这是一个古老的问题,但我认为我会接受我的建议,因为我只需要解决同样的问题。现在,我承认我的问题与你的问题略有不同,因为我有额外的要求,不希望用户自己构建这个对象(数据的所有水合都来自数据库,所以我可以在内部监禁所有构造)。这允许我使用私有构造函数和以下模式;

    public class ExampleClass
    {
        //create properties like this...
        private readonly int _exampleProperty;
        public int ExampleProperty { get { return _exampleProperty; } }

        //Private constructor, prohibiting construction outside of this class
        private ExampleClass(ExampleClassParams parameters)
        {                
            _exampleProperty = parameters.ExampleProperty;
            //and so on... 
        }

        //The object returned from here will be immutable
        public ExampleClass GetFromDatabase(DBConnection conn, int id)
        {
            //do database stuff here (ommitted from example)
            ExampleClassParams parameters = new ExampleClassParams()
            {
                ExampleProperty = 1,
                ExampleProperty2 = 2
            };

            //Danger here as parameters object is mutable

            return new ExampleClass(parameters);    

            //Danger is now over ;)
        }

        //Private struct representing the parameters, nested within class that uses it.
        //This is mutable, but the fact that it is private means that all potential 
        //"damage" is limited to this class only.
        private struct ExampleClassParams
        {
            public int ExampleProperty { get; set; }
            public int AnotherExampleProperty { get; set; }
            public int ExampleProperty2 { get; set; }
            public int AnotherExampleProperty2 { get; set; }
            public int ExampleProperty3 { get; set; }
            public int AnotherExampleProperty3 { get; set; }
            public int ExampleProperty4 { get; set; }
            public int AnotherExampleProperty4 { get; set; } 
        }
    }

答案 8 :(得分:2)

您可以使用Builder风格的方法,但根据DoSomeAction方法的复杂程度,这可能是一个重量级的触摸。这些方面的东西:

public class DoSomeActionParametersBuilder
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }

    public DoSomeActionParameters Build()
    {
        return new DoSomeActionParameters(A, B, C, D, E, F);
    }
}

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, string e, string f)
    {
        A = a;
        // etc.
    }
}

// usage
var actionParams = new DoSomeActionParametersBuilder
{
    A = "value for A",
    C = DateTime.Now,
    F = "I don't care for B, D and E"
}.Build();

result = foo.DoSomeAction(actionParams, out code);

答案 9 :(得分:2)

除了manji响应之外 - 您可能还希望将一个操作拆分为几个较小的操作。比较:

 BOOL WINAPI CreateProcess(
   __in_opt     LPCTSTR lpApplicationName,
   __inout_opt  LPTSTR lpCommandLine,
   __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
   __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
   __in         BOOL bInheritHandles,
   __in         DWORD dwCreationFlags,
   __in_opt     LPVOID lpEnvironment,
   __in_opt     LPCTSTR lpCurrentDirectory,
   __in         LPSTARTUPINFO lpStartupInfo,
   __out        LPPROCESS_INFORMATION lpProcessInformation
 );

 pid_t fork()
 int execvpe(const char *file, char *const argv[], char *const envp[])
 ...

对于那些不了解POSIX的人来说,创建孩子可以像以下一样简单:

pid_t child = fork();
if (child == 0) {
    execl("/bin/echo", "Hello world from child", NULL);
} else if (child != 0) {
    handle_error();
}

每个设计选择都代表了它可能做什么操作的权衡。

PS。是的 - 它类似于构建器 - 仅反向(即在被叫方而不是调用方)。在这种特定情况下,它可能会或可能不会比建造者更好。

答案 10 :(得分:2)

这是与Mikeys略有不同的一个 但我要做的是尽可能少地写下整个事情

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }

    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }

        public DoSomeActionParameters Create()
        {
            return new DoSomeActionParameters(this);
        }
    }
}

DoSomeActionParameters是不可变的,因为它可以并且不能直接创建,因为它的默认构造函数是私有的

初始化程序不是不可变的,而只是传输

该用法利用了初始化程序中的初始化程序(如果你得到了我的漂移) 我可以在Initializer默认构造函数

中使用默认值
DoSomeAction(new DoSomeActionParameters.Initializer
            {
                A = "Hello",
                B = 42
            }
            .Create());

这里的参数是可选的,如果你需要一些参数,可以将它们放在Initializer的默认构造函数中

验证可以在Create方法

中进行
public class Initializer
{
    public Initializer(int b)
    {
        A = "(unknown)";
        B = b;
    }

    public string A { get; set; }
    public int B { get; private set; }

    public DoSomeActionParameters Create()
    {
        if (B < 50) throw new ArgumentOutOfRangeException("B");

        return new DoSomeActionParameters(this);
    }
}

所以现在看起来像

DoSomeAction(new DoSomeActionParameters.Initializer
            (b: 42)
            {
                A = "Hello"
            }
            .Create());

我知道还有一点kooki,但无论如何都要去尝试

编辑:将create方法移动到参数对象中的静态,并添加一个传递初始化程序的委托,将一些kookieness从调用中移除

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }
    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }
    }

    public static DoSomeActionParameters Create(Action<Initializer> assign)
    {
        var i = new Initializer();
        assign(i)

        return new DoSomeActionParameters(i);
    }
}

所以这个电话现在看起来像这个

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );

答案 11 :(得分:1)

使用结构,但不是公共字段,而是具有公共属性:

  

•每个人(包括FXCop和Jon Skeet)都同意暴露公共领域是不好的。

Jon和FXCop会因为你露出的不是字段而感到满意。

  

•Eric Lippert等人说,依靠readonly字段来获取不变性是一个谎言。

Eric会因为使用属性而感到满意,您可以确保该值只设置一次。

    private bool propC_set=false;
    private date pC;
    public date C {
        get{
            return pC;
        }
        set{
            if (!propC_set) {
               pC = value;
            }
            propC_set = true;
        }
    }

一个半不可变对象(可以设置值但不能更改)。适用于价值和参考类型。

答案 12 :(得分:0)

当我遇到同样的问题时,我在项目中使用的Samuel's answer变体:

class MagicPerformer
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
    public DateTime Param3 { get; set; }

    public MagicPerformer SetParam1(int value) { this.Param1 = value; return this; }
    public MagicPerformer SetParam2(string value) { this.Param2 = value; return this; }
    public MagicPerformer SetParam4(DateTime value) { this.Param3 = value; return this; }

    public void DoMagic() // Uses all the parameters and does the magic
    {
    }
}

使用:

new MagicPerformer().SeParam1(10).SetParam2("Yo!").DoMagic();

在我的情况下,参数是有意修改的,因为setter方法不允许所有可能的组合,只是暴露了它们的常见组合。那是因为我的一些参数非常复杂,所有可能情况的编写方法都很困难且不必要(很少使用疯狂的组合)。