【问题标题】:What's the point of DSLs / fluent interfacesDSL/流利的接口有什么意义
【发布时间】:2010-10-09 22:26:22
【问题描述】:

我最近正在观看一个关于 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# 中不存在的命名方法参数吗?命名参数是否会使流利的接口过时,例如类似 Objective-C 提供的东西:

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

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

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

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

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

【问题讨论】:

  • +1 我也认为他们目前被滥用了。

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


【解决方案1】:

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

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

如果您正在使用 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】:

    考虑:

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

    这需要重载的构造函数才能达到相同的效果。

    【讨论】:

    • 所以流畅接口的一个优点是它确保了代码的可读性(而传统接口可能更多地是关于接口/api的可用性)。
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

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

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

      我最近在 Windows 窗体的 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);
      

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

      希望对你有帮助

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

          【解决方案6】:

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

          【讨论】:

            【解决方案7】:

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

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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-03-15
              • 1970-01-01
              • 2010-10-14
              • 1970-01-01
              • 1970-01-01
              • 2011-04-27
              • 2015-06-18
              • 2023-02-02
              相关资源
              最近更新 更多