如何避免APIdevise中“参数太多”的问题?
我有这个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; }
这减less了我的API声明:
public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)
尼斯。 看起来很无辜,但我们实际上引入了一个巨大的变化:我们引入了可变性。 因为我们以前一直在做的是实际上传递一个匿名的不可变的对象:函数参数在堆栈上。 现在我们创build了一个非常可变的新课程。 我们创build了操纵调用者状态的能力。 这很糟糕。 现在我想让我的对象不可改变,我该怎么办?
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 } }
正如你所看到的,我实际上重新创build了我原来的问题:太多的参数。 很明显,这不是要走的路。 我该怎么办? 实现这种不变性的最后一个选项是使用这样的“只读”结构:
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; }
这使我们可以避免参数过多的构造函数,并实现不变性。 其实它修复了所有的问题(参数sorting等)。 然而:
- 每个人(包括FXCop和Jon Skeet)都认为暴露公共领域是不好的 。
- 埃里克·利珀特(Eric Lippert)等人说, 依靠只读字段来实现不变性是一个谎言 。
那是当我感到困惑,并决定写这个问题:什么是最简单的方式在C#避免“太多参数”的问题,而不引入可变性? 有没有可能为此目的使用只读结构,但没有一个糟糕的APIdevise?
澄清:
- 请假设没有违反单一的责任原则。 在我原来的情况下,函数只是将给定的参数写入单个数据库logging。
- 我不是在寻求一个特定的解决scheme给定的function。 我正在寻求对这些问题的一种广义的方法。 我特别感兴趣的是解决“太多参数”的问题,而不引入可变性或可怕的devise。
UPDATE
这里提供的答案有不同的优点/缺点。 因此,我想将其转换为社区维基。 我认为每一个代码示例和Pros / Cons的答案都可以为将来的类似问题提供一个很好的指导。 我现在试图找出如何做到这一点。
使用构build器和特定领域语言风格的API – Fluent接口的组合。 这个API稍微冗长一些,但是通过intellisense,input起来非常快,而且易于理解。
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());
框架中所包含的一种风格通常就像将相关的参数分组到相关的类中一样(但是在可变性方面又有问题):
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
你在那里有一个非常确定的迹象表明,这个class级违反了单一责任原则,因为它有太多的依赖关系。 寻找方法将这些依赖关系重构为Facade Dependencies的集群。
只要将您的参数数据结构从一个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)
该方法现在将得到它自己的结构副本。 该方法无法观察对参数variables所做的更改,并且更改该方法对该variables调用者无法观察到的variables。 隔离是不可变的。
优点:
- 最容易实施
- 基础力学行为的最小变化
缺点:
- 不变性不明显,需要开发者注意。
- 不必要的复制,以保持不变性
- 占用堆栈空间
如何在数据类中创build构build器类? 数据类将所有的设置器设置为私有,只有构build器才能设置它们。
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(); } }
我不是一个C#程序员,但我相信C#支持命名参数:(F#和C#在很大程度上是functioncompatable这样的事情)它确实: 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没有;
为什么不build立一个强制不变性的接口(即只有getter)呢?
这本质上是你的第一个解决scheme,但你强制该函数使用接口来访问参数。
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
解决scheme没有堆栈空间问题 - 使用语言语义的自然解决scheme
- 不变性是显而易见的
- 灵活(消费者可以使用不同的课程)
缺点:
- 一些重复的工作(在两个不同的实体中相同的声明)
- 开发人员必须猜测,
DoSomeActionParameters
是一个可以映射到IDoSomeActionParameters
我知道这是一个古老的问题,但我想我会考虑我的build议,因为我只需要解决同样的问题。 现在,我承认我的问题与您的问题稍有不同,因为我不希望用户自己构build这个对象(所有数据的水合来自数据库,所以我可以在内部监禁所有的构造)。 这允许我使用私有构造函数和以下模式;
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; } } }
除了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的人来说,创build孩子可以如此简单:
pid_t child = fork(); if (child == 0) { execl("/bin/echo", "Hello world from child", NULL); } else if (child != 0) { handle_error(); }
每一个deviseselect代表了它可能做什么操作的权衡。
PS。 是的 – 它类似于build设者 – 只是在反向(即在被调用方,而不是主叫方)。 在这个具体情况下,build设者可能也可能不会更好。
这里和Mikeys稍有不同,但是我想要做的是尽可能less的写所有东西
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是不可变的,因为它的默认构造函数是私有的,所以不能直接创build
初始化器不是一成不变的,而只是一个运输工具
这个用法利用了初始化器的初始化器(如果你得到我的漂移)并且我可以在初始化器的默认构造器
DoSomeAction(new DoSomeActionParameters.Initializer { A = "Hello", B = 42 } .Create());
这里的参数是可选的,如果你想要一些参数,你可以把它们放在Initializer的默认构造函数中
validation可以在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());
还有一点我知道,但无论如何要尝试
编辑:将创build方法移动到参数对象中的一个静态,并添加一个委托,通过初始化程序将一些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 => { iA = "Hello"; }) );
尽pipe取决于DoSomeAction
方法的复杂性,但您可以使用Builder风格的方法,这可能是一个重量级的触摸。 沿着这些线路的东西:
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);
使用该结构,而不是公共领域,具有公共属性:
•每个人(包括FXCop和Jon Skeet)都认为暴露公共领域是不好的。
乔恩和FXCop将被满足,因为你暴露的领域不是领域。
•Eric Lippert等人说依赖只读字段来实现不变性是一个谎言。
埃里克将是令人满意的,因为使用属性,你可以确保值只设置一次。
private bool propC_set=false; private date pC; public date C { get{ return pC; } set{ if (!propC_set) { pC = value; } propC_set = true; } }
一个半不可变的对象(值可以设置,但不能改变)。 适用于值和引用types。
当我遇到同样的问题时,我在项目中使用了Samuel答案的一个变体:
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方法不允许所有可能的组合,只是暴露了它们的常见组合。 这是因为我的一些参数非常复杂,而且为所有可能的情况编写方法都是困难和不必要的(很less使用疯狂的组合)。