Stories

Detail Return Return

一個短信服務工廠,從思想到具體實現瞭解工廠模式 - Stories Detail

最近完成了使用阿里雲提供的短信服務,完成了短信推送。代碼中用到了一個工廠類,其實使用的並不是傳統意義上的那三種工廠模式,更大程度上是使用了工廠模式的思想。

先談用到的工廠模式的思想

一、問題場景

系統需要支持多種短信通道(阿里雲、騰訊雲等),根據application.yml的配置選擇使用不同的服務。

  • 開發時,使用本地local避免真實發送
  • 生產時,切換為ali
  • 未來可以輕鬆切換其他廠商

二、實現方式(核心思想)

借鑑工廠模式的思想,將“對象選擇邏輯”集中管理,業務代碼只依賴接口。
具體做法:

  • 所有的短信服務都是ShortMessageSerivce的實現類
  • 工廠啓動時自動收集所有類,按type構建映射表
  • 通過配置文件指定默認type

image.png

關鍵代碼

1、統一接口

public interface ShortMessageService {
    /**
     * 返回唯一標識,如 local = 0, ali = 1
     */
    Short getType();

    /**
     * @param data 短信模板需要的變量
     * @param phoneNumber 手機號
     */
    void sendMessage(ShortMessageDto.TeacherScheduleInfo data, String phoneNumber);
}

2、核心工廠類

@Component
public class ShortMessageServiceFactory {
    private final Short smsTypeValue;
    private final Map<Short, ShortMessageService> serviceMap;

    public ShortMessageServiceFactory(ShortMessageProperties shortMessageProperties,
                                      List<ShortMessageService> shortMessageServices) {
        this.serviceMap = shortMessageServices.stream()
                .collect(Collectors.toConcurrentMap(
                        ShortMessageService::getType, s -> s));
        // 根據短信屬性的配置,獲取到application.yml文件中默認的type對應的code
        this.smsTypeValue = shortMessageProperties.getCode();
    }

    /**
     * 根據配置文件獲取默認類型的service
     */
    public ShortMessageService getDefaultService() {
        return serviceMap.get(this.smsTypeValue);
    }

    public ShortMessageService getService(short type) {
        ShortMessageService service = serviceMap.get(type);
        if (service == null) {
            throw new RuntimeException("不支持的短信類型:" + type);
        }
        return service;
    }
}

3、業務代碼

ShortMessageService shortMessageService = shortMessageServiceFactory.getDefaultService();

三、為什麼要用工廠模式?

使用了工廠模式,有以下幾點好處

  • 如果想要新增短信渠道,使用其他廠商提供的服務,比如想要添加騰訊雲,只需要新增一個TencentSmsServiceImpl。符合開閉原則:對擴展開放,對修改關閉。
  • 業務代碼解耦,Controller或Service只依賴ShortMessageSerivce接口:
ShortMessageService service = factory.getDefaultService();
service.send(...);

四、不使用工廠模式會怎麼樣?

  • 硬編碼if-else
    1、每次新增廠商,必須修改這個方法 -> 違反開閉原則
    2、無法通過修改配置實現切換廠商

    public void sendSms(String phone, String msg, String type) {
      if ("ali".equals(type)) {
          new AliyunSmsService().send(phone, msg);
      } else if ("tencent".equals(type)) {
          new TencentSmsService().send(phone, msg);
      } else if ("huawei".equals(type)) {
          new HuaweiSmsService().send(phone, msg);
      } // 每加一個廠商,這裏就要改!
    }
  • 直接在業務代碼中new
    1、無法統一管理,不同的業務代碼中使用不同實現,行為不一致。
    2、強耦合,如果想要修改廠商,需要修改很多地方。

    // 實際發送短信的業務代碼中
    AliSmsService smsService = new AliyunSmsService();
    smsService.send(...);

傳統的工廠模式

上面的場景只是使用了工廠模式的思想,下面簡單介紹一下傳統的工廠模式——簡單工廠模式、工廠方法、抽象工廠模式。看了實驗室的《Head First 設計模式》這本書,寫的超級好,通俗易懂,所以在這裏借用書裏面的例子,簡單交流一下思想。

一、前言

假設你是一家比薩店的老闆,剛開始只有一種比薩,顧客來了,new一個Pizza
代碼如下:

public Pizza orderPizza() {
  Pizza pizza = new Pizza();
  pizza.prepare();
  pizza.bake();
  pizza.cut();
  pizza.box();
  return pizza
}

那如果想要增加其他風味的比薩,怎麼增加呢?直接修改原代碼,再去增加if else判斷嗎,每要增加一種比薩,就要修改一次代碼,違反開閉原則;new 很多對象,難以管理。

二、簡單工廠模式

這個時候就需要使用到簡單工廠模式了。不自己 new 比薩,而是創建一個簡單的比薩工廠,如果想要增加其他風味的比薩,修改這個工廠就可以了。
代碼如下:

public class SimplePizzaFactory {
    public Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        } else if (type.equals("clam")) {
            pizza = new ClamPizza();
        } else if (type.equals("veggie")) {
            pizza = new VeggiePizza();
        }

        return pizza;
    }
}

可能會説,這跟原來的修改方式不是沒什麼區別嗎?對的,這種方式仍然違背了開閉原則。但是SimplePizzaFactory工廠可以有很多客户,不僅僅有orderPizza,還有PizzaMenu(比薩菜單)等等,這樣將創建比薩的代碼包裝成一個類,當需要修改時,只需要修改工廠的代碼就可以了。

三、工廠方法模式

隨着比薩店越做越大,有了很多加盟店。每家加盟店都想要提供不同風味的比薩(比如紐約、芝加哥、加州),那麼比薩的製作就受到了地區的影響,不同的地區製作方法不同。

image.png
代碼如下:

public abstract class PizzaStore {
    public Pizza orderPizza(String type) {
        Pizza pizza = createPizza(type); // 工廠方法
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }

    // 工廠方法 —— 由子類實現抽象的方法
    protected abstract Pizza createPizza(String type);
}

做到了真正的“對擴展開放”——加新分店只需要繼承,實現對應的抽象方法。

四、抽象工廠模式

隨着比薩店越開越大,怎麼保證每家加盟店使用的都是高品質的原料,來保證比薩的質量呢?但是對於不同地區不同的比薩加盟店,要用到的原料也不相同。
需要引入原料工廠,不同地區的加盟店通過實現原料工廠製作原料。

image.png

原料工廠代碼如下:

public interface PizzaIngredientFactory {
    Dough createDough();
    Sauce createSauce();
    Cheese createCheese();
    Veggies[] createVeggies();
    Pepperoni createPepperoni();
    Clams createClam();
}

紐約原料工廠:

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
    public Dough createDough() {
        return new ThinCrustDough();
    }
    public Cheese createCheese() {
        return new ReggianoCheese();
    }
}

製作比薩:

public abstract class Pizza {
    protected Dough dough;
    protected Sauce sauce;
    protected Cheese cheese;

    public abstract void prepare();

    // 其他方法 bake(), cut(), box()
}

public class CheesePizza extends Pizza {
    PizzaIngredientFactory ingredientFactory;

    public CheesePizza(PizzaIngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;
    }

    @Override
    public void prepare() {
        this.dough = ingredientFactory.createDough();
        this.sauce = ingredientFactory.createSauce();
        this.cheese = ingredientFactory.createCheese();
    }
}

抽象工廠解決的不是單個產品怎麼變,而是構建了整個產品族,一組相關產品如何整體切換。

五、對比總結

模式 解決的問題 擴展是否需要修改代碼 適合場景
簡單工廠 隱藏創建細節,集中管理 要改(加 if) 產品少、變化少
工廠方法 每個產品由獨立工廠創建 不用改(加新工廠類) 需要靈活擴展單個產品
抽象工廠 創建“一組相關產品” 要改(加新產品需改所有工廠) 產品成族、需整體切換

總結

本文只是對工廠模式進行了一個簡單的瞭解,主要是學習了工廠模式的思想,沒有在具體場景中實踐。主要參考《Head First設計模式》這本書中的例子。如有不對,請多指教。

Add a new Comments

Some HTML is okay.