設計模式學習筆記(二)工廠模式、模板模式和策略模式的混合使用

Ethan_Wong發表於2022-03-23

一、工廠模式(Factory pattern)

工廠模式又叫做工廠方法模式,是一種建立型設計模式,一般是在父類中提供一個建立物件的方法,允許子類決定例項化物件的型別。

1.1 工廠模式介紹

工廠模式是Java 中比較常見的一種設計模式,實現方法是定義一個統一建立物件的介面,讓其子類自己決定去例項化那個工廠類,解決不同條件下建立不同例項的問題。工廠方法模式在實際使用時會和其他的設計模式一起結合,而不是單獨使用。比如在Lottery 專案中獎品的發放就是工廠+模板+策略模式。

1.2 工廠模式實現

舉個例子,比如要實現不同獎品的發放業務,有優惠券、實體商品和會員電子卡這些獎品,那麼我們可以定義這三種型別獎品的介面:

序號 型別 介面 描述
1 優惠券 CouponResult sendCoupon(String uId, String couponNumber, String uuid) 返回優惠券資訊(物件型別)
2 實體商品 Boolean deliverGoods(DeliverReq req) 返回是否傳送實體商品(布林型別)
3 愛奇藝會員電子卡 void grantToken(String bindMobileNumber, String cardId) 執行發放會員卡(空型別)

從上表可以看出,不同的獎品有不同的返回型別需求,那麼我們該如何處理這些資料,並對應返回呢?常規思路可以想到通過統一的入參AwardReq,出參AwardRes,外加上一個PrizeController來具體實現這些獎品的資料處理任務:

AwardReq
AwardRes
PrizeController

但是這樣勢必會造成PrizeController這個類中邏輯判斷過多,後期如果要繼續擴充套件獎品型別,是非常困難和麻煩的。比如可以看看PrizeController中的程式碼:

public class PrizeController {

    private Logger logger = LoggerFactory.getLogger(PrizeController.class);

    public AwardRes AwardToUser(AwardReq awardReq) {
        String reqJson = JSON.toJSONString(awardReq);
        AwardRes awardRes = null;

        try {
            logger.info("獎品發放開始{}。 awardReq:{}", awardReq.getuId(), reqJson);
            if (awardReq.getAwardType() == 1) {
                CouponService couponService = new CouponService();
                CouponResult couponResult = couponService.sendCoupon(awardReq.getuId(), awardReq.getAwardNumber(), awardReq.getBizId());
                if ("0000".equals(couponResult.getCode())) {
                    awardRes = new AwardRes(0000, "發放成功");
                } else {
                    awardRes = new AwardRes(0001, "傳送失敗");
                }
            } else if (awardReq.getAwardType() == 2) {
                GoodsService goodsService = new GoodsService();
                DeliverReq deliverReq = new DeliverReq();
                deliverReq.setUserName(queryUserName(awardReq.getuId()));
                deliverReq.setUserPhone(queryUserPhoneNumber(awardReq.getuId()));
                deliverReq.setSku(awardReq.getAwardNumber());
                deliverReq.setOrderId(awardReq.getBizId());
                deliverReq.setConsigneeUserName(awardReq.getExtMap().get("consigneeUserName"));
                deliverReq.setConsigneeUserPhone(awardReq.getExtMap().get("consigneeUserPhone"));
                deliverReq.setConsigneeUserAddress(awardReq.getExtMap().get("consigneeUserAddress"));
                Boolean isSuccess = goodsService.deliverGoods(deliverReq);
                if (isSuccess) {
                    awardRes = new AwardRes(0000, "發放成功");
                } else {
                    awardRes = new AwardRes(0001, "傳送失敗");
                }
            } else {
                IQiYiCardService iQiYiCardService = new IQiYiCardService();
                iQiYiCardService.grantToken(queryUserPhoneNumber(awardReq.getuId()), awardReq.getAwardNumber());
                awardRes = new AwardRes(0000, "傳送成功");
            }
            logger.info("獎品發放完成{}。", awardReq.getuId());
        } catch (Exception e) {
            logger.error("獎品發放失敗{}。req:{}", awardReq.getuId(), reqJson, e);
            awardRes = new AwardRes(0001, e.getMessage());
        }
        return awardRes;

    }

PrizeController的類中,我們發現使用了很多簡單的if-else判斷。而且整個程式碼看起來很長,對於後續迭代和擴充套件會造成很大的麻煩,因此在考慮設計模式的單一職責原則後,我們可以利用工廠模式對獎品處理返回階段進行抽取,讓每個業務邏輯在自己所屬的類中完成。

首先,我們從業務邏輯中發現無論是那種獎品,都需要傳送,因此可以提煉出統一的入參介面和傳送方法:ICommoditysendCommodity(String uId, String awardId, String bizId, Map<String, String> extMap)入參內容包括使用者Id,獎品Id,yewuId,擴充套件欄位進行實現業務邏輯的統一,具體如下UML圖

然後,我們可以在具體的獎品內部實現對應的邏輯。

最後建立獎品工廠StoreFactory,可以通過獎品型別判斷來實現不同獎品的服務,如下所示:

public class StoreFactory {

    public ICommodity getCommodityService(Integer commodityType) {
        if (null == commodityType) {
            return null;
        }
        if (1 == commodityType) {
            return new CouponCommodityService();
        }
        if (2 == commodityType) {
            return new GoodsCommodityService();
        }
        if (3 == commodityType) {
            return new CardCommodityService();
        }
        throw new RuntimeException("不存在的商品服務型別");
    }
}

二、模板模式(Template pattern)

模板模式的核心就是:通過一個公開定義抽象類中的方法模板,讓繼承該抽象類的子類重寫方法實現該模板。

2.1 模板模式介紹

定義一個操作的大致框架,然後將具體細節放在子類中實現。也就是通過在抽象類中定義模板方法,讓繼承該子類具體實現模板方法的細節。

2.2 模板模式實現

舉個例子,在爬取不同網頁資源並生成對應推廣海報業務時,我們會有固定的步驟,如:模擬登入、爬取資訊、生成海報。這個時候就可以將流程模板抽離出來,讓對應子類去實現具體的步驟。比如爬取微信公眾號、淘寶、京東、噹噹網的網頁服務資訊。

首先,定義一個抽象類NetMall,然後再在該類中定義對應的模擬登入login、爬取資訊reptile、生成海報createBase的抽象方法讓子類繼承。具體程式碼如下所示:

public abstract class NetMall {

    String uId;   // 使用者ID
    String uPwd;  // 使用者密碼

    public NetMall(String uId, String uPwd) {
        this.uId = uId;
        this.uPwd = uPwd;
    }
    // 1.模擬登入
    protected abstract Boolean login(String uId, String uPwd);

    // 2.爬蟲提取商品資訊(登入後的優惠價格)
    protected abstract Map<String, String> reptile(String skuUrl);

    // 3.生成商品海報資訊
    protected abstract String createBase64(Map<String, String> goodsInfo);
    
    /**
     * 生成商品推廣海報
     *
     * @param skuUrl 商品地址(京東、淘寶、噹噹)
     * @return 海報圖片base64位資訊
     */
    public String generateGoodsPoster(String skuUrl) {
        if (!login(uId, uPwd)) return null;             // 1. 驗證登入
        Map<String, String> reptile = reptile(skuUrl);  // 2. 爬蟲商品
        return createBase64(reptile);                   // 3. 組裝海報
    }

}

接下來以抓取京東網頁資訊為例實現具體步驟:

public class JDNetMall extends NetMall {

    public JDNetMall(String uId, String uPwd) {
        super(uId, uPwd);
    }
    //1.模擬登入
    public Boolean login(String uId, String uPwd) {
        return true;
    }
    //2.網頁爬取
    public Map<String, String> reptile(String skuUrl) {
        String str = HttpClient.doGet(skuUrl);
        Pattern p9 = Pattern.compile("(?<=title\\>).*(?=</title)");
        Matcher m9 = p9.matcher(str);
        Map<String, String> map = new ConcurrentHashMap<String, String>();
        if (m9.find()) {
            map.put("name", m9.group());
        }
        map.put("price", "5999.00");
        return map;
    }
    //3.生成海報
    public String createBase64(Map<String, String> goodsInfo) {
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(JSON.toJSONString(goodsInfo).getBytes());
    }

}

最後進行測試:

@Test
public void test_NetMall() {
    NetMall netMall = new JDNetMall("ethan", "******");
    String base64 = netMall.generateGoodsPoster("https://item.jd.com/100008348542.html");
}

模板模式主要是提取子類中的核心公共程式碼,讓每個子類對應完成所需的內容即可。

三、策略模式(Strategy Pattern)

策略模式是一種行為型別模式,如果在一個系統中有許多類,而區分他們的只是它們的行為,這個時候就可以利用策略模式來進行切換。

3.1 策略模式介紹

在側率模式中,我們建立表示各種策略的物件和一個行為隨著側率物件改變而改變的 context 物件。

比如諸葛亮的錦囊妙計,每一個錦囊都是一個策略。在業務邏輯中,我們一般是使用具有同類可替代的行為邏輯演算法場景,比如,不同型別的交易方式(信用卡、支付寶、微信),生成唯一ID的策略(UUID、雪花演算法、Leaf演算法)等,我們都可以先用策略模式對其進行行為包裝,然後提供給外界進行呼叫。

注意,如果一個系統中的策略多於四個,就需要考慮使用混合模式,解決策略類膨脹的問題。

3.2 策略模式實現

就拿生成唯一ID業務來舉例子,比如在雪花演算法提出之前,我們一般使用的是UUID 來確認唯一ID。但是如果需要有序的生成ID,這個時候就要考慮一下其他的生成方法,比如雪花、Leaf等演算法了。

可能剛開始我們是直接寫一個類,在類裡面呼叫UUID演算法來生成,但是需要呼叫其他方法時,我們就必須在這個類裡面用if-else等邏輯判斷,然後再轉換成另外的演算法中。這樣的做法和前面提到的工廠模式一樣,會提高類之間的耦合度。所以我們可以使用策略模式將這些策略抽離出來,單獨實現,防止後期若需要擴充套件帶來的混亂。

首先,定義一個ID生成的介面IIdGenerator

public interface IIdGenerator {
    /**
     * 獲取ID, 目前有三種實現方式
     * 1.雪花演算法,主要用於生成單號
     * 2.日期演算法,用於生成活動標號類,特性是生成數字串較短,但是指定時間內不能生成太多
     * 3.隨機演算法,用於生成策略ID
     * @return ID 返回ID
     */
    long nextId();
}

讓不同生成ID策略實現該介面:

下面是雪花演算法的具體實現 :

public class SnowFlake implements IIdGenerator {

    private Snowflake snowflake;

    @PostConstruct
    public void init() {
        //總共有5位,部署0~32臺機器
        long workerId;
        try {
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        } catch (Exception e) {
            workerId = NetUtil.getLocalhostStr().hashCode();
        }

        workerId = workerId >> 16 & 31;

        long dataCenterId = 1L;
        snowflake = IdUtil.createSnowflake(workerId, dataCenterId);
    }

    @Override
    public long nextId() {
        return snowflake.nextId();
    }
}

其次還要定義一個ID策略控制類IdContext ,通過外部不同的策略,利用統一的方法執行ID策略計算,如下所示:

@Configuration
public class IdContext {

    @Bean
    public Map<Constants.Ids, IIdGenerator> idGenerator(SnowFlake snowFlake, ShortCode shortCode, RandomNumeric randomNumeric) {
        Map<Constants.Ids, IIdGenerator> idGeneratorMap = new HashMap<>(8);
        idGeneratorMap.put(Constants.Ids.SnowFlake, snowFlake);
        idGeneratorMap.put(Constants.Ids.ShortCode, shortCode);
        idGeneratorMap.put(Constants.Ids.RandomNumeric, randomNumeric);
        return idGeneratorMap;
    }
}

所以在最後測試時,直接呼叫idGeneratorMap就可以實現不同策略服務的呼叫:

 @Test
 public void init() {
     logger.info("雪花演算法策略,生成ID: {}", idGeneratorMap.get(Constants.Ids.SnowFlake).nextId());
     logger.info("日期演算法策略,生成ID: {}", idGeneratorMap.get(Constants.Ids.ShortCode).nextId());
     logger.info("隨機演算法策略,生成ID: {}", idGeneratorMap.get(Constants.Ids.RandomNumeric).nextId());
 }

四、三種模式的混合使用

在實際業務開發中,一般是多種設計模式一起混合使用。而工廠模式和策略模式搭配使用就是為了消除if-else的巢狀,下面就結合工廠模式中的案例來介紹一下:

4.1 策略模式+工廠模式

在第一節中的工廠模式中,我們利用工廠實現不同型別的獎品發放,但是在StoreFactory中還是有if-else巢狀的問題:

public class StoreFactory {

    public ICommodity getCommodityService(Integer commodityType) {
        if (null == commodityType) {
            return null;
        }
        if (1 == commodityType) {
            return new CouponCommodityService();
        }
        if (2 == commodityType) {
            return new GoodsCommodityService();
        }
        if (3 == commodityType) {
            return new CardCommodityService();
        }
        throw new RuntimeException("不存在的商品

這個時候可以利用策略模式消除if-else語句:

public class StoreFactory {
    /**設定策略Map**/
    private static Map<Integer, ICommodity> strategyMap = Maps.newHashMap();

    public static ICommodity getCommodityService(Integer commodityType) {
        return strategyMap.get(commodityType);
    }
    /**提前將策略注入 strategyMap **/
    public static void register(Integer commodityType, ICommodity iCommodity) {
        if (0 == commodityType || null == iCommodity) {
            return;
        }
        strategyMap.put(commodityType, iCommodity);
    }
}

在獎品介面中繼承InitializingBean,便於注入策略strategyMap

public interface ICommodity extends InitializingBean {
    
    void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap);
}

然後再具體策略實現上注入對應策略:

@Component
public class GoodsCommodityService implements ICommodity {

    private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class);

    private GoodsService goodsService = new GoodsService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) {
        DeliverReq deliverReq = new DeliverReq();
        deliverReq.setUserName(queryUserName(uId));
        deliverReq.setUserPhone(queryUserPhoneNumber(uId));
        deliverReq.setSku(commodityId);
        deliverReq.setOrderId(bizId);
        deliverReq.setConsigneeUserName(extMap.get("consigneeUserName"));
        deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone"));
        deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress"));
        Boolean isSuccess = goodsService.deliverGoods(deliverReq);
        if (!isSuccess) {
            throw new RuntimeException("實物商品傳送失敗");
        }
    }

    private String queryUserName(String uId) {
        return "ethan";
    }

    private String queryUserPhoneNumber(String uId) {
        return "12312341234";
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        StoreFactory.register(2, this);
    }
}

最後進行測試:

@SpringBootTest
public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void commodity_test() {
        //1.優惠券
        ICommodity commodityService = StoreFactory.getCommodityService(1);
        commodityService.sendCommodity("10001", "sdfsfdsdfsdfs", "1212121212", null);

        //2.實物商品
        ICommodity commodityService1 = StoreFactory.getCommodityService(2);
        Map<String, String> extMap = new HashMap<String, String>();
        extMap.put("consigneeUserName", "ethan");
        extMap.put("consigneeUserPhone", "12312341234");
        extMap.put("consigneeUserAddress", "北京市 海淀區 xxx");
        commodityService1.sendCommodity("10001", "sdfsfdsdfsdfs", "1212121212", extMap);

        //3.第三方兌換卡
        ICommodity commodityService2 = StoreFactory.getCommodityService(3);
        commodityService2.sendCommodity("10001", "SSDIIUIUHJHJ","12312312312",null);

    }
}

4.2 策略模式+工廠模式+模板模式

還是以之前的例子,上面我們已經用策略+工廠模式實現了業務,如何將模板模式也應用其中呢?我們先看看核心的ICommodity介面:

public interface ICommodity extends InitializingBean {

    void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap);
}

在這個介面中,只有一個sendCommodity方法,那麼如果在具體實現策略的類中,需要不同的實現方法,這個時候我們就可以利用模板模式的思路,將介面換成抽象類:

public abstract class AbstractCommodity implements InitializingBean {

    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) {
        //不支援操作異常,繼承的子類可以任意選擇方法進行實現
        throw new UnsupportedOperationException();
    }

    public String templateTest(String str) {
        throw new UnsupportedOperationException();
    }
}

如上,繼承的子類方法可以任意實現具體的策略,以優惠券為例:

@Component
public class CouponCommodityService extends AbstractCommodity {

    private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class);

    private CouponService couponService = new CouponService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) {

        CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId);
        logger.info("請求引數[優惠券] => uId: {} commodityId: {} bizId: {} extMap: {}", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("測試結果[優惠券]:{}", JSON.toJSON(couponResult));
        if (couponResult.getCode() != 0000) {
            throw new RuntimeException(couponResult.getInfo());
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        StoreFactory.register(1, this);
    }
}

這樣的好處在於,子類可以根據需求在抽象類中選擇繼承一些方法,從而實現對應需要的功能。

綜上,在日常業務邏輯中對於設計模式的使用,並不是非得一定要程式碼中有設計模式才行,簡單的邏輯就用if-else即可。如果有複雜的業務邏輯,而且也符合對應的設計模式,這樣使用模式才能真正夠提高程式碼的邏輯性和可擴充套件性。

參考資料

《重學Java設計模式》

《大話設計模式》

相關文章