我有这个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设计?
澄清:
更新
这里提供的答案有不同的优点/缺点。因此,我想将其转换为社区维基。我认为代码示例和优点/缺点的每个答案都可以为将来的类似问题提供一个很好的指导。我现在正试图找出如何做到这一点。
答案 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方法不允许所有可能的组合,只是暴露了它们的常见组合。那是因为我的一些参数非常复杂,所有可能情况的编写方法都很困难且不必要(很少使用疯狂的组合)。