我正在尝试用C#编写流畅的API。我想要做的是创建一个可以设置一些属性的构建器。可以按任何顺序设置属性,并且所有属性都是可选的。在设置了属性之后,仍然显示在intellisense中设置它的方法没有意义,所以如果已经设置它我想隐藏它。以下是我想要的只有3个属性的API类型的示例。
public interface ISoldier
{
string Name { get; set; }
string Rank { get; set; }
int SerialNumber { get; set; }
}
public interface ISoldierBuilder
{
IInterrogationStarter InterrogateSoldier();
}
public interface IInterrogationEnder
{
ISoldier FinishInterrogation();
}
// No properties set (3 that can still be set)
public interface IInterrogationStarter : IInterrogationEnder
{
IBuilderWithNameSet WithName(string value);
IBuilderWithRankSet WithRank(string value);
IBuilderWithSerialNumberSet WithSerialNumber(int value);
}
// One property set (2 that can still be set)
public interface IBuilderWithNameSet : IInterrogationEnder
{
IBuilderWithNameAndRankSet WithRank(string value);
IBuilderWithNameAndSerialNumberSet WithSerialNumber(int value);
}
public interface IBuilderWithRankSet : IInterrogationEnder
{
IBuilderWithNameAndRankSet WithName(string value);
IBuilderWithRankAndSerialNumberSet WithSerialNumber(int value);
}
public interface IBuilderWithSerialNumberSet : IInterrogationEnder
{
IBuilderWithNameAndSerialNumberSet WithName(string value);
IBuilderWithRankAndSerialNumberSet WithRank(string value);
}
// Two properties set (1 that can still be set)
public interface IBuilderWithNameAndRankSet : IInterrogationEnder
{
IInterrogationEnder WithSerialNumber(int value);
}
public interface IBuilderWithNameAndSerialNumberSet : IInterrogationEnder
{
IInterrogationEnder WithRank(string value);
}
public interface IBuilderWithRankAndSerialNumberSet : IInterrogationEnder
{
IInterrogationEnder WithName(string value);
}
我放弃了实施,因为不需要证明我要做的事情。现在,如果您实际将此代码复制到项目中,您可以看到我正在尝试做什么。一旦设置了属性,它就不再在intellisense的列表中可用。
ISoldierBuilder builder;
ISoldier soldier1 = builder.InterrogateSoldier()
.WithName("Bob")
.WithRank("Corporal")
.FinishInterrogation();
ISoldier soldier2 = builder.InterrogateSoldier()
.WithSerialNumber(12345)
.FinishInterrogation();
ISoldier soldier3 = builder.InterrogateSoldier()
.WithRank("Captain")
.WithSerialNumber(55533)
.WithName("John")
.FinishInterrogation();
例如,对于soldier1,一旦我们调用.WithName(“Bob”)。WithRank(“Corporal”),可用选项中剩下的就是:
WithSerialNumber
FinishInterrogation
再次设置名称会让人感到困惑 - 哪一个是我们需要放入ISoldier实例的实际名称?
现在想象一下尝试将这个简单的例子扩展到10个属性。我的问题是 - 如何设置我的可选属性,在设置它们之后从intellisense中消失而不会创建大量的接口,这些接口会随着属性的数量呈指数级增长?
更多情境
流畅的API的目标是使其更易于编写,更易于阅读和执行业务规则。如果我们还没有完成那些,那么创建一个流畅的API几乎没有意义。
长篇故事
在我的研究中,我发现流畅界面的定义是将单词一起流入一个句子并且可以大声说出的界面。然而,在实践中,真正流畅的API很少见,因为它们需要实施额外的工作量。一个典型的折衷方案是制作一个易于阅读,但不一定有意义的大声说话。
我想稍微超越一点,因为根据我自己对现有流畅的API实现的经验 - 我发现它们通常很难使用,需要相当多的研究来学习如何配置。所以我添加了一个额外的约束,API必须易于使用 - 最好是通过使用intellisense发现可用的选项。
此外,我发现使用流畅API的最令人信服的原因之一是能够在编译器级别强制执行某些业务规则。我阅读了Peter Vogel撰写的some great articles,其中详细介绍了如何实施业务规则,并使用其中一些技术来强制执行13 of the business rules我需要的应用程序除非遵循规则,否则将无法编译。这是使用流畅API的最佳论据,因为只是创建一个可读的API只不过是语法糖,但确保实施业务规则是一个更实际和功能性的用途。
此外,“可读性”是主观的,取决于您期望阅读API的人。我愿意在这里做出一些妥协,因为目标受众是开发者。
后面的故事是我已经实现了non-functional prototype,它强制执行我的几个业务规则,以及根据其上下文更改属性,因为它们在上下文更改时使用的方式不同。我希望有一些方法可以使用泛型,属性或其他一些编译器魔法来解决这个额外的业务规则,而不必使用许多额外的接口来支持它,然后在新属性时必须更改所有这些接口已添加。
流畅的API也将成为XML的替代品,用于语义标记对象的层次结构,这是其使用的另一个引人注目的论据。一些业务规则(尚未实现)处理从父对象到其后代对象的属性继承。
答案 0 :(得分:2)
我的问题是 - 如何设置我的可选属性,在设置它们之后从intellisense中消失而不会创建大量的接口,这些接口会随着属性的数量呈指数级增长?
你无法使用当前的方法,我也不会尝试。
另一种方法是使用带有属性的构建器类型而不是方法 - 这样当使用对象初始化程序时,编译器将阻止您更多地设置属性不止一次。所以:
var soldier = new Soldier.Builder {
SerialNumber = 12345,
Name = "John",
Rank = Rank.Captain // I'd use an enum for the ranks...
}.Build();
在对象初始化程序的一半,Intellisense将仅提示尚未设置的属性,但
在存在 required 属性的其他情况下,您可以将它们放在构造函数中并使用命名参数以便清晰:
var soldier = new Solider.Builder(name: "John", serialNumber: 12345) {
Rank = Rank.Captain,
SomeOtherOptionalProperty = "some value"
}.Build();