一、工廠模式
工廠模式又叫做工廠方法模式,是一種創建型設計模式,一般是在父類中提供一個創建對象的方法,允許子類決定實例化對象的類型。
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
判斷。而且整個代碼看起來很長,對於后續迭代和擴展會造成很大的麻煩,因此在考慮設計模式的單一職責原則后,我們可以利用工廠模式對獎品處理返回階段進行抽取,讓每個業務邏輯在自己所屬的類中完成。
首先,我們從業務邏輯中發現無論是那種獎品,都需要發送,因此可以提煉出統一的入參接口和發送方法:ICommodity
、sendCommodity(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 模板模式介紹
定義一個操作的大致框架,然后將具體細節放在子類中實現。也就是通過在抽象類中定義模板方法,讓繼承該子類具體實現模板方法的細節。我們來看看模板模式的UML圖:
AbstractClass
:抽象類,在抽象類中定義了一系列基本操作,這些操作可以是具體的,也可以是抽象的,每一個基本操作對應算法的一個步驟,在其子類中可以重定義或實現這些步驟。同時在抽象類中實現了一個模板方法TemplateMethod()
,用於定義一個算法的框架。ConcreteClass
:具體子類,實現抽象類中聲明的抽象方法,並完成子類特定算法的步驟Client
:客戶端,使用模板方法模式
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算法)等,我們都可以先用策略模式對其進行行為包裝,然后提供給外界進行調用。
注意,如果一個系統中的策略多於四個,就需要考慮使用混合模式,解決策略類膨脹的問題。下面來看看對應的UML結構圖:
Stategy
:抽象策略結構,定義各種不同的算法實現接口,上下文Context
通過這個接口調用不同算法ConcreteStrategy1、ConcreteStrategy2
:實現抽象策略定義的接口,提供具體的算法實現Context
:上下文類,也叫環境類,持有策略類的引用,是外界調用策略的接口
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設計模式》
《大話設計模式》