我想知道,在构造对象时,setter返回this
:之间有什么区别吗
public User withId(String name) {
this.name = name;
return this;
}
和生成器(例如,由IDEA的生成器生成器插件生成的生成器)?
我的第一印象是,返回this
的setter要好得多:
- 它使用更少的代码-没有为生成器添加额外的类,在对象构造结束时没有
build()
调用 - 它读起来更好:
vsnew User().withName("Some Name").withAge(30);
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模式,即如何继续构建对象:
-
在ObjecBuilder单独的类方法中,您不断返回Builder对象,并且您仅(!)在完成构建后返回最终确定/构建的对象,这就是更好地封装创建过程的方法,因为它是一种更一致且结构设计良好的方法,因为你有两个明显分离的阶段:
1.1)构建对象;
1.2)完成构建,并返回构建的实例(如果您消除setter,此可能为您提供了拥有不可变构建对象的便利)。
-
在从同一类型返回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()
是有效的User
,new User().withName("Some Name")
是有效的User
,new User().withName("Some Name").withAge(30)
是有效的用户,那么一定要使用您的模式。
但是,如果您没有提供姓名和年龄,User
真的有效吗?也许,也许不是:如果有一个合理的默认值,这可能是可能的,但名字和年龄不能真正有默认值。
关于User.Builder
的问题是,中间结果不是User
:您设置了多个字段,然后才构建User
。