英文:
Setter returning this vs builder
问题
我在想,在构造对象时,一个setter返回this
和一个生成器(例如由Builder Generator插件为IDEA生成的生成器)之间是否有任何区别?
我的第一印象是,返回this
的setter更好:
- 它使用的代码更少 - 不需要为生成器创建额外的类,不需要在对象构造的末尾调用
build()
。 - 它的可读性更好:
new User().withName("Some Name").withAge(30);
对比
User.UserBuilder.anUserBuilder().withName("Some Name").withAge(30).build();
那么为什么要使用生成器?我有什么遗漏的吗?
英文:
I was wondering, when constructing an object, is there any difference between a setter returning this
:
public User withId(String name) {
this.name = name;
return this;
}
and a builder (for example one which is generated by Builder Generator plugin for IDEA)?
My first impression is that a setter returning this
is much better:
- it uses less code - no extra class for builder, no
build()
call at the end of object construction. - it reads better:
new User().withName("Some Name").withAge(30);
vs
User.UserBuilder.anUserBuilder().withName("Some Name").withAge(30).build();
Then why to use builder at all? Is there anything I am missing?
答案1
得分: 3
理解的关键是__不可变类型__的概念。
假设我有这段代码:
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"); // 哈哈,华盛顿,你完蛋了!!
而且这会起作用。现在对__所有__的调用者来说,似乎有一个叫做“Turtlia”的州。 华盛顿?哪里?不见了。
问题在于Arrays.asList
返回的是一个__可变对象__:您可以在此对象上调用更改它的方法。
这样的对象__不能与您不信任的代码共享__,鉴于您不记得自己写的每一行代码,一个月或两个月后您也无法信任自己,因此基本上不能信任任何人。如果要正确编写此代码,您只需使用List.of
而不是Arrays.asList
,因为List.of
生成一个不可变对象。它__没有任何方法__可以更改它。它似乎有方法(它有一个set
方法!),但尝试调用它。它不会起作用,您将收到异常,并且关键的是,列表不会更改。实际上是不可能的。幸运的是,String也是不可变的。
不可变对象更容易理解,可以自由共享而无需复制。
所以,想要自己的不可变对象吗?很好 - 但显然制作一个的唯一方法是拥有一个构造函数,其中所有值都被设置,然后就是了 - 不可变类型不能有set
方法,因为那会使它们变异。
如果有很多字段,特别是如果这些字段具有相同或相似的类型,那么这会很快变得令人讨厌。快点!
new Bridge("Golden Gate", 1280, 1937, 2737);
它是什么时候建造的?有多长?最大跨度的长度是多少?
嗯……怎么样这个:
new BridgeBuilder()
.name("Golden Gate")
.longestSpan(1280)
.built(1937)
.length(2737)
.build();
很棒。名称!构建器还允许您随着时间的推移构建(通过将构建器传递给不同的代码块,每个代码块负责设置它们的部分)。但是桥构建器不是桥,每次调用build()
方法都会创建一个新的桥,因此您要保留关于不可变性的一般规则(BridgeBuilder
不是不可变的,但由build()
方法创建的任何Bridge
对象都是不可变的)。
如果我们尝试使用setter,它不起作用。桥不能有setter。您可以拥有“withers”,其中有像设置一样的方法,可以创建全新的对象,但是,将它们称为“set”是误导性的,并且会创建大量无用的对象(很少相关,GC在收集短生命周期对象方面非常出色),以及中间的无意义的桥梁:
Bridge goldenGate = Bridge.create().withName("Golden Gate").withLength(2737);
在这个操作的中间某个地方,您有一个名为“Golden Gate”的桥,根本没有长度。
实际上,构建器可以决定不允许您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();
。
英文:
The crucial thing to understand is the concept of an immutable type.
Let's say I have this code:
public class UnitedStates {
private static final List<String> STATE_NAMES =
Arrays.asList("Washington", "Ohio", "Oregon", "... etc");
public static List<String> getStateNames() {
return STATE_NAMES:
}
}
Looks good, right?
Nope! This code is broken! See, I could do this, whilst twirling my moustache and wielding a monocle:
UnitedStates.getStateNames().set(0, "Turtlia"); // Haha, suck it washington!!
and that will work. Now for ALL callers, apparently there's some state called Turtlia
. Washington? Wha? Nowhere to be found.
The problem is that Arrays.asList
returns a mutable object: There are methods you can invoke on this object that change it.
Such objects cannot be shared with code you don't trust, and given that you don't remember every line you ever wrote, you can't trust yourself in a month or two, so, you basically can't trust anybody. If you want to write this code properly, all you had to do is use List.of
instead of Arrays.asList
, because List.of
produces an immutable object. It has zero methods that change it. It seems like it has methods (it has a set
method!), but try invoking it. It won't work, you'll get an exception, and crucially, the list does not change. It is in fact impossible to do so. Fortunately, String is also immutable.
Immutables are much easier to reason about, and can be shared freely with whatever you like without copying.
So, want your own immutable? Great - but apparently the only way to make one, is to have a constructor where all values are set and that's it - immutable types cannot have set
methods, because that would mutate them.
If you have a lot of fields, especially if those fields have the same or similar types, this gets annoying fast. Quick!
new Bridge("Golden Gate", 1280, 1937, 2737);
when was it built? How long is it? What's the length of the largest span?
Uhhhhhhh..... how about this instead:
newBridge()
.name("Golden Gate")
.longestSpan(1280)
.built(1937)
.length(2737)
.build();
sweet. Names! builders also let you build over time (by passing the builder around to different bits of code, each responsible for setting up their bits). But a bridgebuilder isn't a bridge, and each invoke of build() will make a new one, so you keep the general rules about immutability (a BridgeBuilder
is not immutable, but any Bridge
objects made by the build()
method are.
If we try to do this with setters, it doesn't work. Bridges can't have setters. you can have 'withers', where you have set-like methods that create entirely new objects, but, calling these 'set' is misleading, and you create both a ton of garbage (rarely relevant, the GC is very good at collecting short lived objects), and intermediate senseless bridges:
Bridge goldenGate = Bridge.create().withName("Golden Gate").withLength(2737);
somewhere in the middle of that operation you have a bridge named 'Golden Gate', with no length at all.
In fact, the builder can decide to not let you build()
bridge with no length, by checking for that and throwing if you try. This process of invoking one method at a time can't do that. At best it can mark a bridge instance as 'invalid', and any attempt to interact with it, short of calling .withX() methods on it, results in an exception, but that's more effort, and leads to a less discoverable API (the with methods are mixed up with the rest, and all the other methods appear to throw some state exception that is normally never relevant.. that feels icky).
THAT is why you need builders.
NB: Project Lombok's @Builder
annotation gives you builders for no effort at all. All you'd have to write is:
import lombok.Value;
import lombok.Builder;
@Value @Builder
public class Bridge {
String name;
int built;
int length;
int span;
}
and lombok automatically takes care of the rest. You can just Bridge.builder().name("Golden Gate").span(1280).built(1937).length(2737).build();
.
答案2
得分: 2
建造者是设计模式,用于为代码提供清晰的结构。它们通常用于创建不可变的类变量。您还可以在调用 build()
方法时定义前提条件。
英文:
Builders are design patterns and are used to bring a clear structure to the code. They are also often used to create immutable class variables. You can also define preconditions when calling the build()
method.
答案3
得分: 1
我认为你的问题更好地表述为:
根据《Head First Design Patterns》:
使用建造者模式封装产品的构建过程,并允许它逐步构建。
因此,封装是一个重要的点。
现在让我们看看你在原始问题中提供的两种方法之间的区别。主要区别在于设计,即你如何实现建造者模式,即如何继续构建对象:
-
在ObjecBuilder分离类方法中,你会继续返回建造者对象,只有在完成构建后才返回已完成/已建立的对象。这更好地封装了创建过程,因为它是一种更一致且结构良好的方法,因为你明确地分离了两个不同的阶段:
1.1) 构建对象;
1.2) 完成构建并返回已构建的实例(如果消除了设置器,则可能会为你提供创建的不可变对象)。
-
在从相同类型中只返回this的示例中,仍然可以修改它,这可能会导致类的设计不一致和不安全。
英文:
I think your question is better formulated like:
>Shall we create a separate Builder class when implementing the Builder Pattern or shall we just keep returning the same instance?
According to the Head First Design Patterns:
> Use the Builder Pattern to encapsulate the construction of a product
> and allow it to be constructed in steps.
Hence, the Encapsulation is important point.
Let's now see the difference in the approaches you have provided in your original question. The main difference is the Design, of how you implement the Builder Pattern, i.e. how you keep building the object:
-
In the ObjecBuilder separate class approach, you keep returning the Builder object, and you only(!) return the finalized/built Object, after you have finalized building, and that's what better encapsulates creation process, as it's more consistent and structurally well designed approach, because you have a clearly separated two distinct phases:
1.1) Building the object;
1.2) Finalizing the building, and returning the built instance (this may give you the facility to have immutable built objects, if you eliminate setters).
-
In the example of just returning this from the same type, you still can modify it, which probably will lead to inconsistent and insecure design of the class.
答案4
得分: 0
根据您的类的性质而定。如果您的字段不是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);
... 但是当您有大量参数时,能够在构建对象之前看到您设置的每个参数会更加方便和可读。
英文:
It depends on the nature of your class. If your fields are not final
(i.e. if the class can be mutable), then doing this:
new User().setEmail("alalal@gmail.com").setPassword("abcde");
or doing this:
User.newBuilder().withEmail("alalal@gmail.com").withPassowrd("abcde").build();
... changes nothing.
However, if your fields are supposed to be final
(which generally speaking is to be preferred, in order to avoid unwanted modifications of the fields, when of course it is not necessary for them to be mutable), then the builder pattern guarantees you that your object will not be constructed until when all fields are set.
Of course, you may reach the same result exposing a single constructor with all the parameters:
public User(String email, String password);
... but when you have a large number of parameters it becomes more convenient and more readable to be able to see each of the sets you do before building the object.
答案5
得分: 0
优点之一是,您可以使用构建器创建对象,而无需知道其精确的类别 - 就像您可以使用工厂一样。想象一种情况,您想要创建一个数据库连接,但连接类在MySQL、PostgreSQL、DB2或其他数据库之间不同 - 构建器可以选择并实例化正确的实现类,您无需真正担心它。
当然,设置函数无法做到这一点,因为它需要已经实例化的对象。
英文:
One advantage of a Builder is you can use it to create an object without knowing its precise class - similar to how you could use a Factory. Imagine a case where you want to create a database connection, but the connection class differs between MySQL, PostgreSQL, DB2 or whatever - the builder could then choose and instantiate the correct implementation class, and you do not need to actually worry about it.
A setter function, of course, can not do this, because it requires an object to already be instantiated.
答案6
得分: 0
以下是已翻译的内容:
"关键点是中间对象是否是有效实例。
如果 new User()
是有效的 User
,而 new User().withName("Some Name")
是有效的 User
,而 new User().withName("Some Name").withAge(30)
是有效的用户,那么尽管使用您的模式。
然而,如果没有提供姓名和年龄,User
是否真的有效呢?也许,也许不是:如果对于这些字段有合理的默认值,那就可以是有效的,但姓名和年龄不能真正具有默认值。
User.Builder
的特点是中间结果 不是 User
:您设置多个字段,然后才构建一个 User
。"
英文:
The key point is whether the intermediate object is a valid instance.
If new User()
is a valid User
, and new User().withName("Some Name")
is a valid User
, and new User().withName("Some Name").withAge(30)
is a valid user, then by all means use your pattern.
However, is a User
really valid if you've not provided a name and an age? Perhaps, perhaps not: it could be if there is a sensible default value for these, but names and ages can't really have default values.
The thing about a User.Builder
is the intermediate result isn't a User
: you set multiple fields, and only then build a User
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论