1. 程式人生 > 其它 >JAVA通過設計模式消除ifelse和重複程式碼

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:學習朱曄老師課程總結