27

我最近正在观看有关如何创建流畅的 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# 中不存在的命名方法参数的替代品?命名参数是否会使流利的接口过时,例如类似的objective-C提供的东西:

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

3 -流畅的界面是否仅仅因为它们目前很流行而被过度使用?

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

顺便说一句:我知道 jquery,并且看到它使事情变得多么容易,所以我不是在寻找关于那个或其他现有示例的评论。

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

4

7 回答 7

13

2 - 这种流畅的接口方法是否只是 C# 中不存在的命名方法参数的替代品?命名参数是否会使流利的接口过时,例如类似的objective-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"); 
于 2009-02-25T22:24:13.577 回答
9

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

当你用它构建一个复杂的模型时,我发现流利的接口特别强大。对于模型,我的意思是例如实例化对象的复杂关系。流利的接口是指导开发人员正确构建语义模型实例的一种方式。这样一个流畅的界面是将模型的机制和关系与用于构建模型的“语法”分开的极好方法,从本质上屏蔽了最终用户的细节,并将可用的动词减少到可能只是那些相关的动词。特定场景。

你的例子似乎有点矫枉过正。

我最近在 Windows Forms 的 SplitterContainer 上做了一些流畅的界面。可以说,控制层次结构的语义模型很难正确构建。通过提供一个小的 fluent 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);

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

希望这可以帮助

于 2009-02-25T22:34:14.877 回答
7

考虑:

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

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

于 2009-02-25T21:53:12.760 回答
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 应用程序的核心设计中几乎没有用处。只要设计只涉及操作一些数据,这种模式就不错。

于 2009-02-25T21:52:37.647 回答
2

您应该阅读Domain Driven DesignEric Evans 的文章,以了解为什么 DSL 被认为是好的设计选择。

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

于 2009-03-01T23:11:25.520 回答
1

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

于 2011-01-25T18:21:12.350 回答
0

@sam-saffron关于在添加新操作时流畅界面的灵活性的建议:

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

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

于 2013-04-06T14:40:20.720 回答