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

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

如您所见,我实际上重新创建了我最初的问题: 参数太多。很明显,这不是解决问题的办法。我该怎么办?实现这种不可变性的最后一个选项是使用“ readonly”结构,如下所示:

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 # 中避免“参数过多”问题而不引入可变性的最直接的方法是什么?有没有可能在使用只读结构的同时,还没有糟糕的 API 设计?

澄清:

  • 请假设没有违反单一责任原则。在我最初的例子中,该函数只是将给定的参数写入单个 DB 记录。
  • 我不是在寻求给定函数的特定解。我正在寻找一种解决这类问题的通用方法。我特别感兴趣的是在不引入可变性或糟糕的设计的情况下解决“参数太多”的问题。

更新

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

38877 次浏览

框架中包含的一种风格通常类似于将相关参数分组到相关类中(但同样存在可变性问题) :

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

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

如何在数据类中创建生成器类。数据类将所有的 setter 作为私有的,并且只有构建器能够设置它们。

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

您可以使用 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);

结合使用生成器和特定于域的语言样式 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());

为什么不直接创建一个实现不可变性的接口(即只使用 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解决方案那样的堆栈空间问题
  • 使用语言语义的自然解
  • 不变性是显而易见的
  • 灵活(消费者可以使用不同的类,如果他想)

缺点:

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

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

•所有人(包括 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;
}
}

一个半不可变对象(值可以设置,但不能更改)。适用于值和引用类型。

我不是 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不支持

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

除了 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();
}

每个设计选择都代表了它可以做哪些操作的权衡。

附言。是的-它类似于反过来的只生成器(即在被调用方而不是调用方)。在这个特定的情况下,它可能比构建器更好,也可能不更好。

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

该方法现在将获得它自己的结构副本。方法无法观察对参数变量所做的更改,调用方也无法观察方法对变量所做的更改。没有不变性就可以实现隔离。

优点:

  • 最容易实现
  • 基础力学行为的最小变化

缺点:

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

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

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

这里有一个与 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);
}
}
}

DosomActionParameter 是不可变的,因为它的缺省构造函数是私有的,所以不能直接创建

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

这种用法利用了 Initializer 上的初始化器(如果您明白我的意思的话) 我可以在 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());

我知道还是有点古怪,但还是要试试

编辑: 将 create 方法移动到参数对象中的 static,并添加一个传递初始值设定项的委托,这样可以消除调用中的一些古怪之处

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