Setter 返回此 vs 生成器
Setter returning this vs builder
我想知道,在构造对象时,setter 返回 this
:
之间有什么区别吗?
public User withId(String name) {
this.name = name;
return this;
}
和一个构建器(例如由 Builder Generator IDEA 插件生成的构建器)?
我的第一印象是 setter 返回 this
好多了:
- 它使用更少的代码 - 构建器没有额外的 class,在对象构建结束时没有
build()
调用。
- 读起来更好:
new User().withName("Some Name").withAge(30);
对比
User.UserBuilder.anUserBuilder().withName("Some Name").withAge(30).build();
那为什么要使用构建器呢?有什么我想念的吗?
构建器是设计模式,用于为代码带来清晰的结构。它们还经常用于创建不可变的 class 变量。您还可以在调用 build()
方法时定义前提条件。
这取决于您的 class 的性质。如果您的字段不是 final
(即如果 class 可以是可变的),那么这样做:
new User().setEmail("alalal@gmail.com").setPassword("abcde");
或这样做:
User.newBuilder().withEmail("alalal@gmail.com").withPassowrd("abcde").build();
...没有任何改变。
但是,如果您的字段应该是 final
(一般来说这是首选,以避免对字段进行不必要的修改,当然,它们不需要可变),那么构建器模式保证在设置所有字段之前不会构建对象。
当然,你可能会得到相同的结果,暴露一个带有所有参数的构造函数:
public User(String email, String password);
...但是当您拥有大量参数时,在构建对象之前能够看到您所做的每组设置会变得更加方便和更具可读性。
我认为您的问题更适合表述为:
Shall we create a separate Builder class when implementing the Builder Pattern or shall we just keep returning the same instance?
Use the Builder Pattern to encapsulate the construction of a product
and allow it to be constructed in steps.
因此,封装很重要。
现在让我们看看您在原始问题中提供的方法的不同之处。主要区别在于 设计,如何实现构建器模式,即如何继续构建对象:
在 ObjecBuilder 单独的 class 方法中,您保持 return Builder 对象,而你 only(!)return finalized/built 对象,在你完成构建之后,那就是什么更好地封装了创建过程,因为它是更一致且结构上设计良好的方法,因为你有一个明确分开的两个不同阶段:
1.1) 构建对象;
1.2) 完成构建,并return构建实例(这可能 为您提供不可变 构建对象的便利,如果您消除 setter)。
在同一个类型returning this的例子中,你仍然可以修改它,这可能会导致不一致和class.
的不安全设计
Builder 的一个优点是您可以使用它来创建对象而无需知道其精确度 class - 类似于您使用工厂的方式。想象一下这样一种情况,您想要创建一个数据库连接,但连接 class 在 MySQL、PostgreSQL、DB2 或其他任何东西之间是不同的——然后构建器可以选择并实例化正确的实现 class,而且您实际上不需要担心。
一个setter函数,当然不能这样做,因为它要求一个对象已经被实例化了。
关键在于中间对象是否是有效实例。
如果 new User()
是有效的 User
,并且 new User().withName("Some Name")
是有效的 User
,并且 new User().withName("Some Name").withAge(30)
是有效的用户,那么无论如何使用你的模式。
但是,如果您没有提供姓名和年龄, User
真的有效吗?也许,也许不是:如果这些有一个合理的默认值,但名称和年龄不能真正具有默认值。
一个User.Builder
的事情是中间结果不是一个User
:你设置了多个字段,然后才建立一个User
.
要理解的关键是不可变类型的概念。
假设我有这个代码:
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
returns 一个 可变对象 :您可以调用此对象的一些方法来更改它。
此类对象不能与您不信任的代码共享,而且鉴于您不记得自己写过的每一行,您不能相信自己一两个月,所以,你基本上不能相信任何人。如果你想正确地编写这段代码,你所要做的就是使用 List.of
而不是 Arrays.asList
,因为 List.of
会产生一个不可变的对象。它有 零个方法 来改变它。它似乎有方法(它有一个 set
方法!),但请尝试调用它。它不会工作,你会得到一个例外,而且至关重要的是,列表不会改变。事实上这是不可能的。幸运的是,String 也是不可变的。
不可变更容易推理,并且可以自由地与任何你喜欢的人共享而无需复制。
那么,想要你自己的不可变的吗?太好了 - 但显然唯一的方法是拥有一个设置所有值的构造函数,就是这样 - 不可变类型不能有 set
方法,因为那会改变它们。
如果您有很多字段,尤其是如果这些字段具有相同或相似的类型,这很快就会变得烦人。快!
new Bridge("Golden Gate", 1280, 1937, 2737);
它是什么时候建造的?多久了?最大跨度是多少?
Uhhhhhhh.....这个怎么样:
newBridge()
.name("Golden Gate")
.longestSpan(1280)
.built(1937)
.length(2737)
.build();
甜蜜。名字!构建器还允许您随着时间的推移进行构建(通过将构建器传递给不同的代码位,每个代码位负责设置它们的位)。但是 bridgebuilder 不是一座桥,每次调用 build() 都会创建一个新的,所以你要遵守关于不变性的一般规则(BridgeBuilder
不是不可变的,但是任何 Bridge
对象都会被创建通过 build()
方法是。
如果我们尝试使用 setter 来执行此操作,那是行不通的。桥梁不能有二传手。你可以有 'withers',在那里你有类似集合的方法来创建全新的对象,但是,调用这些 'set' 会产生误导,并且你会创建大量垃圾(很少相关,GC 是 非常擅长收集短命的物品),中级无意义的桥梁:
Bridge goldenGate = Bridge.create().withName("Golden Gate").withLength(2737);
在该操作中间的某个地方,您有一个名为 'Golden Gate' 的桥,完全没有长度。
事实上,建造者可以决定不让你 build()
没有长度的桥,通过检查并在你尝试时抛出。这种一次调用一种方法的过程无法做到这一点。充其量它可以将桥实例标记为 'invalid',并且任何与它交互的尝试,除了对其调用 .withX() 方法之外,都会导致异常,但那是 more 努力,并导致更不易发现 API(with 方法与其余方法混合在一起,所有其他方法似乎会抛出一些通常不相关的状态异常……感觉很恶心)。
这 就是您需要建筑商的原因。
注意:Project Lombok's @Builder
annotation 为您提供毫不费力的构建器。您只需要写:
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();
.
我想知道,在构造对象时,setter 返回 this
:
public User withId(String name) {
this.name = name;
return this;
}
和一个构建器(例如由 Builder Generator IDEA 插件生成的构建器)?
我的第一印象是 setter 返回 this
好多了:
- 它使用更少的代码 - 构建器没有额外的 class,在对象构建结束时没有
build()
调用。 - 读起来更好:
对比new User().withName("Some Name").withAge(30);
User.UserBuilder.anUserBuilder().withName("Some Name").withAge(30).build();
那为什么要使用构建器呢?有什么我想念的吗?
构建器是设计模式,用于为代码带来清晰的结构。它们还经常用于创建不可变的 class 变量。您还可以在调用 build()
方法时定义前提条件。
这取决于您的 class 的性质。如果您的字段不是 final
(即如果 class 可以是可变的),那么这样做:
new User().setEmail("alalal@gmail.com").setPassword("abcde");
或这样做:
User.newBuilder().withEmail("alalal@gmail.com").withPassowrd("abcde").build();
...没有任何改变。
但是,如果您的字段应该是 final
(一般来说这是首选,以避免对字段进行不必要的修改,当然,它们不需要可变),那么构建器模式保证在设置所有字段之前不会构建对象。
当然,你可能会得到相同的结果,暴露一个带有所有参数的构造函数:
public User(String email, String password);
...但是当您拥有大量参数时,在构建对象之前能够看到您所做的每组设置会变得更加方便和更具可读性。
我认为您的问题更适合表述为:
Shall we create a separate Builder class when implementing the Builder Pattern or shall we just keep returning the same instance?
Use the Builder Pattern to encapsulate the construction of a product and allow it to be constructed in steps.
因此,封装很重要。
现在让我们看看您在原始问题中提供的方法的不同之处。主要区别在于 设计,如何实现构建器模式,即如何继续构建对象:
在 ObjecBuilder 单独的 class 方法中,您保持 return Builder 对象,而你 only(!)return finalized/built 对象,在你完成构建之后,那就是什么更好地封装了创建过程,因为它是更一致且结构上设计良好的方法,因为你有一个明确分开的两个不同阶段:
1.1) 构建对象;
1.2) 完成构建,并return构建实例(这可能 为您提供不可变 构建对象的便利,如果您消除 setter)。
在同一个类型returning this的例子中,你仍然可以修改它,这可能会导致不一致和class.
的不安全设计
Builder 的一个优点是您可以使用它来创建对象而无需知道其精确度 class - 类似于您使用工厂的方式。想象一下这样一种情况,您想要创建一个数据库连接,但连接 class 在 MySQL、PostgreSQL、DB2 或其他任何东西之间是不同的——然后构建器可以选择并实例化正确的实现 class,而且您实际上不需要担心。
一个setter函数,当然不能这样做,因为它要求一个对象已经被实例化了。
关键在于中间对象是否是有效实例。
如果 new User()
是有效的 User
,并且 new User().withName("Some Name")
是有效的 User
,并且 new User().withName("Some Name").withAge(30)
是有效的用户,那么无论如何使用你的模式。
但是,如果您没有提供姓名和年龄, User
真的有效吗?也许,也许不是:如果这些有一个合理的默认值,但名称和年龄不能真正具有默认值。
一个User.Builder
的事情是中间结果不是一个User
:你设置了多个字段,然后才建立一个User
.
要理解的关键是不可变类型的概念。
假设我有这个代码:
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
returns 一个 可变对象 :您可以调用此对象的一些方法来更改它。
此类对象不能与您不信任的代码共享,而且鉴于您不记得自己写过的每一行,您不能相信自己一两个月,所以,你基本上不能相信任何人。如果你想正确地编写这段代码,你所要做的就是使用 List.of
而不是 Arrays.asList
,因为 List.of
会产生一个不可变的对象。它有 零个方法 来改变它。它似乎有方法(它有一个 set
方法!),但请尝试调用它。它不会工作,你会得到一个例外,而且至关重要的是,列表不会改变。事实上这是不可能的。幸运的是,String 也是不可变的。
不可变更容易推理,并且可以自由地与任何你喜欢的人共享而无需复制。
那么,想要你自己的不可变的吗?太好了 - 但显然唯一的方法是拥有一个设置所有值的构造函数,就是这样 - 不可变类型不能有 set
方法,因为那会改变它们。
如果您有很多字段,尤其是如果这些字段具有相同或相似的类型,这很快就会变得烦人。快!
new Bridge("Golden Gate", 1280, 1937, 2737);
它是什么时候建造的?多久了?最大跨度是多少?
Uhhhhhhh.....这个怎么样:
newBridge()
.name("Golden Gate")
.longestSpan(1280)
.built(1937)
.length(2737)
.build();
甜蜜。名字!构建器还允许您随着时间的推移进行构建(通过将构建器传递给不同的代码位,每个代码位负责设置它们的位)。但是 bridgebuilder 不是一座桥,每次调用 build() 都会创建一个新的,所以你要遵守关于不变性的一般规则(BridgeBuilder
不是不可变的,但是任何 Bridge
对象都会被创建通过 build()
方法是。
如果我们尝试使用 setter 来执行此操作,那是行不通的。桥梁不能有二传手。你可以有 'withers',在那里你有类似集合的方法来创建全新的对象,但是,调用这些 'set' 会产生误导,并且你会创建大量垃圾(很少相关,GC 是 非常擅长收集短命的物品),中级无意义的桥梁:
Bridge goldenGate = Bridge.create().withName("Golden Gate").withLength(2737);
在该操作中间的某个地方,您有一个名为 'Golden Gate' 的桥,完全没有长度。
事实上,建造者可以决定不让你 build()
没有长度的桥,通过检查并在你尝试时抛出。这种一次调用一种方法的过程无法做到这一点。充其量它可以将桥实例标记为 'invalid',并且任何与它交互的尝试,除了对其调用 .withX() 方法之外,都会导致异常,但那是 more 努力,并导致更不易发现 API(with 方法与其余方法混合在一起,所有其他方法似乎会抛出一些通常不相关的状态异常……感觉很恶心)。
这 就是您需要建筑商的原因。
注意:Project Lombok's @Builder
annotation 为您提供毫不费力的构建器。您只需要写:
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();
.