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

Sed*_*glu 157 c# immutability data-structures

我有这个API函数:

public ResultEnum DoSomeAction(string a, string b, DateTime c, OtherEnum d, 
     string e, string f, out Guid code)
Run Code Online (Sandbox Code Playgroud)

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

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

public class DoSomeActionParameters
{
    public string A;
    public string B;
    public DateTime C;
    public OtherEnum D;
    public string E;
    public string F;        
}
Run Code Online (Sandbox Code Playgroud)

这减少了我的API声明:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)
Run Code Online (Sandbox Code Playgroud)

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

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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;        
}
Run Code Online (Sandbox Code Playgroud)

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

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

澄清:

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

UPDATE

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

Sam*_*eff 83

使用构建器和特定于域的语言样式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());
Run Code Online (Sandbox Code Playgroud)


Teo*_*gul 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
Run Code Online (Sandbox Code Playgroud)

  • 为什么要downvote?他只是举个例子,afaik这些变量与OP不同.+1 (11认同)
  • 随着时间的推移,我越来越倾向于这种风格.显然,通过良好的逻辑组和抽象可以解决大量"太多参数"问题.最后,它使代码更具可读性和模块化. (2认同)

Mar*_*ann 10

你所拥有的是一个非常明确的迹象表明所讨论的类违反了单一责任原则,因为它有太多的依赖关系.寻找将这些依赖项重构为Facade Dependencies集群的方法.

  • @Ruben:没有理智的书会说"在一个好的OO设计中,一个类不应该有超过5个属性".类按逻辑分组,并且这种情境化不能基于数量.然而,在我们开始违反良好的OO设计原则之前,C#的不变性支持开始在一定数量的属性上产生问题**. (3认同)
  • @Ruben:我没有判断你的知识,态度或脾气.我希望你也这样做.我不是说你的推荐不好.我说我的问题甚至出现在最完美的设计上,这似乎是一个被忽视的问题.虽然我理解为什么有经验的人会就最常见的错误提出基本问题,但在几轮之后澄清它会变得不那么愉快.我必须再说一次,完美的设计很可能会出现这个问题.并感谢upvote! (3认同)
  • 您可以将每个组本身转换为不可变对象.每个对象只需要获取一些参数,因此虽然参数的实际数量保持不变,但任何单个构造函数中使用的参数数量都将减少. (2认同)
  • +1 @ssg:每当我设法说服自己这样的事情时,我已经证明自己错了,因为需要这个级别的参数的大方法驱动有用的抽象.埃文斯的DDD书可能会给你一些关于如何思考这个问题的想法(虽然你的系统听起来可能与这些模式的应用相差甚远) - (无论哪种方式,它都只是一本好书). (2认同)

Jef*_*dge 10

只需将参数数据结构从a更改class为a 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) 
Run Code Online (Sandbox Code Playgroud)

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

优点:

  • 最容易实现
  • 底层力学中行为的最小变化

缺点:

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

  • 我认为暴露字段是邪恶的规则适用于面向对象设计中的对象.我提出的结构只是一个裸金属容器的参数.既然你的本能是不可改变的,我认为这样一个基本的容器可能适合这种场合. (8认同)
  • @JeffreyLWhitledge:我真的不喜欢暴露字段结构是邪恶的想法。在我看来,这种说法就等于说螺丝刀是邪恶的,人们应该使用锤子,因为螺丝刀的尖端会凹陷钉头。如果需要钉钉子,就应该使用锤子,但是如果需要钉螺丝,就应该使用螺丝刀。在很多情况下,暴露字段结构正是完成这项工作的正确工具。顺便说一句,具有 get/set 属性的结构真正是正确工具的情况要少得多(在大多数情况下...... (2认同)

mar*_*rto 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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 解决方案中的参数对象不是不可变的.保存构建器的人甚至可以在构建之后编辑参数 (5认同)
  • 这如何解决"参数太多"问题?语法可能不同,但问题看起来是一样的.这不是批评,我只是好奇,因为我不熟悉这种模式. (2认同)
  • 对于@astef 的观点,正如当前编写的那样,“DoSomeActionParameters.Builder”实例可用于创建和配置一个“DoSomeActionParameters”实例。调用“Build()”后,对“Builder”属性的后续更改将继续修改原始“DoSomeActionParameters”实例的属性,并且对“Build()”的后续调用将继续返回相同的“DoSomeActionParameters”实例。它实际上应该是“public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); 返回旧对象;}`。 (2认同)

tru*_*ity 6

为什么不只是创建一个强制不变性的界面(即只有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; }        
}
Run Code Online (Sandbox Code Playgroud)

并且函数声明变为:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)
Run Code Online (Sandbox Code Playgroud)

优点:

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

缺点:

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

  • 我不喜欢那种方法.引用"IDoSomeActionParameters"的任意实现并希望捕获其中的值的人无法知道保持引用是否足够,或者是否必须将值复制到其他对象.可读接口在某些上下文中很有用,但不能作为使事物不可变的手段. (2认同)

Lyn*_*ite 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")
Run Code Online (Sandbox Code Playgroud)

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

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