設計模式(三):生成器模式

這是設計模式系列文章的第三篇

之前兩篇的閱讀效果不是很好,我一度懷疑這種題材的文章不受大家歡迎,直到前兩天我面試了一個小姐姐…

面試過程中和小姐姐聊起她在上家公司做過的項目,其中有一個功能,根據小姐姐的描述,我第一感覺應該用生成器模式來實現

小姐姐說她並沒有用生成器模式,就是簡單的硬編碼

我問她為什麼不使用生成器模式實現的時候,小姐姐的一句話突破了我的認知下線

小姐姐說:我不知道什麼是生成器模式,我不打算做架構師,沒必要學設計模式

原來她認為設計模式只有在做架構設計的時候才會用到,跟普通程式設計師沒有關係

我覺得小姐姐的觀點存在嚴重問題,設計模式是程式設計師的基本技能,每個程式設計師都應該掌握並靈活應用

良好的程式碼設計不僅可以讓程式碼重複性更高,還能使程式碼更易讀從而降低程式碼後期的維護成本,最重要的是可以提高系統的可靠性

今天,我們就使用生成器模式來實現小姐姐的需求

實際案例

我們先來看一下這個小姐姐的項目的具體需求

根據用戶近期的消費金額、消費次數、瀏覽商品類型、商品價格區間等一些屬性,生成用戶畫像。根據畫像分析用戶行為,實現精準營銷或刺激消費等。

當然,不同的業務關注的角度也不同。比如精準營銷業務關注的是用戶近半年的數據,而且以消費數據為主;刺激消費業務關注的是用戶近一個月的數據,而且以常打開的商品為主

從編程角度把需求提煉一下,大概就是以下兩點:

  1. 提供一個用戶對象,這個對象包括用戶名、消費金額、消費次數、瀏覽商品類型、商品價格區間等屬性
  2. 根據這個對象進行一些業務處理

我們先來看一下小姐姐當初是怎麼實現這個需求的

小姐姐的程式碼是在精準營銷和刺激消費的業務邏輯裡面,分別創建了一個User對象。

兩個業務中創建User對象的邏輯基本一樣,只有在獲取近期消費數據時稍有差別。一個是獲取近半年的數據,另一個是獲取近一個月的數據

這樣的硬編碼是把User對象的創建過程,嵌入到了其他業務邏輯裡面,這就造成一些問題

  • 問題一:程式碼重用性降低

User對象的創建邏輯基本一樣,但是寫了兩遍。如果後期加入新的業務,User對象的創建邏輯還要再寫一遍,程式碼重用性太低

  • 問題二:維護成本增大

示例中的偽程式碼模擬的比較簡單,實際上User對象的創建過程非常複雜,需要查詢各種數據並且對數據進行過濾、分類、整理,程式碼可能有幾百行

精準營銷或刺激消費的業務邏輯也是非常複雜的,把兩塊複雜的邏輯寫到一塊,後期閱讀或維護程式碼的成本將幾何倍的增長

  • 問題三:程式碼耦合度增加

將兩塊業務邏輯寫到一起,其中不免會共享一些邏輯。

如果後期想對共享的邏輯進行修改,讓其僅對其中一方生效,程式碼的修改是很不友好的,很容易造成另一方的邏輯漏洞

我們可以嘗試使用生成器模式來解決這些問題

生成器模式

生成器模式定義

生成器模式是將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示

換成大白話理解就是:一個複雜的對象,它的創建過程和使用過程要分開。對於對象的使用者來說,我只需要告訴創建者我需要使用這個複雜對象,至於這個複雜對象是怎麼創建的,不關我事 (ps:有點渣男的味道)

生成器模式使用場景

在創建一個對象時,同時滿足以下條件,可以使用生成器模式

  1. 對象的創建過程非常複雜
  2. 對象的創建步驟固定
  3. 不同的調用者獲得的對象不完全相同

如果需要創建的對象不複雜,這時候是沒必要使用生成器模式的。因為生成器模式本身的程式碼實現有一點複雜,使用它成本有點高,還不如簡單的硬編碼

如果對象的創建步驟不固定,也不推薦使用生成器模式。

假如在小姐姐的項目中,如果精準營銷需要用戶的消費數據,不需要瀏覽商品數據;刺激消費需要用戶的瀏覽商品數據,不需要消費數據。

User對象的創建步驟就是

兩個業務創建User對象的步驟是不一樣的,這時候不適合使用生成器模式

如果所有的調用者需要的對象完全一樣,也不需要使用生成器模式。

假如小姐姐的需求中,兩個業務關注的消費數據都是近一個月的,對消費數據和商品數據的關注度也是一樣的,就不需要使用生成器模式,只需要把User對象的創建過程進行單獨的封裝,兩個業務直接調用即可

生成器模式實戰

我們先來看一下生成器模式的架構

套用到我們需求中,User對象的創建就是下面這個樣子

下面使用程式碼實現小姐姐的需求,首先定義我們要創建的對象,也就是 User

public class User {
    private String nickname;
    private int payCnt;
    private int payAmt;
    private List<String> productType;
    private List<String> amtInterval;

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public void setPayCnt(int payCnt) {
        this.payCnt = payCnt;
    }

    public void setPayAmt(int payAmt) {
        this.payAmt = payAmt;
    }

    public void setProductType(List<String> productType) {
        this.productType = productType;
    }

    public void setAmtInterval(List<String> amtInterval) {
        this.amtInterval = amtInterval;
    }
}

第二步,編寫構造介面定義創建 User 對象需要的步驟,並提供返回 User 對象的方法

public interface IUserBuilder {
    // 構建用戶昵稱
    String buildNicaname();
    // 構建用戶消費次數,days代表最近天數
    int buildPayCnt();
    // 構建用戶消費金額,days代表最近天數
    int buildPayAmt();
    // 構建用戶經常瀏覽商品類型
    List<String> buildProductType();
    // 構建用戶經常瀏覽商品價格區間
    List<String> buildAmtInterval();
    // 獲取user對象
    User getUser();
}

第三步,編寫構造介面的具體實現類,重寫每一個方法,編寫每一個方法的具體實現邏輯。

public class UserBuilder implements IUserBuilder {

    private String days;

    public UserBuilder(String days) {
        this.days = days;
    }

    @Override
    public String buildNicaname() {
        String nicaname = "赫連小伍";
        System.out.println("查詢用戶昵稱為:" + nicaname);
        return nicaname;
    }

    @Override
    public int buildPayCnt() {
        int payCnt = 0;
        if ("30".equals(days)) {
            payCnt = 1;
        } else{
            payCnt = 10;
        }
        System.out.println("查詢用戶近" + days + "天的消費筆數為:" + payCnt);
        return payCnt;
    }

    @Override
    public int buildPayAmt() {
        int payAmt = 0;
        if ("30".equals(days)) {
            payAmt = 2;
        } else{
            payAmt = 100;
        }
        System.out.println("查詢用戶近" + days + "天的消費金額為:" + payAmt);
        return payAmt;
    }

    @Override
    public List<String> buildProductType() {
        List<String> list = new ArrayList<>();
        list.add("增發劑");
        list.add("格子衫");
        System.out.println("查詢用戶瀏覽的商品類型為:" + list);
        return list;
    }

    @Override
    public List<String> buildAmtInterval() {
        List<String> list = new ArrayList<>();
        list.add("1-9");
        list.add("2-10");
        System.out.println("查詢用戶瀏覽的商品價格區間為:" + list);
        return list;
    }

    @Override
    public User getUser() {
        User user = new User();
        user.setNickname(this.buildNicaname());
        user.setPayCnt(this.buildPayCnt());
        user.setPayAmt(this.buildPayAmt());
        user.setProductType(this.buildProductType());
        user.setAmtInterval(this.buildAmtInterval());
        return user;
    }
}

第四步,編寫 Director 類,對精準營銷和刺激消費兩塊業務分別提供對應的獲取 User 的方法。這裡為了方便調用,方法全部採用 static

public class Director {

    // 為精準營銷提供獲取User的方法
    public static User getJzyxUser() {
        IUserBuilder userBuilder = new UserBuilder("360");
        return userBuilder.getUser();
    }

    // 為刺激消費提供獲取User的方法
    public static User getCjxfUser() {
        IUserBuilder userBuilder = new UserBuilder("30");
        return userBuilder.getUser();
    }
}

最後一步,模擬精準營銷和刺激消費的業務,分別獲取對應的 User 對象

 public static void main(String[] args) {
 // 模擬精準營銷業務邏輯
 User jzyxUser = Director.getJzyxUser();
 System.out.println("精準營銷獲得的User對象為:" + jzyxUser);
 System.out.println("開始精準營銷的業務邏輯");

 // 模擬刺激消費業務邏輯
 User cjxfUser = Director.getCjxfUser();
 System.out.println("刺激消費獲得的User對象為:" + cjxfUser);
 System.out.println("開始刺激消費的業務邏輯");
}

這就用生成器模式實現了小姐姐的需求

對於精準營銷或刺激消費的業務邏輯來說,它們不用再關心 User 對象的創建過程,可以更專註於自身的業務邏輯,無論是程式碼閱讀或後期維護都更方便

總結

生成器模式也被稱作創建者模式或建造者模式,它屬於設計模式三大類型中的創建型模式

與工廠模式相比,生成器模式更善於處理創建步驟固定的複雜對象。它與工廠模式並沒有很明顯的界限,在許多設計初期,大部分程式設計師都習慣用工廠方法模式來構建程式碼,隨著業務變得複雜,程式碼也會不斷的重構。程式碼架構也逐漸的演變成抽象工廠模式、生成器模式

生成器模式也不能頻繁的使用,如果項目的內部變化複雜,可能會導致需要定義很多具體生成器類來實現這種變化,導致系統變得很龐大

每一種設計模式都有利有弊,權衡利弊後找出適合自己項目的模式才會使程式碼變得更 「完美」

— 以上內容來自公眾號 赫連小伍,轉載請註明出處 ​