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

赫連小伍發表於2021-09-23

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

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

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

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

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

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

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

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

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

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

實際案例

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

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

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

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

  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 物件的建立過程,可以更專注於自身的業務邏輯,無論是程式碼閱讀或後期維護都更方便

總結

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

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

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

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

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

相關文章