DSL / 流畅接口的用途是什么?

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,也看到了它使事情变得容易,所以我不需要有关它或其他现有示例的评论。

我更希望得到一些(通用)评论,以帮助我理解例如何实现流畅接口(而不是经典的类库),以及在实现流畅接口时要注意什么。


1
+1 我也认为它们目前被过度使用了。 - Bjorn Reppen
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);

流畅接口和许多编程技术一样,存在被误用、过度使用或者未充分利用的问题。我认为,当这种技术被有效地使用时,它可以创建更丰富、更简洁的编程模型。甚至StringBuilder也支持它。

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

9
我认为流畅接口有点过度,你选择的只是其中一个例子。
我发现当你使用它构建一个复杂的模型时,流畅接口特别强大。这里的模型是指实例化对象的复杂关系。流畅接口是引导开发人员正确构建语义模型实例的一种方式。这样的流畅接口是将模型的机制和关系与用于构建模型的“语法”分离的绝佳方法,从而将细节屏蔽对最终用户不可见,并将动词限制在特定场景下可能仅涉及的那些动词上。
你的例子似乎有点过度设计。
最近我在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);

我现在将控制层次结构的复杂机制简化为与问题相关的几个动词。希望这可以帮助你。

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

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

1
流畅接口的一个优点是确保代码可读性(而传统接口可能更多关注接口/ API 的可用性)。 - M4N

2

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

对于那些只是反复操作同一项内容的对象,这种方法并没有什么问题。考虑C++流:它们是这种接口的终极体。每个操作都会再次返回流,因此您可以链接另一个流操作。

如果您正在使用LINQ,并且反复操纵一个对象,那么这是有道理的。

但是,在设计时,您必须小心。如果您想在中途偏离原来的操作,行为应该是什么?(即,

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应用程序的核心中,这种设计风格几乎没有任何用处。只要设计涉及操纵一些数据,这种模式就不是一个坏选择。

2

为了了解为什么DSL被认为是良好的设计选择,您应该阅读Eric Evans的领域驱动设计

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


1

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


0

关于@sam-saffron提出的流畅接口在添加新操作时的灵活性:

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

因此,流畅接口的一个可能的好处是限制未来更改的影响。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接