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

我最近正在观看关于如何创建流畅的DSL的网络直播,我不得不承认,我不明白为什么会使用这种方法(至少对于给定的例子)。

网络广播提供了一个图像大小调整类,它允许您指定输入图像,resize并使用以下语法将其保存到输出文件(使用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,看看它有多容易,所以我不是在寻找关于那个或其他现有例子的评论。

我正在寻找一些(一般)注释来帮助我理解(例如)何时实现流畅的接口(而不是经典的类库),以及实现它时需要注意的事项。

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

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

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

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

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

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

我最近在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); 

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

希望这可以帮助

考虑:

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

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

这是实现事物的一种方式。

对于除了一遍又一遍地操纵同一个项目之外什么都不做的对象,它没有什么问题。 考虑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应用程序的核心中几乎没有用处。 只要设计只涉及操纵某些数据,这种模式就不错了。

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

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

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

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

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

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