JAVA通過設計模式消除ifelse和重複程式碼
通過設計模式消除ifelse和重複程式碼
使用該方法前,先來了解一下工廠模式以及模板方法模式
工廠模式(建立型模式)
工廠模式分為3種
- 簡單工廠模式(Simple Factory Pattern)
- 工廠方法模式(Factory Method Pattern)
- 抽象工廠模式(Abstract Factory Pattern)
簡單工廠模式(Simple Factory Pattern)
模擬場景是,有一家汽車廠(AutoFactory)要生產汽車,主要生產巴士(Bus)和轎車(Car),使用程式碼模擬如下:
public interface Auto { //汽車具有被駕駛功能 void drive(); }
然後設計兩種汽車:轎車和巴士
class Bus implements Auto{
@Override
public void drive() {
// 巴士駕駛方式
}
}
class Car implements Auto{
@Override
public void drive() {
// 轎車駕駛方式
}
}
開始建造"工廠"
public class AutoFactory{ // 生產汽車 public Auto produceCar(String type){ if("car".equals(type)){ return new Car(); }else if("bus".equals(type)){ return new Car(); } return null; } }
若需要生產一臺轎車,則需通過工廠建立
AutoFactory autoFactory = new AutoFactory();
Auto car = factory.produce("car");
car.drive();
簡單工廠模式實現了生成產品類的程式碼跟具體的產品實現分離
在工廠類中你可以新增所需的生成產品的邏輯程式碼
但是問題來了,這不符合“開放-封閉”原則的,也就是說對擴充套件開放,對修改關閉,如果你要加一個新的汽車型別還需要修改produce方法,為解決這個問題,從而引入了工廠方法模式(Factory Method Pattern)。
工廠方法模式(Factory Method Pattern)
工廠為了擴大市場,現在要開始生產卡車(Truck)了,於是我們設計一輛卡車:
class Truck implements Auto{
@Override
public void drive() {
// 卡車駕駛方式
}
}
按照簡單工廠的邏輯,需要修改Produce方法(也就是說要改造已有的工廠),這樣會影響已有生產,那怎麼解決呢?辦法是再建新的工廠:
首先設計一個工廠原型(工廠介面):
public interface IAutoFactory{
// 生產汽車
public Auto produce(String type)
}
然後將原來的工廠簡單改造符合設計好的工廠原型(實現介面即可):
public class AutoFactory implements IAutoFatory{
// 生產汽車
public Auto produceCar(String type){
if("car".equals(type)){
return new Car();
}else if("bus".equals(type)){
return new Car();
}
return null;
}
}
接下來為了生產卡車,我們要為卡車單獨建廠
public class TruckAutofFactory implements IAutofactory{
//生產卡車
@Override
public Auto produce(){
return new Truck();
}
}
開始生產卡車:
IAutoFactory factory = new TruckAutofFactory();
Auto car = factory.produce();
car.drive();
這裡的抽象工廠中,我們為了減少改造成本,在簡單工廠基礎上做最小修改,理論上produce引數可以沒有,然後為小轎車、大巴車和卡車分別建立工廠,分別生產。這樣如果有了新的型別的車,可以不改動之前的程式碼,新建一個“工廠”即可,做到“開放封閉原則”。
雖然看似類變多了,邏輯複雜了,但是這種改造帶來的好處也是顯而易見的:不變動老的程式碼,通過新建工廠類完成新功能的新增,老功能不變,最大限度的避免動了老程式碼的邏輯導致引入新的bug。
工廠方法的結構圖如下:
抽象工廠模式(Abstract Factory Pattern)
我們繼續針對汽車工廠說明,由於接下來工廠需要繼續擴大規模,開始涉足汽車配件,上層決定涉足汽車大燈業務,針對已有車型生產前大燈。但是如果按照工廠方法模式,需要再繼續新建一批工廠,針對每種汽車再建N個工廠,考慮到成本和簡單性,針對對已有汽車工廠改造。
首先“設計”大燈原型:
// 大燈
public interface Light(){
// 開燈
public void turnOn();
}
再“設計”小轎車、大巴車和卡車大燈:
public carLight() implements Light{
@Override
public void turnOn(){
// 轎車大燈
}
}
public busLight() implements Light{
@Override
public void turnOn(){
// 巴士大燈
}
}
public trustLight() implements Light{
@Override
public void turnOn(){
// 卡車大燈
}
}
接下來我們重新“設計”原有的汽車工廠(修改工廠介面或者抽象工廠類)
public interface IAutoFactory{
// 生產汽車
public Auto produce(String type);
// 生產大燈
public Light produceLight();
}
好的,改造工廠,首先改造卡車工廠:
public class TruckAutofFactory implements IAutofactory{
//生產卡車
@Override
public Auto produce(){
return new Truck();
}
// 生產車燈
@Override
public Auto produce2(){
return new trustLight();
}
}
就可以使用TruckAutofFactory生產卡車了
模板方法模式(行為型模式)
模板方法模式定義了一個演算法的步驟,並允許子類別為一個或多個步驟提供其實踐方式。讓子類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。
模板方法(Template Method)模式包含以下主要角色:
-
抽象類:負責給出一個演算法的輪廓和骨架。它由一個模板方法和若干個基本方法構成。
-
模板方法:定義了演算法的骨架,按某種順序呼叫其包含的基本方法。
-
基本方法:是模板方法的組成部分,可以分為三種:
-
抽象方法:一個抽象方法由抽象類宣告、由其具體子類實現
-
具體方法:一個具體方法由一個抽象類或具體類宣告並實現,其子類可以進行覆蓋也可以直接繼承。
-
鉤子方法:在抽象類中已經實現,包括用於判斷的邏輯方法和需要子類重寫的空方法兩種。
一般鉤子方法是用於判斷的邏輯方法,這類方法名一般為isXxx,返回值型別為boolean型別
-
-
-
具體子類:實現抽象類中所定義的抽象方法和鉤子方法,它們是一個頂級邏輯的組成步驟。
使用場景通常是:一些方法通用,卻在每一個子類都重新寫了這一方法。(即再向上抽取,提取公共程式碼)
案例:銀行辦理業務為例子。去銀行辦理業務一般要經過以下4個流程:取號、排隊、辦理具體業務、對銀行工作人員進行評分等,其中取號、排隊和對銀行工作人員進行評分的業務對每個客戶是一樣的,可以在父類中實現,但是辦理具體業務卻因人而異,它可能是存款、取款或者轉賬等,可以延遲到子類中實現。
實現:
public abstract class AbstractClass {
public final void takeNumber(){
// 取號
}
public final void queueUp(){
// 排隊
}
/* 辦理具體業務 根據字類的需求來進行實現 */
public abstract void handleBusiness();
public final void score(){
// 評分
}
}
模擬不同的顧客
public class AUser extends AbstractClass{
@Override
public void handleBusiness() {
this.takeNumber();
this.queueUp();
// todo貸款邏輯
this.score();
}
}
public class BUser extends AbstractClass{
@Override
public void handleBusiness() {
this.takeNumber();
this.queueUp();
// todo存錢邏輯
this.score();
}
}
客戶端:
AbstractClass aUser = new AUser();
aUser.handleBusiness();
結合兩種設計模式進行消除程式碼重複
假設要開發一個購物車下單的功能,針對不同使用者進行不同處理:【這個時候第一反應是想到模板方法模式】
- 普通使用者需要收取運費,運費是商品價格的10%,無商品折扣;
- VIP使用者同樣需要收取商品價格10%的快遞費,但購買兩件以上相同商品時,第三件開始享受一定折扣;
- 內部使用者可以免運費,無商品折扣。
目標是實現三種類型的購物車業務邏輯,把入參Map物件(Key是商品ID,Value是商品數量),轉換為出參購物車型別Cart。
先實現針對普通使用者的購物車處理邏輯:
//購物車
@Data
public class Cart {
//商品清單
private List<Item> items = new ArrayList<>();
//總優惠
private BigDecimal totalDiscount;
//商品總價
private BigDecimal totalItemPrice;
//總運費
private BigDecimal totalDeliveryPrice;
//應付總價
private BigDecimal payPrice;
}
//購物車中的商品
@Data
public class Item {
//商品ID
private long id;
//商品數量
private int quantity;
//商品單價
private BigDecimal price;
//商品優惠
private BigDecimal couponPrice;
//商品運費
private BigDecimal deliveryPrice;
}
//普通使用者購物車處理
public class NormalUserCart {
public Cart process(long userId, Map<Long, Integer> items){
Cart cart = new Cart();
//把Map的購物車轉換為Item列表
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);
//處理運費和商品優惠
itemList.stream().forEach(item -> {
//運費為商品總價的10%
item.setDeliveryPrice(item.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity()))
.multiply(new BigDecimal("0.1")));
//無優惠
item.setCouponPrice(BigDecimal.ZERO);
});
//計算商品總價
cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
//計算運費總價
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//計算總優惠
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//應付總價=商品總價+運費總價-總優惠
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}
}
然後實現針對VIP使用者的購物車邏輯。與普通使用者購物車邏輯的不同在於,VIP使用者能享受同類商品多買的折扣。所以,這部分程式碼只需要額外處理多買折扣部分:
public class VipUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
...
itemList.stream().forEach(item -> {
//運費為商品總價的10%
item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
//購買兩件以上相同商品,第三件開始享受一定折扣
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
});
...
return cart;
}
}
最後是免運費、無折扣的內部使用者,同樣只是處理商品折扣和運費時的邏輯差異:
public class InternalUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
...
itemList.stream().forEach(item -> {
//免運費
item.setDeliveryPrice(BigDecimal.ZERO);
//無優惠
item.setCouponPrice(BigDecimal.ZERO);
});
...
return cart;
}
}
對比程式碼可發現,三種邏輯理論有大部分的程式碼是重複的
有了三個購物車後,我們就需要根據不同的使用者型別使用不同的購物車了。如下程式碼所示,使用三個if實現不同型別使用者呼叫不同購物車的process方法:
@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {
//根據使用者ID獲得使用者型別
String userCategory = Db.getUserCategory(userId);
//普通使用者處理邏輯
if (userCategory.equals("Normal")) {
NormalUserCart normalUserCart = new NormalUserCart();
return normalUserCart.process(userId, items);
}
//VIP使用者處理邏輯
if (userCategory.equals("Vip")) {
VipUserCart vipUserCart = new VipUserCart();
return vipUserCart.process(userId, items);
}
//內部使用者處理邏輯
if (userCategory.equals("Internal")) {
InternalUserCart internalUserCart = new InternalUserCart();
return internalUserCart.process(userId, items);
}
return null;
}
電商的營銷玩法是多樣的,以後勢必還會有更多使用者型別,需要更多的購物車。我們就只能不斷增加更多的購物車類,一遍一遍地寫重複的購物車邏輯、寫更多的if邏輯嗎?
我們的原則是相同的程式碼只在一處地方出現
如果我們熟記抽象類和抽象方法的定義的話,這時或許就會想到,是否可以把重複的邏輯定義在抽象類中,三個購物車只要分別實現不同的那份邏輯
這個就是模板方法模式。我們在父類中實現了購物車處理的流程模板,然後把需要特殊處理的地方留空白也就是留抽象方法定義,讓子類去實現其中的邏輯。由於父類的邏輯不完整無法單獨工作,因此需要定義為抽象類
public abstract class AbstractCart {
//處理購物車的大量重複邏輯在父類實現
public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);
//讓子類處理每一個商品的優惠
itemList.stream().forEach(item -> {
// todo
processCouponPrice(userId, item);
// todo
processDeliveryPrice(userId, item);
});
//計算商品總價
cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
//計算總運費
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//計算總折扣
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//計算應付價格
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}
//處理商品優惠的邏輯留給子類實現
protected abstract void processCouponPrice(long userId, Item item);
//處理配送費的邏輯留給子類實現
protected abstract void processDeliveryPrice(long userId, Item item);
}
有了這個抽象類,三個子類的實現就非常簡單了。
// 普通使用者的購物車NormalUserCart,實現的是0優惠和10%運費的邏輯:
@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {
@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}
@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(item.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity()))
.multiply(new BigDecimal("0.1")));
}
}
// VIP使用者的購物車VipUserCart,直接繼承了NormalUserCart,只需要修改多買優惠策略:
@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {
@Override
protected void processCouponPrice(long userId, Item item) {
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
}
}
// 內部使用者購物車InternalUserCart是最簡單的,直接設定0運費和0折扣即可:
@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {
@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}
@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(BigDecimal.ZERO);
}
}
抽象類和三個子類的實現關係圖,如下所示:
接下來,我們再看看如何能避免三個if邏輯。
定義三個購物車子類時,我們在@Service註解中對Bean進行了命名。既然三個購物車都叫XXXUserCart,那我們就可以把使用者型別字串拼接UserCart構成購物車Bean的名稱,然後利用Spring的IoC容器,通過Bean的名稱直接獲取到AbstractCart,呼叫其process方法即可實現通用。
這 就是工廠模式,只不過是藉助Spring容器實現罷了:
@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
String userCategory = Db.getUserCategory(userId);
AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
return cart.process(userId, items);
}
試想, 之後如果有了新的使用者型別、新的使用者邏輯,是不是完全不用對程式碼做任何修改,只要新增一個XXXUserCart類繼承AbstractCart,實現特殊的優惠和運費處理邏輯就可以了?
這樣一來,我們就利用工廠模式+模板方法模式,不僅消除了重複程式碼,還避免了修改既有程式碼的風險。這就是設計模式中的開閉原則:對修改關閉,對擴充套件開放。
ps:學習朱曄老師課程總結