为什么链式装订工非常规?


46

在bean上实现链接非常方便:无需重载构造函数,大型构造函数,工厂,并提高了可读性。我想不出任何弊端,除非您希望对象是不可变的,在这种情况下,它就不会有任何设置方法。那么,这不是OOP约定的原因吗?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");

32
因为Java可能是
二传手

11
这是不好的样式,因为返回值在语义上没有意义。这是误导。唯一的好处是可以节省很少的击键。
usr

58
这种模式并不罕见。甚至有个名字。这就是所谓的流畅接口
菲利普

9
您也可以在构建器中抽象此创建内容,以获得更易读的结果。myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();我会这样做,以免与二传手就是空白的一般观念相抵触。

9
@Philipp,虽然从技术上讲您是正确的,但不会说那new Foo().setBar('bar').setBaz('baz')感觉很“流利”。我的意思是,肯定可以完全一样地实施,但我非常希望能读到更多类似的东西Foo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Answers:


50

那么,为什么这不是OOP约定呢?

我最好的猜测:因为它违反了CQS

您已经将命令(更改对象的状态)和查询(返回状态的副本-在这种情况下,对象本身)混合到了同一方法中。这不一定是问题,但确实违反了一些基本准则。

例如,在C ++中,std :: stack :: pop()是返回void的命令,而std :: stack :: top()是返回对堆栈顶部元素的引用的查询。传统上,您希望将两者结合起来,但是您不能这样做并且要保证例外安全。(在Java中不是问题,因为Java中的赋值运算符不会抛出)。

如果DTO是值类型,则可能会达到类似的目的

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

同样,在处理继承时,链接返回值是一个巨大的痛苦。请参阅“奇怪的重复模板模式”

最后,存在一个问题,默认构造函数应让您保留处于有效状态的对象。如果必须运行一堆命令以将对象恢复到有效状态,则说明出现了非常错误的情况。


4
最后,这里是真正的船长。继承是真正的问题。我认为链接可能是组合中使用的数据表示对象的常规设置方法。
2016年

6
“从对象返回状态副本”-不会这样做。
user253751 '16

2
我认为遵循这些准则的最大原因(除了迭代器等一些值得注意的例外)是,它使代码的推理变得非常容易。
jpmc26 2016年

1
@MatthieuM。不。我主要讲Java,并且在高级“流体” API中很流行-我确信它在其他语言中也存在。本质上,您可以在扩展自身的类型上使类型通用。然后,您添加一个abstract T getSelf()带有返回通用类型的方法。现在,您而不是this从设置器中返回return getSelf(),然后再由任何覆盖的类仅使类型本身具有泛型,并this从返回getSelf。这样,setter才返回实际​​类型,而不是声明类型。
蜘蛛鲍里斯(Boris the Spider)

1
@MatthieuM。真?听起来类似于C ++的CRTP ...
没用,

33
  1. 节省一些击键并不引人注目。可能不错,但是OOP约定更关心概念和结构,而不是按键。

  2. 返回值是没有意义的。

  3. 由于用户可能期望返回值具有意义,因此返回值甚至比毫无意义的还容易产生误导。他们可能期望这是一个“不变的二传手”

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }
    

    实际上,安装员会更改对象。

  4. 它不能很好地继承。

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 
    

    我会写

    new BarHolder().setBar(2).setFoo(1)

    但不是

    new BarHolder().setFoo(1).setBar(2)

对我来说,#1至#3是重要的。编写良好的代码不是关于排列整齐的文本。编写良好的代码是有关基本概念,关系和结构的代码。文本只是代码真正含义的外在反映。


2
但是,大多数这些论点适用于任何流畅/可链接的接口。
凯西

@Casey,是的。建设者(即有二传手)是我看到的最常见的连锁形式。
Paul Draper

10
如果您熟悉流畅接口的概念,则不适用#3,因为您可能会怀疑返回类型的流畅接口。我还必须不同意“编写良好的代码不是关于排列合理的文本”。那不是全部,但排列整齐的文本非常重要,因为它使程序的普通读者可以轻松地理解它。
Michael Shaw

1
设置器链接不是关于愉快地安排文本或保存击键。这是关于减少标识符的数量。使用setter链接,您可以在单个表达式中创建和设置对象,这意味着您可能不必将其保存在变量中(您将不得不命名该变量),并且该变量将一直保留到作用域末尾。
Idan Arye

3
关于第4点,在Java中可以通过以下方法解决此问题:public class Foo<T extends Foo> {...}setter返回Foo<T>。还有那些“设置”方法,我更喜欢将它们称为“ with”方法。如果重载工作正常,则为Foo<T> with(Bar b) {...},否则为Foo<T> withBar(Bar b)
2016年

12

我认为这不是OOP约定,它与语言设计及其约定有关。

看来您喜欢使用Java。Java具有JavaBeans规范,该规范将setter的返回类型指定为void,即它与setter的链冲突。该规范被广泛接受并以多种工具实现。

当然,您可能会问,为什么没有链接规范的一部分。我不知道答案,也许那时这种模式还不为人所知。


是的,JavaBeans正是我想到的。
2016年

我不认为大多数使用JavaBeans的应用程序都很在意,但是它们可能会(使用反射来捕获名为“ set ...”,具有单个参数并且返回void的方法)。许多静态分析程序抱怨无法检查返回值的方法的返回值,这可能会很烦人。

@MichaelT反射调用Java中的get方法未指定返回类型。调用以这种方式返回的方法,如果该方法指定为其返回类型Object,则可能导致Void返回类型的单例void。在其他新闻中,显然,“留下评论而不是投票”是未能通过“第一篇文章”审核的理由,即使这是一个很好的评论也是如此。我为此怪怪。

7

正如其他人所说的,这通常被称为流利的界面

通常,setter是调用程序传入的变量,以响应应用程序中的逻辑代码。您的DTO班就是一个例子。设置程序什么都不返回的常规代码对此通常是最好的。其他答案已经解释了方式。

但是也有少数情况下,流畅的界面可能是一个很好的解决方案,这些有共同之处。

  • 常量通常传递给二传手
  • 程序逻辑不会更改传递给设置器的内容。

设置配置,例如fluent-nhibernate

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

使用特殊的TestDataBulderClasses(对象母亲)在单元测试中设置测试数据

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

但是,创建良好的流利界面非常困难,因此仅当您具有大量“静态”设置时,才值得这样做。流利的界面也不应与“普通”类混为一谈;因此,经常使用构建器模式。


5

我认为在大多数情况下将一个setter链接在一起不是一个惯例,这是因为在这种情况下,在构造函数中看到options对象或参数更为典型。C#也具有初始化程序语法。

代替:

DTO dto = new DTO().setFoo("foo").setBar("bar");

一个人可能会这样写道:

(在JS中)

var dto = new DTO({foo: "foo", bar: "bar"});

(在C#中)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(在Java中)

DTO dto = new DTO("foo", "bar");

setFoosetBar随后不再需要初始化,并且以后可以用于突变。

虽然可链接性在某些情况下很有用,但不要为了减少换行符而将所有内容都塞在一行上,这一点很重要。

例如

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

使得阅读和理解正在发生的事情变得更加困难。重新格式化为:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

更容易理解,并使第一个版本中的“错误”更加明显。将代码重构为该格式后,就没有真正的优势了:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");

3
1.我真的不同意可读性问题,在每次调用之前都没有添加一个实例,这根本无法使它更加清晰。2.您使用js和C#显示的初始化模式与构造函数无关:您在js中执行的操作仅传递了一个参数,而在C#中执行的则是语法shugar,该脚本在后台调用了getter和setter以及Java没有C#那样的getter-setter shugar。
2016年

1
@Benedictus,我展示了OOP语言无需链接即可处理此问题的各种不同方式。并不是要提供相同的代码,而是要显示使链接不必要的替代方法。
zzzzBov

2
“在每次调用之前添加实例根本无法使其变得更加清晰”,我从未宣称在每次调用之前添加实例可以使任何事物变得更加清晰,我只是说这相对来说是相当的。
zzzzBov

1
VB.NET也有一个可用的“ With”关键字,它创建了编译器临时引用,因此,例如[ /用于表示换行符] With Foo(1234) / .x = 23 / .y = 47将等同于Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47。这样的语法不会造成歧义,因为.x它本身除了绑定到紧挨着的“ With”语句之外没有其他含义(如果没有,或者对象没有成员x,则.x毫无意义)。Oracle尚未在Java中包含任何类似内容,但这样的构造将很适合该语言。
超级猫

5

该技术实际上在Builder模式中使用。

x = ObjectBuilder()
        .foo(5)
        .bar(6);

但是,通常避免使用它,因为它不明确。返回值是对象(因此您可以调用其他设置器)还是返回对象是刚分配的值(也是常见模式),这一点并不明显。因此,“最不惊奇原则”建议您不要试图假设用户想要看到一个解决方案,除非它是对象设计的基础。


6
区别在于,在使用构建器模式时,您通常会编写一个只写对象(构建器),该对象最终将构造一个只读(不可变)对象(无论您要构建的是什么类)。就这一点而言,具有长链的方法调用是理想的,因为它可以用作单个表达式。
Darkhogg '16

“或者如果返回对象是刚刚分配的值”,我讨厌,如果我想保留刚传入的值,则将其首先放入变量中。但是,获得先前分配的值可能会很有用(我相信某些容器接口会这样做)。
JAB 2016年

@JAB我同意,我不喜欢这种表示法,但是有它的位置。浮现在脑海的一个是Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); 我想这也得到普及,因为它的镜子a = b = c = d,但我不相信,普及良好成立。我已经在一些原子操作库中看到了您提到的返回前一个值的代码。故事的道德启示?比我听起来还令人困惑=)
Cort Ammon

我相信学者们会有点little腐:这个成语没有内在的错误。在某些情况下,这对于增加清晰度非常有用。以以下文本格式设置类为例,该类缩进字符串的每一行并在其周围绘制一个ascii-art框new BetterText(string).indent(4).box().print();。在那种情况下,我绕过了一堆粗话,取了一个字符串,将其缩进缩进一个盒子,然后输出。您甚至可能希望在级联中使用复制方法(例如.copy()),以允许所有后续操作不再修改原始操作。
tgm1024 '18

2

这更多的是评论,而不是答案,但是我无法发表评论,所以...

只是想提一下,这个问题令我感到惊讶,因为我一点也不觉得这很罕见。实际上,在我的工作环境(Web开发人员)中非常常见。

举例来说,这是Symfony的学说如何:生成:实体命令自动生成所有的 setters默认情况下

jQuery 有点以非常相似的方式链接了大多数方法。


JS是一种憎恶

@Benedictus我想说PHP是更大的可憎之处。JavaScript是一种非常好的语言,并且由于包含ES6功能而变得非常不错(尽管我确定有些人仍然喜欢CoffeeScript或变体;就我个人而言,我不喜欢CoffeeScript在检查外部范围时如何处理变量作用域首先,而不是将在本地范围内分配给本地的变量视为本地,除非以Python的方式明确表示为非本地/全局。
JAB 2016年

我同意@@ Benedictus JAB。在我上大学的那年,我曾经看不起JS作为一种令人讨厌的脚本语言,但是在使用JS几年后,学习了正确的使用方式之后,我开始...实际上很喜欢它。而且我认为,有了ES6,它将成为一种非常成熟的语言。但是,让我转瞬即逝的是……首先,我的回答与JS有什么关系?^^ U
xDaizu

我实际上在js工作了几年,可以说我已经学到了方法。不过,我认为这种语言仅是一个错误。但我同意,Php更糟糕。
2016年

@Benedictus我理解你的立场,我尊重你的立场。不过我还是喜欢。当然,它有它的怪癖和特权(哪种语言没有?),但是它正在稳步朝着正确的方向迈进。但是... 我实际上并不喜欢它,我需要捍卫它。我不从人爱或恨它...哈哈哈此外,获得任何这是没有地方可言,甚至对JS我的回答wasnt。
xDaizu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.