如何避免“参数过多” 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
}
}
正如你所看到的,我实际上重新创建了我原来的问题:参数太多。显然这不是正确的方法。我要做什么?实现这种不变性的最后一个选择是使用像这样的“只读”结构:
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)都同意暴露公共字段是不好的。
- Eric Lippert 等人说依靠只读字段来实现不变性是一个谎言。
就在那时,我感到困惑,决定写下这个问题:C# 中避免“参数过多”问题而不引入可变性的最直接方法是什么?是否可以使用只读结构来实现此目的,同时又不会出现糟糕的 API 设计?
澄清:
- 请假设没有违反单一责任原则。在我原来的情况下,该函数只是将给定的参数写入单个数据库记录。
- 我并不是在寻找给定函数的特定解决方案。我正在寻找解决此类问题的通用方法。我特别感兴趣的是在不引入可变性或糟糕设计的情况下解决“参数太多”问题。
更新
此处提供的答案有不同的优点/缺点。因此我想将其转换为社区维基。我认为每个带有代码示例和优点/缺点的答案都将为将来的类似问题提供很好的指导。我现在正在尝试找出如何做到这一点。
I have this API function:
public ResultEnum DoSomeAction(string a, string b, DateTime c, OtherEnum d,
string e, string f, out Guid code)
I don't like it. Because parameter order becomes unnecessarily significant. It becomes harder to add new fields. It's harder to see what's being passed around. It's harder to refactor method into smaller parts because it creates another overhead of passing all the parameters in sub functions. Code is harder to read.
I came up with the most obvious idea: have an object encapsulating the data and pass it around instead of passing each parameter one by one. Here is what I came up with:
public class DoSomeActionParameters
{
public string A;
public string B;
public DateTime C;
public OtherEnum D;
public string E;
public string F;
}
That reduced my API declaration to:
public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)
Nice. Looks very innocent but we actually introduced a huge change: we introduced mutability. Because what we previously had been doing was actually to pass an anonymous immutable object: function parameters on stack. Now we created a new class which is very mutable. We created the ability to manipulate the state of the caller. That sucks. Now I want my object immutable, what do I do?
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
}
}
As you can see I actually re-created my original problem: too many parameters. It's obvious that that's not the way to go. What am I going to do? The last option to achieve such immutability is to use a "readonly" struct like this:
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;
}
That allows us to avoid constructors with too many parameters and achieve immutability. Actually it fixes all the problems (parameter ordering etc). Yet:
- Everybody (including FXCop & Jon Skeet) agree that exposing public fields are bad.
- Eric Lippert et al say relying on readonly fields for immutability is a lie.
That's when I got confused and decided to write this question: What's the most straightforward way in C# to avoid "too many parameters" problem without introducing mutability? Is it possible to use a readonly struct for that purpose and yet not have a bad API design?
CLARIFICATIONS:
- Please assume there is no violation of single responsibiltiy principle. In my original case the function just writes given parameters to a single DB record.
- I'm not seeking a specific solution to the given function. I'm seeking a generalized approach to such problems. I'm specifically interested in solving "too many parameters" problem without introducing mutability or a terrible design.
UPDATE
The answers provided here have different advantages/disadvantages. Therefore I'd like to convert this to a community wiki. I think each answer with code sample and Pros/Cons would make a good guide for similar problems in the future. I'm now trying to find out how to do it.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(13)
使用构建器和特定领域语言风格 API 的组合——Fluent Interface。 API 有点冗长,但通过智能感知,输入速度非常快并且易于理解。
Use a combination of builder and domain-specific-language style API--Fluent Interface. The API is a little more verbose but with intellisense it's very quick to type out and easy to understand.
框架中采用的一种风格通常是将相关参数分组到相关类中(但又存在可变性问题):
One style embraced in the frameworks is usually like grouping related parameters into related classes (but yet again problematic with mutability):
只需将参数数据结构从
class
更改为struct
即可。该方法现在将获得其自己的结构副本。该方法无法观察到对参数变量所做的更改,并且调用者也无法观察到该方法对变量所做的更改。隔离是在没有不变性的情况下实现的。
优点:
缺点:
Just change your parameter data structure from a
class
to astruct
and you’re good to go.The method will now get its own copy of the structure. Changes made to the argument variable cannot be observed by the method, and changes the method makes to the variable can not be observed by the caller. Isolation is achieved without immutability.
Pros:
Cons:
您所拥有的内容非常明确地表明,相关类违反了单一职责原则,因为它有太多的依赖性。寻找将这些依赖项重构为外观依赖项集群的方法。
What you have there is a pretty sure indication that the class in question is violating the Single Responsibility Principle because it has too many dependencies. Look for ways to refactor those dependencies into clusters of Facade Dependencies.
我不是 C# 程序员,但我相信
C# 支持命名参数:(F# 支持,并且 C# 的功能在很大程度上兼容此类事情)
它确实:
http://msdn.microsoft.com/en-us/library/dd264739 .aspx#Y342
所以调用你的原始代码变成:
这不需要你的对象创建更多的空间/思想
并且具有所有的好处,即您根本没有改变根本系统中正在发生的事情。
您甚至不必重新编码任何内容来指示参数被命名为
“编辑”:
这是我找到的一篇关于它的文章。
http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/
我应该提到 C# 4.0 支持命名参数,3.0 不支持
I'm not a C# programmer but I believe
C# supports named arguments: (F# does and C# is largely feature compatable for that sort of thing)
It does:
http://msdn.microsoft.com/en-us/library/dd264739.aspx#Y342
So calling your original code becomes:
this takes no more space/thought that your object creation
and has all the benifits, of the fact that you haven't changed what is happening in the unerlying system at all.
You don't even have to recode anything to indicate the arguments are named
Edit:
here is a artical i found about it.
http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/
I should mention C# 4.0 supports named arguments, 3.0 did not
为什么不直接创建一个强制不变性的接口(即只有 getter)?
这本质上是您的第一个解决方案,但您强制函数使用接口来访问参数。
并且函数声明变为:
优点:
缺点:
DoSomeActionParameters
是一个可以映射到IDoSomeActionParameters
的类Why not just make an interface that enforces immutability (i.e. only getters)?
It's essentially your first solution, but you force the function to use the interface to access the parameter.
and the function declaration becomes:
Pros:
struct
solutionCons:
DoSomeActionParameters
is a class that could be mapped toIDoSomeActionParameters
如何在数据类中创建一个构建器类?数据类将所有设置器设为私有,并且只有构建器才能设置它们。
How about creating a builder class inside your data class. The data class will have all the setters as private and only the builder will be able to set them.
我知道这是一个老问题,但我想我应该提出我的建议,因为我刚刚必须解决同样的问题。现在,诚然,我的问题与您的问题略有不同,因为我有额外的要求,即不希望用户能够自己构造这个对象(所有数据的水合作用都来自数据库,因此我可以在内部禁止所有构造)。这允许我使用私有构造函数和以下模式;
I know this is an old question but I thought I'd wade in with my suggestion as I've just had to solve the same problem. Now, admittadly my problem was slightly different to yours as I had the additional requirement of not wanting users to be able to construct this object themselves (all hydration of the data came from the database, so I could jail off all construction internally). This allowed me to use a private constructor and the following pattern;
您可以使用构建器风格的方法,尽管根据
DoSomeAction
方法的复杂性,这可能是一个重量级的方法。沿着这些思路:You could use a Builder-style approach, though depending on the complexity of your
DoSomeAction
method, this might be a touch heavyweight. Something along these lines:除了 manji 响应之外 - 您可能还想将一项操作拆分为几个较小的操作。比较:
对于
那些不了解 POSIX 的人,子级的创建可以像以下一样简单:
每个设计选择都代表着对其可能执行的操作的权衡。
附言。是的 - 它类似于构建器 - 只是相反(即在被调用方而不是调用方)。在这种特定情况下,它可能会或可能不会比构建器更好。
In addition to manji response - you may also want to split one operation into several smaller ones. Compare:
and
For those who don't know POSIX the creation of child can be as easy as:
Each design choice represent trade-off over what operations it may do.
PS. Yes - it is similar to builder - only in reverse (i.e. on callee side instead of caller). It may or may not be better then builder in this specific case.
这是与 Mikeys 稍有不同的
但我想做的是让整个事情尽可能少写
DoSomeActionParameters 是不可变的,因为它可以是不可变的,不能直接创建,因为它的默认构造函数是私有的
初始化程序不是不可变的,但只是一个传输
用法需要初始化程序上的初始化程序的优点(如果你明白我的意思)
我可以在初始化程序默认构造函数中有默认值
参数在这里是可选的,如果您想要一些参数,您可以将它们放在初始化程序默认构造函数中
并且验证可以在 Create 方法中进行
所以现在看起来
仍然有点奇怪我知道,但无论如何都要尝试一下
编辑:将 create 方法移动到参数对象中的静态并添加传递初始化程序的委托会消除调用中的一些怪异
所以调用现在看起来像这样
here is a slightly different one from Mikeys
but what I am trying to do is make the whole thing as little to write as possible
The DoSomeActionParameters is immutable as it can be and cannot be created directly as its default constructor is private
The initializer is not immutable, but only a transport
The usage takes advantage of the initializer on the Initializer (if you get my drift)
And I can have defaults in the Initializer default constructor
The parameters will be optional here, if you want some to be required you could put them in the Initializer default constructor
And validation could go in the Create method
So now it looks like
Still a little kooki I know, but going to try it anyway
Edit: moving the create method to a static in the parameters object and adding a delegate which passes the initializer takes some of the kookieness out of the call
So the call now looks like this
使用结构体,但不使用公共字段,而使用公共属性:
Jon 和 FXCop 将会感到满意,因为您公开的是属性而不是字段。
Eric 会很满意,因为使用属性,您可以确保该值仅设置一次。
一个半不可变对象(值可以设置但不能更改)。适用于值和引用类型。
Use the structure, but instead of public fields, have public properties:
Jon and FXCop will be satisified because you are exposing properites not fields.
Eric will be satisifed because using properties, you can ensure that the value is only set once.
One semi-immutable object (value can be set but not changed). Works for value and Reference types.
Samuel 的答案 当我遇到同样的问题时,我在项目中使用了它:
并使用:
在我的情况下,参数是有意修改的,因为设置方法不允许所有可能的组合,并且只暴露了它们的常见组合。这是因为我的一些参数非常复杂,并且为所有可能的情况编写方法将是困难且不必要的(很少使用疯狂的组合)。
A variant of Samuel's answer that I used in my project when I had the same problem:
And to use:
In my case the parameters were intentionally modifiable, because the setter methods didn't allow for all possible combinations, and just exposed common combinations of them. That's because some of my parameters were pretty complex and writing methods for all possible cases would have been difficult and unnecessary (crazy combinations are rarely used).