我有这个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;
}
这允许我们避免具有太多参数的构造函数并实现不变性 . 实际上它解决了所有问题(参数排序等) . 然而:
-
每个人(包括FXCop和Jon Skeet)同意exposing public fields are bad .
-
Eric Lippert等人说relying on readonly fields for immutability is a lie .
那时我感到困惑并决定写下这个问题:在没有引入可变性的情况下,C#中最直接的方法是避免“太多参数”问题?是否有可能为此目的使用readonly结构,但没有错误的API设计?
CLARIFICATIONS:
-
请假设没有违反单一责任原则 . 在我原来的情况下,该函数只是将给定的参数写入单个DB记录 .
-
我想要对这些问题采取一般方法 . 我特别感兴趣的是解决"too many parameters"问题,而不会引入可变性或可怕的设计 .
UPDATE
这里提供的答案有不同的优点/缺点 . 因此,我想将其转换为社区维基 . 我认为代码示例和优点/缺点的每个答案都可以为将来的类似问题提供一个很好的指导 . 我现在正试图找出如何做到这一点 .
13 回答
除了manji响应之外 - 您可能还希望将一个操作拆分为几个较小的操作 . 相比:
和
对于那些不了解POSIX的人来说,创建孩子可以像以下一样简单:
每种设计选择都代表了它可能做什么操作的权衡 .
PS . 是的 - 它类似于构建器 - 仅反向(即在被叫方而不是调用方) . 在这种特定情况下,它可能会或可能不会比建造者更好 .
框架中包含的一种风格通常就像将相关参数分组到相关类中一样(但是又有问题可变性):
我知道这是一个古老的问题,但我认为我会接受我的建议,因为我只需要解决同样的问题 . 现在,我承认我的问题与你的问题略有不同,因为我有额外的要求,不希望用户自己构建这个对象(数据的所有水合都来自数据库,所以我可以在内部监禁所有构造) . 这允许我使用私有构造函数和以下模式;
为什么不只是创建一个强制不变性的界面(即只有getter)?
它本质上是您的第一个解决方案,但您强制该函数使用该接口来访问该参数 .
并且函数声明变为:
优点:
没有像
struct
解决方案那样的堆栈空间问题使用语言语义的自然解决方案
不变性是显而易见的
灵活(消费者可以根据需要使用不同的课程)
缺点:
一些重复的工作(两个不同实体中的相同声明)
Developer必须猜测
DoSomeActionParameters
是一个可以映射到IDoSomeActionParameters
的类如何在数据类中创建构建器类 . 数据类将所有setter设置为private,只有构建器才能设置它们 .
只需将参数数据结构从
class
更改为struct
,就可以了 .该方法现在将获得自己的结构副本 . 方法无法观察对参数变量所做的更改,并且调用方无法观察到方法对变量的更改 . 在没有不变性的情况下实现隔离 .
优点:
最容易实施
底层力学中行为的最小变化
缺点:
不变性不明显,需要开发人员注意 .
不必要的复制维护不变性
占用堆栈空间
你所拥有的是一个非常明确的迹象,表明有问题的类违反了Single Responsibility Principle,因为它有太多的依赖关系 . 寻找将这些依赖项重构为Facade Dependencies的集群的方法 .
当我遇到同样的问题时,我在项目中使用的Samuel's answer变体:
并使用:
在我的例子中,参数是有意修改的,因为setter方法不允许所有可能的组合,只是暴露了它们的常见组合 . 那是因为我的一些参数非常复杂,所有可能情况的编写方法都很困难且不必要(很少使用疯狂的组合) .
使用构建器和特定于域的语言样式API - Fluent Interface的组合 . API有点冗长,但是通过intellisense可以非常快速地输出并且易于理解 .
使用结构,但不是公共字段,具有公共属性:
Jon和FXCop将因为您曝光的不是字段而感到满意 .
Eric会因为使用属性而感到满意,您可以确保该值仅设置一次 .
一个半不可变对象(值可以设置但不能更改) . 适用于 Value 和参考类型 .
你可以使用Builder风格的方法,虽然取决于你的
DoSomeAction
方法的复杂性,这可能是一个触摸重量级 . 这些方面的东西:这里有一个与Mikeys稍微不同的一个,但我要做的是让整个事情尽可能少写
DoSomeActionParameters是不可变的,因为它可以并且不能直接创建,因为它的默认构造函数是私有的
初始化程序不是不可变的,而只是一个传输
该用法利用了初始化程序的初始化程序(如果你得到我的漂移)我可以在初始化程序默认构造函数中有默认值
这里的参数是可选的,如果你需要一些参数,可以将它们放在Initializer默认构造函数中
验证可以在Create方法中进行
所以现在看起来像
我知道还有一点kooki,但无论如何都要试试
编辑:将create方法移动到参数对象中的静态,并添加一个通过初始化程序的委托从调用中取出一些kookieness
所以这个电话现在看起来像这样
我不是C#程序员,但我相信C#支持命名参数:( F#和C#在很大程度上是特征兼容的)它确实:http://msdn.microsoft.com/en-us/library/dd264739.aspx#Y342
所以调用原始代码变为:
这不需要更多的空间/思考你的对象创建和所有的好处,事实上你没有改变在unerlying系统中发生的事情 . 您甚至不需要重新编码任何内容来指示参数已命名
编辑:这是我发现的关于它的艺术品 . http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/我应该提到C#4.0支持命名参数,3.0没有