让制定者返回"这个"是不好的做法吗?

Ken*_*Liu 235 java oop api design-patterns

在java中使setter返回"this"是一个好主意还是坏主意?

public Employee setName(String name){
   this.name = name;
   return this;
}
Run Code Online (Sandbox Code Playgroud)

这种模式很有用,因为你可以链接这样的setter:

list.add(new Employee().setName("Jack Sparrow").setId(1).setFoo("bacon!"));
Run Code Online (Sandbox Code Playgroud)

而不是这个:

Employee e = new Employee();
e.setName("Jack Sparrow");
...and so on...
list.add(e);
Run Code Online (Sandbox Code Playgroud)

......但这有点违反标准惯例.我想这可能是值得的,因为它可以让那个setter做一些有用的东西.我已经看到这种模式使用了一些地方(例如JMock,JPA),但它似乎并不常见,并且通常仅用于定义良好的API,其中此模式随处可用.

更新:

我所描述的内容显然是有效的,但我真正想要的是关于这是否普遍可以接受,以及是否存在任何陷阱或相关最佳实践的一些想法.我知道Builder模式,但它比我描述的要多一些 - 正如Josh Bloch描述的那样,有一个关联的静态Builder类用于创建对象.

cle*_*tus 101

这不是一种糟糕的做法.这是一种越来越普遍的做法.如果您不想这样做,大多数语言都不需要您处理返回的对象,因此它不会更改"正常"的setter使用语法,但允许您将setter链接在一起.

这通常称为构建器模式或流畅的接口.

它在Java API中也很常见:

String s = new StringBuilder().append("testing ").append(1)
  .append(" 2 ").append(3).toString();
Run Code Online (Sandbox Code Playgroud)

  • 它也被称为火车残骸反模式.问题是,当空指针异常堆栈跟踪包含这样的行时,您不知道哪个调用返回null.这并不是说应该不惜一切代价避免链接,但要注意糟糕的库(特别是家庭酿造). (29认同)
  • 它经常在构建器中使用*,但我不会说"这是......被称为构建器模式". (24认同)
  • @ddimitrov只要你限制它返回**这**它永远不会是一个问题(只有第一次调用才能抛出NPE) (17认同)
  • 对我而言,流畅的接口的一些基本原理是它们使代码更容易阅读,这很有趣.我可以看到它写起来更方便,但有点让我觉得难以阅读.这是我唯一真正的分歧. (9认同)
  • **它更容易编写和阅读**假设您将换行符和缩进放在可读性会受到影响的地方!(因为它可以避免重复代码的冗余混乱,如`bla.foo.setBar1(...); bla.foo.setBar2(...)`当你可以写`bla.foo/*newline indented*/.setBar1( ...)/*在之前的setter*/.setBar2(...)`下面(不能在这样的SO注释中使用换行符:-( ...希望你能够考虑10个这样的setter或更复杂的调用) (4认同)
  • @AndreasDietrich同样,这使得可以使用调试器来逐步完成“培训” :-D (2认同)

ndp*_*ndp 83

总结一下:

  • 它被称为"流畅的界面",或"方法链".
  • 这不是"标准"Java,虽然你现在看到它更多(在jQuery中运行得很好)
  • 它违反了JavaBean规范,因此它会破坏各种工具和库,特别是JSP构建器和Spring.
  • 它可能会阻止JVM通常会做的一些优化
  • 有些人认为它清理代码,其他人认为它"可怕"

还没有提到其他几点:

  • 这违反了每个函数应该做一个(并且只有一个)事情的原则.您可能会或可能不会相信这一点,但在Java中我相信它运作良好.

  • IDE不会为您生成这些(默认情况下).

  • 我终于,这是一个真实的数据点.我使用像这样构建的库时遇到了问题.Hibernate的查询构建器就是现有库中的一个示例.由于Query的set*方法正在返回查询,因此仅通过查看签名如何使用它是不可能的.例如:

    Query setWhatever(String what);
    
    Run Code Online (Sandbox Code Playgroud)
  • 它引入了歧义:该方法是否修改了当前对象(您的模式),或者,Query可能是不可变的(非常流行且有价值的模式),并且该方法返回一个新方法.它只是使库更难使用,许多程序员不利用这个功能.如果setter是setter,那么如何使用它会更清楚.

  • 虽然我总体上同意,但我不同意这违反了"只做一件事"的原则.回归"这个"并不是复杂的火箭科学.:-) (7认同)
  • 顺便说一句,它是"流畅的",而不是"流畅的"...因为它可以让你构建一系列方法调用,如口语. (4认同)
  • 关于不变性的最后一点非常重要。最简单的例子是字符串。Java 开发人员期望在 String 上使用方法时,他们会得到全新的实例,而不是相同但经过修改的实例。对于流畅的接口,必须在方法文档中提到返回的对象是“this”而不是新实例。 (2认同)

qua*_*ial 77

我更喜欢使用'with'方法:

public String getFoo() { return foo; }
public void setFoo(String foo) { this.foo = foo; }
public Employee withFoo(String foo) {
  setFoo(foo);
  return this;
}
Run Code Online (Sandbox Code Playgroud)

从而:

list.add(new Employee().withName("Jack Sparrow")
                       .withId(1)
                       .withFoo("bacon!"));
Run Code Online (Sandbox Code Playgroud)

  • +1有趣的约定.我不会在我自己的代码中采用它,因为现在你似乎必须为每个类字段都有一个`get`,一个`set`和一个`with`.不过,它仍然是一个有趣的解决方案.:) (16认同)
  • @ AlikElzin-kilaka实际上我只是注意到Java 8中的java.time不可变类使用了这种模式,例如LocalDate.withMonth,withYear等. (4认同)
  • `with`前缀是一个不同的约定.正如@qualidafial举了一个例子.以`with`为前缀的方法不应该返回`this`,而是返回一个像当前实例一样的新实例但是`with`会改变.当您希望对象不可变时,这样做.所以当我看到一个前缀为`with`的方法时,我假设我将获得一个新对象,而不是同一个对象. (3认同)
  • 如果你把这个添加到`Project Lombok`的`@Getter/@ Setter`注释......这对链接来说太棒了.或者你可以使用很多像JQuery和Javascript恶魔使用的`Kestrel` combinator(https://github.com/raganwald/Katy). (2认同)

Tom*_*ift 75

我不认为它有什么特别的错误,这只是风格问题.它在以下情况下很有用:

  • 您需要一次设置多个字段(包括构建时)
  • 你知道在编写代码时需要设置哪些字段,以及
  • 您要设置的字段有许多不同的组合.

此方法的替代方法可能是:

  1. 一个巨型构造函数(缺点:您可能会传递大量的空值或默认值,并且很难知道哪个值对应于什么)
  2. 几个重载的构造函数(缺点:一旦你有多个构建器就变得笨重)
  3. 工厂/静态方法(缺点:与重载的构造函数相同 - 一旦存在多个构造函数,就变得难以处理)

如果你只想一次设置一些房产,我会说不值得返回'这个'.如果您以后决定返回其他内容,例如状态/成功指示符/消息,它肯定会失败.

  • 也许不是从一开始,但是制定者并不一定保持其原始目的.以前变量可能会变为包含多个变量或具有其他副作用的状态.某些setter可能会返回先前的值,如果失败对于异常来说太常见,则其他可能会返回失败指示符.这提出了另一个有趣的观点:如果你使用的工具/框架在返回值时无法识别你的setter怎么办? (15认同)
  • @Tom好点,这样做打破了getter和setter的"Java bean"约定. (10认同)
  • 这就是为什么Builder模式存在的原因,setters不应该返回一些内容,而是创建一个构建器,如果它需要看起来更好并且需要更少的代码:) (3认同)
  • 好吧,按照惯例,通常你不会从 setter 返回任何东西。 (2认同)
  • @TomClift打破"Java Bean"约定会导致任何问题吗?使用"Java Bean"约定的库是查看返回类型还是仅查看方法参数和方法名称. (2认同)

Luk*_*ane 25

如果您不想'this'从setter 返回但不想使用第二个选项,则可以使用以下语法来设置属性:

list.add(new Employee()
{{
    setName("Jack Sparrow");
    setId(1);
    setFoo("bacon!");
}});
Run Code Online (Sandbox Code Playgroud)

另外,我认为它在C#中略显清晰:

list.Add(new Employee() {
    Name = "Jack Sparrow",
    Id = 1,
    Foo = "bacon!"
});
Run Code Online (Sandbox Code Playgroud)

  • 双括号初始化可能有equals问题,因为它创建了一个匿名内部类; 请参阅http://www.c2.com/cgi/wiki?DoubleBraceInitialization (14认同)

Ste*_*e K 10

它不仅打破了getter/setter的惯例,还打破了Java 8方法参考框架.MyClass::setMyValue是一个BiConsumer<MyClass,MyValue>,myInstance::setMyValue是一个Consumer<MyValue>.如果你有你的setter return this,那么它不再是一个有效的实例Consumer<MyValue>,而是一个Function<MyValue,MyClass>,并且会导致使用对这些setter的方法引用(假设它们是void方法)来破坏.

  • 啊!**这完全是错误的!**这称为无效兼容性。返回值的 setter 可以充当 Consumer。https://ideone.com/ZIDy2M (4认同)
  • 如果Java有一些方法可以通过返回类型而不仅仅是JVM来重载,那将是非常棒的.您可以轻松绕过许多这些重大变化. (2认同)

Car*_*ers 8

我不懂Java,但我用C++做过.其他人说它会让线条很长很难读,但我已经这么做了很多次:

list.add(new Employee()
    .setName("Jack Sparrow")
    .setId(1)
    .setFoo("bacon!"));
Run Code Online (Sandbox Code Playgroud)

这甚至更好:

list.add(
    new Employee("Jack Sparrow")
    .Id(1)
    .foo("bacon!"));
Run Code Online (Sandbox Code Playgroud)

至少,我想.但是如果你愿意的话,欢迎你给我一个可怕的程序员.而且我不知道你是否被允许在Java中做到这一点.

  • 在链中的每个方法调用之后,源代码格式化程序可以用简单的//强制执行.它略微提升了你的代码,但没有那么多的水平重新格式化你的垂直系列语句. (2认同)

Noo*_*ilk 6

这种方案(双关语)被称为"流畅的界面",现在变得非常流行.这是可以接受的,但这不是我的一杯茶.


小智 6

因为它不返回void,所以它不再是有效的JavaBean属性setter.如果您是世界上使用可视化"Bean Builder"工具的七个人之一,或使用JSP-bean-setProperty元素的17个人之一,这可能很重要.


Mar*_*ian 5

至少在理论上,它可以通过设置调用之间的错误依赖性来破坏JVM的优化机制.

它应该是语法糖,但事实上可以在超级智能Java 43的虚拟机中产生副作用.

这就是为什么我投票不,不要使用它.

  • 如果您不知道,请测试.`-XX:+ UnlockDiagnosticVMOptions -XX:+ PrintInlining` java7 jdk肯定是内联链式方法,并且在将void setter标记为hot并将它们内联的过程中进行相同数量的迭代.你低估了JVM的操作码修剪算法的力量; 如果它知道你正在返回它,它将跳过jrs(java return语句)操作码并将其保留在堆栈中. (11认同)
  • 有意思......你可以稍微扩展一下吗? (10认同)
  • 想想超标量处理器如何处理并行执行.执行第二个`set`方法的对象依赖于第一个`set`方法,尽管程序员知道它. (2认同)
  • 我仍然没有跟随.如果您使用两个单独的语句设置Foo然后设置Bar,则您为其设置Bar的对象具有与您为其设置Foo的对象不同的状态.因此,编译器也无法并行化这些语句.至少,我没有看到如何在没有引入无根据的假设的情况下.(因为我不知道它,我不会否认Java 43确实在曾经的情况下进行并行化而不是另一种并且引入在一个案例中的无理假设,而不是另一个案例中的无理假设. (2认同)

Jin*_*won 5

这根本不是一个坏习惯.但它与JavaBeans Spec不相容.

并且有很多规范取决于那些标准访问器.

你总是可以让它们彼此共存.

public class Some {
    public String getValue() { // JavaBeans
        return value;
    }
    public void setValue(final String value) { // JavaBeans
        this.value = value;
    }
    public String value() { // simple
        return getValue();
    }
    public Some value(final String value) { // fluent/chaining
        setValue(value);
        return this;
    }
    private String value;
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以一起使用它们.

new Some().value("some").getValue();
Run Code Online (Sandbox Code Playgroud)

这是另一个不可变对象的版本.

public class Some {

    public static class Builder {

        public Some build() { return new Some(value); }

        public Builder value(final String value) {
            this.value = value;
            return this;
        }

        private String value;
    }

    private Some(final String value) {
        super();
        this.value = value;
    }

    public String getValue() { return value; }

    public String value() { return getValue();}

    private final String value;
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以做到这一点.

new Some.Builder().value("value").build().getValue();
Run Code Online (Sandbox Code Playgroud)

  • 我的编辑被拒绝,但是您的Builder示例不正确。首先,.value()不返回任何内容,甚至不设置“ some”字段。其次,您应该在build()中添加一个安全措施并将“ some”设置为“ null”,以便“ Some”确实是不可变的,否则您可以在同一Builder实例上再次调用“ builder.value()”。最后,是的,您有一个构建器,但是您的`Some`仍然有一个公共构造器,这意味着您不公开提倡使用该构建器,即用户除了尝试或寻找一种方法来了解它之外,一无所知。设置一个自定义的“值”。 (2认同)

Mar*_*zak 5

如果您在整个应用程序中使用相同的约定,那似乎没问题。

另一方面,如果应用程序的现有部分使用标准约定,我会坚持它并将构建器添加到更复杂的类中

public class NutritionalFacts {
    private final int sodium;
    private final int fat;
    private final int carbo;

    public int getSodium(){
        return sodium;
    }

    public int getfat(){
        return fat;
    }

    public int getCarbo(){
        return carbo;
    }

    public static class Builder {
        private int sodium;
        private int fat;
        private int carbo;

        public Builder sodium(int s) {
            this.sodium = s;
            return this;
        }

        public Builder fat(int f) {
            this.fat = f;
            return this;
        }

        public Builder carbo(int c) {
            this.carbo = c;
            return this;
        }

        public NutritionalFacts build() {
            return new NutritionalFacts(this);
        }
    }

    private NutritionalFacts(Builder b) {
        this.sodium = b.sodium;
        this.fat = b.fat;
        this.carbo = b.carbo;
    }
}
Run Code Online (Sandbox Code Playgroud)