1. 程式人生 > 實用技巧 >大話設計模式讀書筆記(第2章)

大話設計模式讀書筆記(第2章)

人物:小菜,大鳥

事件:做一個商場收銀軟體,營業員根據客戶所購買的商品的單價和數量,向客戶收費


小菜初次嘗試

用兩個文字框輸入單價和數量,一個確定按鈕算出每種商品費用,列表框記錄商品清單,一個標籤記錄總計,最後加上一個重置按鈕,關鍵程式碼如下:

@Slf4j
public class CashierSystem {
    BigDecimal totalPrice = BigDecimal.ZERO;

    private BigDecimal getTotalPrice(BigDecimal price, BigDecimal num) {
        totalPrice = totalPrice.add(price.multiply(num)).setScale(2, RoundingMode.HALF_UP);
        
return totalPrice; } public static void main(String[] args) { CashierSystem result = new CashierSystem(); BigDecimal applePrice = new BigDecimal("2"); BigDecimal appleNumber = new BigDecimal("5"); result.getTotalPrice(applePrice, appleNumber).toString(); BigDecimal peachPrice
= new BigDecimal("6.6"); BigDecimal peachNumber = new BigDecimal("6"); String resultPrice = result.getTotalPrice(peachPrice, peachNumber).toString(); log.info("總價為:{}", resultPrice); } }

大鳥:那現在商場要求對商品搞活動,所有商品打八折

小菜:那在最後的totalPrice乘以0.8不就行了?

大鳥:那這樣不是活動完了,還要把程式碼再改一遍?

小菜:那我增加一個下拉框,可以選擇是打8折還是原價

小菜嘗試將可能打折的內容全部列出:

public enum DiscountTypeEnum {

    NO_DISCOUNT(BigDecimal.ONE, "原價"),

    DISCOUNT_EIGHTY(new BigDecimal("0.8"), "打八折"),

    DISCOUNT_HALF(new BigDecimal("0.5"), "打半折");

    private BigDecimal code;
    private String message;

    DiscountTypeEnum(BigDecimal code, String message) {
        this.code = code;
        this.message = message;
    }

    public BigDecimal getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

然後前端頁面只要選了相應的折扣,就能直接在totalPrice上做打折處理:

@Slf4j
public class CashierSystem {
    BigDecimal totalPrice = BigDecimal.ZERO;

    private BigDecimal getTotalPrice(BigDecimal price, BigDecimal num) {
        totalPrice = totalPrice.add(price.multiply(num)).setScale(2, RoundingMode.HALF_UP);
        return totalPrice;
    }

    public static void main(String[] args) {
        CashierSystem result = new CashierSystem();
        BigDecimal peachPrice = new BigDecimal("6.6");
        BigDecimal peachNumber = new BigDecimal("6");
        String resultPrice = result.getTotalPrice(peachPrice, peachNumber)
                .multiply(DiscountTypeEnum.DISCOUNT_EIGHTY.getCode())
                .toString();
        log.info("總價為:{}", resultPrice);
    }
}

大鳥:但是你列出的打折可能性有限,如果又出現滿300返100,滿700返300的活動,那就不只是做乘法了,肯定還會改變原來程式碼邏輯的,那又該怎麼辦?

小菜:那用簡單工廠模式,根據需求,子類有幾個寫幾個,如:打八折,打半折,滿300返100等,都寫上

大鳥:用設計模式的時候先想想,怎麼用合理,難道後面打三折還要再加一個子類?要知道哪些是同一型別的,哪些是不同的

小菜用簡單工廠模式的嘗試:

先劃分子類的型別,現在可以區分為三種,一種是正常售賣,一種是打折型別,直接初始化引數即可,最後一種是滿減,用兩個引數做傳參即可:

現金收費抽象類:

public abstract class AbstractCashierSystem {
    public abstract BigDecimal acceptCash(BigDecimal money);
}

原價收費子類:

public class CashNormal extends AbstractCashierSystem {
    @Override
    public BigDecimal acceptCash(BigDecimal money) {
        return money;
    }
}

打折收費子類:

@Data
public class CashRebate extends AbstractCashierSystem {
    private String discountType;

    @Override
    public BigDecimal acceptCash(BigDecimal money) {
        //從客戶端傳來的discountType,這裡舉例:"NO_DISCOUNT"
        String discountType = "NO_DISCOUNT";
        return money.multiply(DiscountTypeEnum.valueOf(discountType).getCode());
    }

    CashRebate(String discountType) {
        this.discountType = discountType;
    }
}

返利收費子類:

@Data
public class CashReturn extends AbstractCashierSystem {
    private BigDecimal moneyCondition;
    private BigDecimal moneyReturn;

    @Override
    public BigDecimal acceptCash(BigDecimal money) {
        BigDecimal result = money;
        if (money.compareTo(moneyCondition) > 0) {
            result = money.subtract(
                    money.divide(moneyCondition, 4, RoundingMode.HALF_UP).multiply(moneyReturn)
            );
        }
        return result;
    }

    public CashReturn(BigDecimal moneyCondition, BigDecimal moneyReturn) {
        this.moneyCondition = moneyCondition;
        this.moneyReturn = moneyReturn;
    }

}

收費工廠類:

public class CashFactory {
    public static AbstractCashierSystem createCashAccept(String type) {
        AbstractCashierSystem cs = null;
        switch (type) {
            case "正常收費":
                cs = new CashNormal();
                break;
            case "滿300返100":
                CashReturn cr1 = new CashReturn(new BigDecimal("300"), new BigDecimal("100"));
                cs = cr1;
                break;
            case "打8折":
                CashRebate cr2 = new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage());
                cs = cr2;
                break;
        }
        return cs;
    }
}

客戶端:

@Slf4j
public class CashOperation {
    public static void main(String[] args) {
        BigDecimal totalPrice;
        //比如客戶端選擇的是滿返,滿300返100
        AbstractCashierSystem cs = CashFactory.createCashAccept("滿300返100");
        totalPrice = cs.acceptCash(new BigDecimal("400"));
        log.info("合計總價為:{}", totalPrice);
    }
}

綜上小結:面向物件的程式設計,並不是類越多越好,類的劃分是為了封裝,但分類的基礎是抽象,具有相同屬性和功能的物件的抽象集合才是類。

小菜:這樣設計好後

1.如果是再加滿500返200,則客戶端加一個選項即可

2.如果再出新的促銷方式,如滿100積分10分,則再出一個子類分支即可,不會影響之前程式碼

大鳥:不錯,簡單工廠模式看似已經解決了這個問題,但是隻是解決了物件的建立,從實際情況出發,工廠本身就包含了多種收費模式,可能經常性地更改打折額度和返回額度,那麼每次都要重新改程式碼重新部署,那還有沒有更好的方法來解決呢?

試用策略模式

定義:策略模式定義了演算法家族,分別封裝起來,讓它們之間可以互相替換,此模式讓演算法的變換,不會影響到試用演算法的使用者

什麼是演算法:從上面的例子來看,打折和返利都是一種演算法

為什麼用策略模式:用工廠生成演算法物件,這沒有錯,但演算法本身是一種策略,最重要的是演算法是隨時可以相互替換的,這就是變化點,而封裝變化點是面向物件的一種很重要的方式

策略模式簡要實現:

抽象演算法類(定義支援的所有演算法的公共介面):

public abstract class Strategy {
    public abstract void AlgorithmInterface();
}

具體演算法A:

public class ConcreteStrategyA extends Strategy {
    @Override
    public void AlgorithmInterface() {
    }
}

具體演算法B:

public class ConcreteStrategyB extends Strategy {
    @Override
    public void AlgorithmInterface() {
    }
}

Context:用於對Strategy物件的引用

public class Context {
    Strategy strategy;

    /**
     * 初始化時傳入的策略
     * @param strategy
     */
    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 根據具體策略物件,呼叫其演算法的方法
     */
    public void ContextInterface() {
        strategy.AlgorithmInterface();
    }
}

客戶端程式碼:

public class StrategyCashOperation {
    public static void main(String[] args) {
        Context context;

        context = new Context(new ConcreteStrategyA());
        context.ContextInterface();

        context = new Context(new ConcreteStrategyB());
        context.ContextInterface();
    }
}

小菜嘗試將策略模式融入收銀系統

其實正常收費,返利,滿減都是一種具體策略,AbstractCashierSystem是抽象策略,現在只要加入Context類,再改下客戶端即可:

新增CashContext類:

public class CashContext {
    private AbstractCashierSystem cs;

    public CashContext(AbstractCashierSystem cs) {
        this.cs = cs;
    }

    public BigDecimal GetResult(BigDecimal money) {
        return cs.acceptCash(money);
    }
}

客戶端程式碼調整:

@Slf4j
public class CashOperation {
    public static void main(String[] args) {
        BigDecimal totalPrice;
        CashContext cs = null;
        BigDecimal money;
        //客戶端傳入策略物件type
        String type = "正常收費";
        switch (type) {
            case "正常收費":
                cs = new CashContext(new CashNormal());
                break;
            case "滿300返100":
                cs = new CashContext(new CashReturn(new BigDecimal("300"), new BigDecimal("100")));
                break;
            case "打8折":
                cs = new CashContext(new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage()));
                break;
        }
        //客戶端傳入金額500
        money = new BigDecimal("500");
        totalPrice = cs.GetResult(money);
        log.info("總額:{}", totalPrice);
    }
}

大鳥:策略模式用進去了,但是在客戶端判斷策略模式,不是又走了之前的老路麼,怎麼講判斷轉移?試試策略模式和工廠模式的結合

改造後的CashContext:

public class CashContext {
    private AbstractCashierSystem cs;

    public CashContext(String type) {
        switch (type) {
            case "正常收費":
                CashNormal cs0 = new CashNormal();
                cs = cs0;
                break;
            case "滿300返100":
                CashReturn cs1 = new CashReturn(new BigDecimal("300"), new BigDecimal("100"));
                cs = cs1;
                break;
            case "打8折":
                CashRebate cs2 = new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage());
                cs = cs2;
                break;
        }
    }

    public BigDecimal GetResult(BigDecimal money) {
        return cs.acceptCash(money);
    }
}

客戶端改造:

@Slf4j
public class CashOperation {
    public static void main(String[] args) {
        //客戶端傳入策略物件type
        String type = "正常收費";
        CashContext cs = new CashContext(type);

        //客戶端傳入金額500
        BigDecimal money = new BigDecimal("500");

        //通過對Context的GetResult方法的呼叫,可以得到收取費用的結果,讓具體演算法與客戶進行了格隔離
        BigDecimal totalPrice = cs.GetResult(money);
        log.info("總額:{}", totalPrice);
    }
}

思考:原來簡單工廠模式並非只有建立一個工廠類的做法,也可以這樣做,那麼簡單工廠模式和上面兩種模式的結果到底有什麼不同呢?

簡單工廠模式:

AbstractCashierSystem cs = CashFactory.createCashAccept("滿300返100");

兩者結合:

CashContext cs = new CashContext(type);

答:可以看出,簡單工廠模式要識別AbstractCashierSystem和CashFactory兩個類,而兩種模式結合後,只用識別CashContext一個類就行了

這樣的好處 --> 耦合度更低,我們在客戶端例項化的是CashContect的物件,呼叫的是CashContext的getResult方法,這使得具體的收費演算法與客戶端徹底分離

策略模式解析

1.什麼是策略模式:

(1)策略模式是一種定義一系列演算法的方法

(2)從概念來看,所有演算法完成工作相同只是實現不同,策略模式可以以相同的方式呼叫所有演算法,減少了演算法類與使用演算法類之間的耦合

2.有什麼好處:

(1)解耦,如上面所說

(2)策略模式的Strategy類層次為Context定義了一系列可重用的演算法或行為,繼承有助於析取出這些演算法的公共功能,比如這裡的Strategy類是:AbstractCashierSystem,然後在Context裡定義了getResult()的方法,這樣所有繼承了AbstractCashierSystem的子類,都可以用Context裡的getResutl()方法

(3)簡化了單元測試,每個演算法都有自己的類,可以通過自己的介面單獨測試