Setter返回此vs建设者



我想知道,在构造对象时,setter返回this:之间有什么区别吗

public User withId(String name) {
this.name = name;
return this;
}

和生成器(例如,由IDEA的生成器生成器插件生成的生成器)?

我的第一印象是,返回this的setter要好得多:

  • 它使用更少的代码-没有为生成器添加额外的类,在对象构造结束时没有build()调用
  • 它读起来更好:
    new User().withName("Some Name").withAge(30);
    
    vs
    User.UserBuilder.anUserBuilder().withName("Some Name").withAge(30).build();
    

那为什么要使用生成器呢?我缺什么了吗?

要理解的关键是不可变类型的概念

假设我有这个代码:

public class UnitedStates {
private static final List<String> STATE_NAMES =
Arrays.asList("Washington", "Ohio", "Oregon", "... etc");
public static List<String> getStateNames() {
return STATE_NAMES:
}
}

看起来不错,对吧?

不!这个代码坏了!看,我可以做到这一点,同时捻着小胡子,挥舞着单片眼镜:

UnitedStates.getStateNames().set(0, "Turtlia"); // Haha, suck it washington!!

,这将起作用。现在,对于ALL呼叫者,显然存在一个称为Turtlia的状态。华盛顿?什么?无处可寻。

问题是Arrays.asList返回一个可变对象:您可以对此对象调用一些方法来更改它

这样的对象不能与你不信任的代码共享,而且考虑到你不记得你写过的每一行,你不能在一两个月内信任自己,所以,你基本上不能信任任何人。如果您想正确地编写此代码,您所要做的就是使用List.of而不是Arrays.asList,因为List.of会生成一个不可变的对象。它有zero方法来改变它。它似乎有方法(它有一个set方法!),但试着调用它。它不起作用,你会得到一个异常,而且至关重要的是,列表不会改变。事实上这是不可能做到的。幸运的是,String也是不可变的。

不可变的东西更容易推理,并且可以与你喜欢的任何东西自由共享,而无需复制。

那么,想要你自己不变的吗?很好,但显然唯一的方法是有一个构造函数来设置所有值,仅此而已,不可变类型不能有set方法,因为这会使它们发生变异。

如果你有很多字段,特别是如果这些字段的类型相同或相似,这会很快变得令人讨厌。快的

new Bridge("Golden Gate", 1280, 1937, 2737);

它是什么时候建的?它有多长?最大跨度的长度是多少?

啊。。。。。这个怎么样:

newBridge()
.name("Golden Gate")
.longestSpan(1280)
.built(1937)
.length(2737)
.build();

甜美。名字!构建器还允许您随时间构建(通过将构建器传递给不同的代码位,每个代码位负责设置自己的代码位)。但是bridgebuilder不是桥,每次调用build()都会生成一个新的桥,所以您保留了关于不变性的一般规则(BridgeBuilder不是不可变的,但build()方法生成的任何Bridge对象都是

如果我们试着用二传手来做这件事,那是行不通的。桥牌不能有二传手。你可以有"枯萎",在那里你有类似集合的方法来创建全新的对象,但是,调用这些"集合"是误导性的,你会创建大量垃圾(很少相关,GC非常擅长收集短期对象)和中间的无意义桥梁:

Bridge goldenGate = Bridge.create().withName("Golden Gate").withLength(2737);

在这项行动的中间,你有一座名为"金门"的桥,根本没有长度。

事实上,建设者可以决定不让你的build()桥没有长度,通过检查和抛出,如果你尝试。这种一次调用一个方法的过程不能做到这一点。充其量,它可以将桥实例标记为"无效",任何与之交互的尝试,除了调用.withX()方法外,都会导致异常,但这是更多的工作,并导致不太可发现的API(with方法与其他方法混在一起,所有其他方法似乎都会抛出一些通常不相关的状态异常…感觉很恶心)。

就是为什么你需要建设者。

注意:Project Lombok的@Builder注释为您提供了毫不费力的建设者。你只需要写:

import lombok.Value;
import lombok.Builder;
@Value @Builder
public class Bridge {
String name;
int built;
int length;
int span;
}

lombok会自动处理剩下的。你只需要Bridge.builder().name("Golden Gate").span(1280).built(1937).length(2737).build();

Builder是设计模式,用于为代码带来清晰的结构。它们还经常用于创建不可变的类变量。您还可以在调用build()方法时定义前提条件。

我认为你的问题最好是这样表述的:

在实现Builder模式时,我们应该创建一个单独的Builder类,还是继续返回相同的实例?

根据头部优先设计模式:

使用生成器模式封装产品的构造并且允许其被分步骤地构造。

因此,封装是重要的一点。

现在让我们看看你在最初的问题中提供的方法有什么不同。主要区别在于设计,即如何实现Builder模式,即如何继续构建对象:

  1. ObjecBuilder单独的类方法中,您不断返回Builder对象,并且您(!)在完成构建后返回最终确定/构建的对象,这就是更好地封装创建过程的方法,因为它是一种更一致且结构设计良好的方法,因为你有两个明显分离的阶段:

    1.1)构建对象

    1.2)完成构建,并返回构建的实例(如果您消除setter,此可能为您提供了拥有不可变构建对象的便利)。

  2. 在从同一类型返回this的例子中,您仍然可以修改它,这可能会导致类的设计不一致和不安全。

这取决于类的性质。如果您的字段不是final(即,如果类可以是可变的),那么执行以下操作:

new User().setEmail("alalal@gmail.com").setPassword("abcde");

或者这样做:

User.newBuilder().withEmail("alalal@gmail.com").withPassowrd("abcde").build();

什么都不改变。

但是,如果您的字段应该是final(一般来说,这是首选,为了避免对字段进行不必要的修改,当然它们不必是可变的),那么构建器模式可以保证在设置完所有字段之前不会构建对象。当然,您可能会得到相同的结果,即暴露具有所有参数的单个构造函数:

public User(String email, String password);

但是,当您有大量的参数时,能够在构建对象之前查看所做的每个集合会变得更加方便和可读。

Builder的一个优点是,您可以在不知道其精确类的情况下使用它来创建对象,这与使用Factory的方式类似。想象一下,在一个情况下,您想要创建一个数据库连接,但MySQL、PostgreSQL、DB2或其他类型的连接类不同——然后构建器可以选择并实例化正确的实现类,您实际上不需要担心它。

setter函数当然不能做到这一点,因为它需要一个对象已经被实例化。

关键是中间对象是否是有效的实例。

如果new User()是有效的Usernew User().withName("Some Name")是有效的Usernew User().withName("Some Name").withAge(30)是有效的用户,那么一定要使用您的模式。

但是,如果您没有提供姓名和年龄,User真的有效吗?也许,也许不是:如果有一个合理的默认值,这可能是可能的,但名字和年龄不能真正有默认值。

关于User.Builder的问题是,中间结果不是User:您设置了多个字段,然后才构建User

相关内容

  • 没有找到相关文章

最新更新