首页 文章

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

提问于
浏览
153

我有这个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)

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

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设计?

CLARIFICATIONS:

  • 请假设没有违反单一责任原则 . 在我原来的情况下,该函数只是将给定的参数写入单个DB记录 .

  • 我想要对这些问题采取一般方法 . 我特别感兴趣的是解决"too many parameters"问题,而不会引入可变性或可怕的设计 .

UPDATE

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

13 回答

  • 3

    除了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 . 是的 - 它类似于构建器 - 仅反向(即在被叫方而不是调用方) . 在这种特定情况下,它可能会或可能不会比建造者更好 .

  • 2

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

    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
    
  • 0

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

    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; } 
            }
        }
    
  • 10

    为什么不只是创建一个强制不变性的界面(即只有getter)?

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

    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 解决方案那样的堆栈空间问题

    • 使用语言语义的自然解决方案

    • 不变性是显而易见的

    • 灵活(消费者可以根据需要使用不同的课程)

    缺点:

    • 一些重复的工作(两个不同实体中的相同声明)

    • Developer必须猜测 DoSomeActionParameters 是一个可以映射到 IDoSomeActionParameters 的类

  • 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();
            }
        }
    
  • 1

    只需将参数数据结构从 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)
    

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

    优点:

    • 最容易实施

    • 底层力学中行为的最小变化

    缺点:

    • 不变性不明显,需要开发人员注意 .

    • 不必要的复制维护不变性

    • 占用堆栈空间

  • 80

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

  • 6

    当我遇到同样的问题时,我在项目中使用的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方法不允许所有可能的组合,只是暴露了它们的常见组合 . 那是因为我的一些参数非常复杂,所有可能情况的编写方法都很困难且不必要(很少使用疯狂的组合) .

  • 2

    使用构建器和特定于域的语言样式API - Fluent Interface的组合 . API有点冗长,但是通过intellisense可以非常快速地输出并且易于理解 .

    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());
    
  • 10

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

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

    Jon和FXCop将因为您曝光的不是字段而感到满意 .

    •Eric Lippert等人说依靠只读字段来获取不变性是一个谎言 .

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

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

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

  • 6

    你可以使用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);
    
  • 21

    这里有一个与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是不可变的,因为它可以并且不能直接创建,因为它的默认构造函数是私有的

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

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

    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";
                })
            );
    
  • 2

    我不是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")
    

    这不需要更多的空间/思考你的对象创建和所有的好处,事实上你没有改变在unerlying系统中发生的事情 . 您甚至不需要重新编码任何内容来指示参数已命名

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

相关问题