設計模式實戰系列之@Builder和建造者模式

iisheng發表於2020-08-12

前言

備受爭議的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。程式碼只需要將上面的ProductConcreteBuilder調整一下即可。

調整後的產品類

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);
    }
}

相應的使用帶BuilderProduct的構造方法。

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介面,兩個介面都定義了快取的基本方法
  • CacheLoaderLocalCache的成員變數
  • LocalCache繼承AbstractMap,是真正意義上的產品類
  • LocalManualCacheCacheBuilderbuild()方法產生的物件的類,LocalManualCache因為有LocalCache作為成員變數,使得它成為了產品類,LocalManualCache實現了Cache介面
  • LocalLoadingCache繼承了LocalManualCache,是CacheBuilderbuild(CacheLoader<? super K1, V1> loader)方法產生的物件的類,LocalLoadingCache實現了LoadingCache介面

總結

什麼時候適合使用建造者模式?

建立物件引數過多的時候

建立一個有很多屬性的物件,如果引數在構造方法中寫,看起來很亂,一長串不說,還很容易寫錯。

物件的部分屬性是可選擇的時候

建立的物件有很多屬性是可選擇的那種,常見的比如配置類等,不同使用者有不同的配置。

物件建立完成後,就不能修改內部屬性的時候

不提供set()方法,使用建造者模式一次性把物件建立完成。

建造者模式和工廠模式的區別是什麼?

  • 建造者模式,通過設定不同的可選引數,“定製化”的建立不同的物件
  • 工廠模式,是直接建立不同但是相關型別的物件(繼承同一父類或者介面的一組子類)

最後想說的

@Builder想到的建造者模式,然後看了StringBuilder以及Guava Cache的原始碼,其中還是有很多值得我們學習的地方。

建造者模式,可能不同的人有不同的理解,不同的實現有不同的方法,但是我們只有深刻的理解了其中的設計思想,才不至於在專案中生搬硬套,才能靈活運用。

參考文獻:
[1]:《設計模式之禪》
[2]:《Effective Java中文版》
[3]:《設計模式之美 建造者模式》

歡迎關注個人微信公眾號【如逆水行舟】,用心輸出基礎、演算法、原始碼系列文章。

相關文章