DSL /流畅接口的重点是什么?

时间:2009-02-25 21:37:40

标签: c# dsl fluent-interface api-design

我最近正在观看关于how to create a fluent DSL的网络直播,我不得不承认,我不明白为什么会使用这种方法(至少对于给定的例子)。

网络广播提供了一个图像大小调整类,它允许您指定输入图像,调整大小并使用以下语法将其保存到输出文件(使用C#):

Sizer sizer = new Sizer();
sizer.FromImage(inputImage)
     .ToLocation(outputImage)
     .ReduceByPercent(50)
     .OutputImageFormat(ImageFormat.Jpeg)
     .Save();

我不明白这比采用某些参数的“传统”方法更好:

sizer.ResizeImage(inputImage, outputImage, 0.5, ImageFormat.Jpeg);

从可用性的角度来看,这似乎更容易使用,因为它清楚地告诉您该方法期望作为输入。相比之下,使用流畅的界面,没有什么可以阻止你省略/忘记参数/方法调用,例如:

sizer.ToLocation(outputImage).Save();

关于我的问题:

1 - 有没有办法提高流利界面的可用性(即告诉用户他应该做什么)?

2 - 这种流畅的界面方法是否只是C#中现有的命名方法参数的替代?命名参数是否会使流畅的接口过时,例如类似的目标-C提供:

sizer.Resize(from:input, to:output, resizeBy:0.5, ..)

3 - 流畅的界面是否过度使用仅仅是因为它们目前很受欢迎?

4 - 或者它只是为网络广播选择的一个不好的例子?在这种情况下,请告诉我这种方法的优点是什么,使用它的意义何在。

BTW:我知道jquery,看看它有多容易,所以我不是在寻找关于那个或其他现有例子的评论。

我更需要一些(一般)评论来帮助我理解(例如)何时实现流畅的界面(而不是经典的类库),以及在实现它时要注意什么。

7 个答案:

答案 0 :(得分:13)

  

2 - 这种流畅的界面方法   只是替代非   现有的命名方法参数   C#?命名参数会流畅吗?   接口过时,例如某物   类似的目标-C提供:

是的,不是。流畅的界面为您提供了更大的灵活性。使用命名参数无法实现的是:

sizer.FromImage(i)
 .ReduceByPercent(x)
 .Pixalize()
 .ReduceByPercent(x)
 .OutputImageFormat(ImageFormat.Jpeg)
 .ToLocation(o)
 .Save();

流体界面中的FromImage,ToLocation和OutputImageFormat,给我一点气味。相反,我会在这些方面做一些事情,我认为这一点更清楚。

 new Sizer("bob.jpeg") 
 .ReduceByPercent(x)
 .Pixalize()
 .ReduceByPercent(x)
 .Save("file.jpeg",ImageFormat.Jpeg);

Fluent接口在许多编程技术中都存在同样的问题,它们可能被滥用,过度使用或未充分利用。我认为,当有效地使用这种技术时,它可以创建更丰富,更简洁的编程模型。甚至StringBuilder也支持它。

var sb = new StringBuilder(); 
sb.AppendLine("Hello")
 .AppendLine("World"); 

答案 1 :(得分:8)

我会说流利的界面略显过度,我认为你只选择了一个这样的例子。

当您使用它构建复杂模型时,我发现流畅的界面特别强大。模型我的意思是,例如实例化对象的复杂关系。然后,流畅的接口是引导开发人员正确构造语义模型实例的一种方式。这样一个流畅的界面是将模型的机制和关系与用于构建模型的“语法”分离的绝佳方式,从根本上屏蔽了最终用户的细节,并将可用的动词减少到可能只是那些相关的动词。特殊情况。

你的例子看起来有点像矫枉过正。

我最近在Windows Forms的SplitterContainer上做了一些流畅的界面。可以说,控件层次结构的语义模型在某种程度上很难构建。通过提供一个小的流畅的API,开发人员现在可以声明性地表达他的SplitterContainer应该如何工作。用法就像

var s = new SplitBoxSetup();
s.AddVerticalSplit()
 .PanelOne().PlaceControl(()=> new Label())
 .PanelTwo()
 .AddHorizontalSplit()
 .PanelOne().PlaceControl(()=> new Label())
 .PanelTwo().PlaceControl(()=> new Panel());
form.Controls.Add(s.TopControl);

我现在已经将控制层次结构的复杂机制简化为几个与手头问题相关的动词。

希望这有帮助

答案 2 :(得分:5)

考虑:

sizer.ResizeImage(inputImage, outputImage, 0.5, ImageFormat.Jpeg);

如果使用不太清晰的变量名称会怎样:

sizer.ResizeImage(i, o, x, ImageFormat.Jpeg);

想象一下,你已经打印出这个代码了。由于您无法访问方法签名,因此很难推断出这些参数是什么。

通过流畅的界面,这一点更加清晰:

 sizer.FromImage(i)
 .ToLocation(o)
 .ReduceByPercent(x)
 .OutputImageFormat(ImageFormat.Jpeg)
 .Save();

此外,方法的顺序并不重要。这相当于:

 sizer.FromImage(i)
 .ReduceByPercent(x)
 .OutputImageFormat(ImageFormat.Jpeg)
 .ToLocation(o)
 .Save();

此外,也许您可​​能有输出图像格式的默认值和缩减,因此可能会变为:

 sizer.FromImage(i)
 .ToLocation(o)
 .Save();

这将需要重载的构造函数来实现相同的效果。

答案 3 :(得分:2)

这是实现目标的一种方式。

对于除了一遍又一遍地操纵同一个项目之外什么都不做的对象,它没有什么问题。考虑C ++ Streams:它们是这个界面的终极。每个操作都会再次返回流,因此您可以将另一个流操作链接在一起。

如果您正在进行LINQ,并且反复操作对象,这是有道理的。

但是,在您的设计中,您必须要小心。如果你想中途偏离,应该采取什么行为? (IE,

var obj1 = object.Shrink(0.50); // obj1 is now 50% of obj2
var obj2 = object.Shrink(0.75); // is ojb2 now 75% of ojb1 or is it 75% of the original?

如果obj2是原始对象的75%,那么这意味着你每次都要制作一个完整的对象副本(并且在许多情况下都有它的优点,就像你试图制作两个相同的实例一样)事情,但略有不同)。

如果方法只是操纵原始对象,那么这种语法有点不诚实。这些是对对象的操作,而不是操作来创建更改的对象。

并非所有课程都像这样工作,做这种设计也没有意义。例如,这种设计风格在硬件驱动程序的设计或GUI应用程序的核心中几乎没有用处。只要设计只涉及操纵某些数据,这种模式就不错了。

答案 4 :(得分:2)

你应该阅读Eric Evans的Domain Driven Design来了解为什么DSL被认为是好的设计选择。

本书充满了很好的例子,最佳实践建议和设计模式。强烈推荐。

答案 5 :(得分:1)

可以在Fluent界面上使用变体来强制执行可选参数的某些组合(例如,要求至少存在一个组中的一个参数,并要求如果指定了某个参数,则必须省略其他一些参数)。例如,可以提供类似于Enumerable.Range的功能,但语法如IntRange.From(5).Upto(19)或IntRange.From(5).LessThan(10).Stepby(2)或IntRange( 3).Count之间(19).StepBy(17)。过度复杂的参数需求的编译时执行可能需要定义烦人数量的中间值结构或类,但在某些情况下,该方法在更简单的情况下证明是有用的。

答案 6 :(得分:0)

@sam-saffron关于添加新操作时Fluent界面的灵活性的建议之后:

如果我们需要添加一个新操作,例如Pixalize(),那么,在'带多个参数的方法'场景中,这将需要将新参数添加到方法签名中。这可能需要修改整个代码库中此方法的每次调用,以便为此新参数添加值(除非使用的语言允许可选参数)。

因此,Fluent界面的一个可能的好处是限制未来变化的影响。