大話設計模式讀書筆記(第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)簡化了單元測試,每個演算法都有自己的類,可以通過自己的介面單獨測試