我的强迫症系列之@Builder和建造者模式
前言
备受争议的Lombok
,有的人喜欢它让代码更整洁,有的人不喜欢它,巴拉巴拉一堆原因。在我看来Lombok
唯一的缺点可能就是需要安装插件了,但是对于业务开发的项目来说,它的优点远远超过缺点。
我们可以看一下,有多少项目使用了Lombok(数量还在疯涨中...)
尽管如此,我们今天也只是单纯的来看一下@Builder()这个东西
@Builder的使用
使用@Builder修饰类
@Data
@Builder
public class UserDO {
private Long id;
private String name;
}
使用建造者模式创建类
@Test
public void test() {
UserDO userDO = UserDO.builder()
.id(1L)
.name("iisheng")
.build();
System.out.println(userDO);
}
编译后源码
执行javac -cp ~/lombok.jar UserDO.java -verbose
将.java
编译成.class
文件。
通过IDE查看该.class
源码
下面展示的是被我处理后的源码,感兴趣的同学,可以自己执行上面命令,查看完整源码
public class UserDO {
private Long id;
private String name;
public String toString() {
return "UserDO(id="
+ this.getId() + ", name=" + this.getName() + ")";
}
UserDO(Long var1, String var2) {
this.id = var1;
this.name = var2;
}
public static UserDO.UserDOBuilder builder() {
return new UserDO.UserDOBuilder();
}
private UserDO() {
}
public static class UserDOBuilder {
private Long id;
private String name;
UserDOBuilder() {
}
public UserDO.UserDOBuilder id(Long var1) {
this.id = var1;
return this;
}
public UserDO.UserDOBuilder name(String var1) {
this.name = var1;
return this;
}
public UserDO build() {
return new UserDO(this.id, this.name);
}
}
}
由此,我们可以看出来Builder的实现步骤:
- 在
UserDO
中创建静态UserDOBuilder
- 编写设置属性方法,返回
UserDOBuilder
对象 - 编写
build()
方法,返回UserDO
对象
是不是很简单?我曾经看过不知道哪个大佬说的一句话,整洁的代码不是说,行数更少,字数更少,而是阅读起来逻辑更清晰。所以,我觉得,哪怕我们不用@Builder,也应该多用这种建造者模式。
是时候看看什么是建造者模式了!
建造者模式
UML类图
这是大部分书籍网络中的建造者模式类图
产品类
public class Product {
private String name;
private Integer val;
Product(String name, Integer val) {
this.name = name;
this.val = val;
}
@Override
public String toString() {
return "Product is " + name + " value is " + val;
}
}
抽象建造者
public abstract class Builder {
protected Integer val;
protected String name;
// 设置产品不同部分,以获得不同的产品
public abstract void setVal(Integer val);
// 设置名字 公用方法
public void setName(String name) {
this.name = name;
}
// 建造产品
public abstract Product buildProduct();
}
具体建造者
public class ConcreteBuilder extends Builder {
@Override
public void setVal(Integer val) {
/**
* 产品类内部的逻辑
* 实际存储的值是 val + 100
*/
this.val = val + 100;
}
@Override
// 组建一个产品
public Product buildProduct() {
// 这块还可以写特殊的校验逻辑
return new Product(name, val);
}
}
导演类
public class Director {
private Builder builder = new ConcreteBuilder();
public Product getAProduct() {
// 设置不同的零件,产生不同的产品
builder.setName("ProductA");
builder.setVal(2);
return builder.buildProduct();
}
}
我更喜欢这样的建造者模式类图
Product
的创建,也依赖于Builder
。代码只需要将上面的Product
和ConcreteBuilder
调整一下即可。
调整后的产品类
public class Product {
private String name;
private Integer val;
Product(Builder builder) {
this.name = builder.name;
this.val = builder.val;
}
@Override
public String toString() {
return "Product is " + name + " value is " + val;
}
}
这代码只是将构造方法改了,使用Builder
来创建Product
对象。
调整后的具体建造者
public class ConcreteBuilder extends Builder {
@Override
public void setVal(Integer val) {
/**
* 产品类内部的逻辑
* 实际存储的值是 val + 100
*/
this.val = val + 100;
}
@Override
// 组建一个产品
public Product buildProduct() {
// 这块还可以写特殊的校验逻辑
return new Product(this);
}
}
相应的使用带Builder
的Product
的构造方法。
JDK中的建造者模式
StringBuilder (截取部分源码)
抽象建造者
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
// Documentation in subclasses because of synchro difference
@Override
public AbstractStringBuilder append(CharSequence s) {
if (s == null)
return appendNull();
if (s instanceof String)
return this.append((String)s);
if (s instanceof AbstractStringBuilder)
return this.append((AbstractStringBuilder)s);
return this.append(s, 0, s.length());
}
public AbstractStringBuilder delete(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
end = count;
if (start > end)
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) {
System.arraycopy(value, start+len, value, start, count-end);
count -= len;
}
return this;
}
}
具体建造者
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
@Override
public StringBuilder append(CharSequence s) {
super.append(s);
return this;
}
/**
* @throws StringIndexOutOfBoundsException {@inheritDoc}
*/
@Override
public StringBuilder delete(int start, int end) {
super.delete(start, end);
return this;
}
}
StringBuilder
中的建造者模式比较简单,但是我的确没找到StringBuilder
非要用建造者模式的原因,或许就是想让我们写下面这样的代码?
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("Love ")
.append("iisheng !")
.insert(0, "I ");
System.out.println(sb);
}
但是我希望你能通过
StringBuilder
,感受一下建造者模式的气息
Guava Cache中的建造者模式
如何使用 Guava Cache?
public static void main(String[] args) {
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
// 最多存放十个数据
.maximumSize(10)
// 缓存10秒
.expireAfterWrite(10, TimeUnit.SECONDS)
.build(new CacheLoader<String, Integer>() {
// 默认返回-1,也可以是查询操作,如从DB查询
@Override
public Integer load(String key) throws Exception {
return -1;
}
});
// 只查询缓存,没有命中,即返回 null
System.out.println(cache.getIfPresent("key1"));
// put数据,放在缓存中
cache.put("key1", 1);
// 再次查询,已经存在缓存中
System.out.println(cache.getIfPresent("key1"));
//查询缓存,未命中,调用load方法,返回 -1
try {
System.out.println(cache.get("key2"));
} catch (ExecutionException e) {
e.printStackTrace();
}
}
下面是截取建造者模式相关的部分代码
产品接口
@DoNotMock("Use CacheBuilder.newBuilder().build()")
@GwtCompatible
public interface Cache<K, V> {
@Nullable
V getIfPresent(@CompatibleWith("K") Object key);
V get(K key, Callable<? extends V> loader) throws ExecutionException;
void put(K key, V value);
long size();
ConcurrentMap<K, V> asMap();
void cleanUp();
}
另一个产品接口
@GwtCompatible
public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {
V get(K key) throws ExecutionException;
V getUnchecked(K key);
void refresh(K key);
@Deprecated
@Override
V apply(K key);
@Override
ConcurrentMap<K, V> asMap();
}
产品实现类
static class LocalManualCache<K, V> implements Cache<K, V>, Serializable {
final LocalCache<K, V> localCache;
LocalManualCache(CacheBuilder<? super K, ? super V> builder) {
this(new LocalCache<K, V>(builder, null));
}
private LocalManualCache(LocalCache<K, V> localCache) {
this.localCache = localCache;
}
// Cache methods
@Override
public @Nullable V getIfPresent(Object key) {
return localCache.getIfPresent(key);
}
@Override
public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException {
checkNotNull(valueLoader);
return localCache.get(
key,
new CacheLoader<Object, V>() {
@Override
public V load(Object key) throws Exception {
return valueLoader.call();
}
});
}
@Override
public void put(K key, V value) {
localCache.put(key, value);
}
@Override
public long size() {
return localCache.longSize();
}
@Override
public ConcurrentMap<K, V> asMap() {
return localCache;
}
@Override
public void cleanUp() {
localCache.cleanUp();
}
// Serialization Support
private static final long serialVersionUID = 1;
Object writeReplace() {
return new ManualSerializationProxy<>(localCache);
}
}
另一个产品实现类
static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>
implements LoadingCache<K, V> {
LocalLoadingCache(
CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
super(new LocalCache<K, V>(builder, checkNotNull(loader)));
}
// LoadingCache methods
@Override
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
}
@Override
public V getUnchecked(K key) {
try {
return get(key);
} catch (ExecutionException e) {
throw new UncheckedExecutionException(e.getCause());
}
}
@Override
public void refresh(K key) {
localCache.refresh(key);
}
@Override
public final V apply(K key) {
return getUnchecked(key);
}
// Serialization Support
private static final long serialVersionUID = 1;
@Override
Object writeReplace() {
return new LoadingSerializationProxy<>(localCache);
}
}
实际产品实现类LocalCache
上面两个产品类实际上,内部使用的是LocalCache
来存储数据。我们再看下LocalCache
的实现。
LocalCache
继承AbstractCache
,我们先看AbstractCache
:
@GwtCompatible
public abstract class AbstractCache<K, V> implements Cache<K, V> {
/** Constructor for use by subclasses. */
protected AbstractCache() {}
@Override
public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException {
throw new UnsupportedOperationException();
}
@Override
public void put(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public void cleanUp() {}
@Override
public long size() {
throw new UnsupportedOperationException();
}
@Override
public ConcurrentMap<K, V> asMap() {
throw new UnsupportedOperationException();
}
}
再来看,LocalCache
:
@GwtCompatible(emulated = true)
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
/** How long after the last write to an entry the map will retain that entry. */
final long expireAfterWriteNanos;
/** The default cache loader to use on loading operations. */
final @Nullable CacheLoader<? super K, V> defaultLoader;
/**
* Creates a new, empty map with the specified strategy, initial capacity and concurrency level.
*/
LocalCache(
CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
maxWeight = builder.getMaximumWeight();
weigher = builder.getWeigher();
expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
refreshNanos = builder.getRefreshNanos();
defaultLoader = loader;
int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
if (evictsBySize() && !customWeigher()) {
initialCapacity = (int) Math.min(initialCapacity, maxWeight);
}
// Find the lowest power-of-two segmentCount that exceeds concurrencyLevel, unless
// maximumSize/Weight is specified in which case ensure that each segment gets at least 10
// entries. The special casing for size-based eviction is only necessary because that eviction
// happens per segment instead of globally, so too many segments compared to the maximum size
// will result in random eviction behavior.
int segmentShift = 0;
int segmentCount = 1;
while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
++segmentShift;
segmentCount <<= 1;
}
this.segmentShift = 32 - segmentShift;
segmentMask = segmentCount - 1;
this.segments = newSegmentArray(segmentCount);
}
}
建造者
@GwtCompatible(emulated = true)
public final class CacheBuilder<K, V> {
long maximumSize = UNSET_INT;
long expireAfterWriteNanos = UNSET_INT;
Supplier<? extends StatsCounter> statsCounterSupplier = NULL_STATS_COUNTER;
public CacheBuilder<K, V> maximumSize(long maximumSize) {
checkState(
this.maximumSize == UNSET_INT, "maximum size was already set to %s", this.maximumSize);
checkState(
this.maximumWeight == UNSET_INT,
"maximum weight was already set to %s",
this.maximumWeight);
checkState(this.weigher == null, "maximum size can not be combined with weigher");
checkArgument(maximumSize >= 0, "maximum size must not be negative");
this.maximumSize = maximumSize;
return this;
}
public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
checkState(
expireAfterWriteNanos == UNSET_INT,
"expireAfterWrite was already set to %s ns",
expireAfterWriteNanos);
checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
this.expireAfterWriteNanos = unit.toNanos(duration);
return this;
}
public CacheBuilder<K, V> recordStats() {
statsCounterSupplier = CACHE_STATS_COUNTER;
return this;
}
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
checkWeightWithWeigher();
checkNonLoadingCache();
return new LocalCache.LocalManualCache<>(this);
}
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) {
checkWeightWithWeigher();
return new LocalCache.LocalLoadingCache<>(this, loader);
}
}
Guava Cache的代码还是蛮复杂的,来一张UML图,便于理解
LoadingCache
接口继承了Cache
接口,两个接口都定义了缓存的基本方法CacheLoader
是LocalCache
的成员变量LocalCache
继承AbstractMap
,是真正意义上的产品类LocalManualCache
是CacheBuilder
的build()
方法产生的对象的类,LocalManualCache
因为有LocalCache
作为成员变量,使得它成为了产品类,LocalManualCache
实现了Cache
接口LocalLoadingCache
继承了LocalManualCache
,是CacheBuilder
的build(CacheLoader<? super K1, V1> loader)
方法产生的对象的类,LocalLoadingCache
实现了LoadingCache
接口
总结
什么时候适合使用建造者模式?
创建对象参数过多的时候
创建一个有很多属性的对象,如果参数在构造方法中写,看起来很乱,一长串不说,还很容易写错。
对象的部分属性是可选择的时候
创建的对象有很多属性是可选择的那种,常见的比如配置类等,不同使用者有不同的配置。
对象创建完成后,就不能修改内部属性的时候
不提供set()方法,使用建造者模式一次性把对象创建完成。
建造者模式和工厂模式的区别是什么?
- 建造者模式,通过设置不同的可选参数,“定制化”的创建不同的对象
- 工厂模式,是直接创建不同但是相关类型的对象(继承同一父类或者接口的一组子类)
最后想说的
由@Builder
想到的建造者模式,然后看了StringBuilder
以及Guava Cache
的源码,其中还是有很多值得我们学习的地方。
建造者模式,可能不同的人有不同的理解,不同的实现有不同的方法,但是我们只有深刻的理解了其中的设计思想,才不至于在项目中生搬硬套,才能灵活运用。
参考文献:
[1]:《设计模式之禅》
[2]:《Effective Java中文版》
[3]:《设计模式之美 建造者模式》
欢迎关注个人微信公众号【如逆水行舟】,用心输出基础、算法、源码系列文章。
我的强迫症系列之@Builder和建造者模式的更多相关文章
- 设计模式实战系列之@Builder和建造者模式
前言 备受争议的Lombok,有的人喜欢它让代码更整洁,有的人不喜欢它,巴拉巴拉一堆原因.在我看来Lombok唯一的缺点可能就是需要安装插件了,但是对于业务开发的项目来说,它的优点远远超过缺点. 我们 ...
- 设计模式(五)Builder Pattern建造者模式
在我们日常生活中,如构建一个飞船,一个手机,一栋建筑,都会有非常复杂的组装,这时候应该用到建造者模式 以建造一个飞船为例 案例:造小页飞船 1.飞船各部分元件 package com.littlepa ...
- 设计模式系列之建造者模式(Builder Pattern)——复杂对象的组装与创建
说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...
- C#设计模式系列:建造者模式(Builder)
1.建造者模式简介 1.1>.定义 建造者模式(Builder)将复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示. 1.2>.使用频率 中低 1.3>.原型模式应用 ...
- 设计模式总结篇系列:建造者模式(Builder)
关于建造者模式网上有很多文章,也有些不同的理解.在此结合网上其他文章对建造者模式进行总结. 总体说来,建造者模式适合于一个具有较多的零件(属性)的产品(对象)的创建过程.根据产品创建过程中零件的构造是 ...
- 设计模式(五)建造者模式(Builder Pattern)
一.引言 在软件系统中,有时需要创建一个复杂对象,并且这个复杂对象由其各部分子对象通过一定的步骤组合而成.例如一个采购系统中,如果需要采购员去采购一批电脑时,在这个实际需求中,电脑就是一个复杂的对象, ...
- 【原】iOS设计模式之:建造者模式Builder Pattern,用于改进初始化参数
本文主要讨论一下iOS中的Builder Pattern.与网上很多版本不同,本文不去长篇大论地解释建造者模式的概念,那些东西太虚了.设计模式这种东西是为了解决实际问题的,不能为了设计模式而设计模式, ...
- C#设计模式(5)——建造者模式(Builder Pattern)
一.引言 在软件系统中,有时需要创建一个复杂对象,并且这个复杂对象由其各部分子对象通过一定的步骤组合而成.例如一个采购系统中,如果需要采购员去采购一批电脑时,在这个实际需求中,电脑就是一个复杂的对象, ...
- .NET设计模式(4):建造者模式(Builder Pattern)(转)
概述 在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成:由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法确相对稳定. ...
随机推荐
- Spring IoC 公共注解详解
前言 本系列全部基于 Spring 5.2.2.BUILD-SNAPSHOT 版本.因为 Spring 整个体系太过于庞大,所以只会进行关键部分的源码解析. 什么是公共注解?公共注解就是常见的Java ...
- Pandas基础知识图谱
所有内容整理自<利用Python进行数据分析>,使用MindMaster Pro 7.3制作,emmx格式,源文件已经上传Github,需要的同学转左上角自行下载或者右击保存图片.该图谱只 ...
- input type=file过滤图片
<input type="file" accept=".png,.jpg,.jpeg,image/png,image/jpg,image/jpeg"> ...
- 数据可视化基础专题(十):Matplotlib 基础(二) 自定义配置文件和绘图风格(rcParams和style)
https://matplotlib.org/api/rcsetup_api.html#module-matplotlib.rcsetup 一.什么是rcParams?我们在使用matplotlibl ...
- Android 性能优化 ---- 启动优化
Android 性能优化 ---- 启动优化 1.为什么要进行启动优化 一款应用的第一印象很重要,第一印象往往决定了用户的去留.打开一款应用,如果速度很快,很顺畅,那么很容易让人觉得这款应用背后的技术 ...
- Java数据类型自动转换(++ ,+=)
在算术表达式中的自动类型转换 数据从类型范围小的自动向数据范围大的转换 整数向浮点数转换(包括long类型向float转换) 例子: char类型的范围内与整数之间转换依据ASCII表 强制转换会丢失 ...
- bzoj2295【POJ Challenge】我爱你啊*
bzoj2295[POJ Challenge]我爱你啊 题意: 求一个字符串中有多少个"luvletter"(不包括引号).字符串长度≤100000. 题解: 连kmp都不用…… ...
- javascript : 找到一个树型数据的一个节点及其所有父节点
如题. (function () { let tree = { "id": 0, "label": "all", "childre ...
- 软件测试必备技能,带你学习jmeter!
一:jmeter用户变量设置: 1.在线程组鼠标右击--添加--配置元件-用户定义的变量, 2.根据业务需求自定义变量的名称,添加需要的变量和对应的值 3.在脚本对应位置添加参数 二:文件参数化: 两 ...
- vue-过渡动画和 第三方动画库导入,带图
vue路由过渡动画 //用transition将路由出口包裹起来 <transition name="fade" mode="out-in"> &l ...