前言
備受爭議的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]:《設計模式之美 建造者模式》
歡迎關注個人微信公眾號【如逆水行舟】,用心輸出基礎、演算法、原始碼系列文章。