作者:小傅哥
部落格:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!?
一、前言
好看的程式碼千篇一律,噁心的程式升職加薪。
該說不說幾乎是程式設計師就都知道或者瞭解設計模式,但大部分小夥伴寫程式碼總是習慣於一把梭。無論多少業務邏輯就一個類幾千行,這樣的開發也可以歸納為三步;定義屬性、建立方法、呼叫展示,Done!只不過開發一時爽,重構火葬場。
好的程式碼不只為了完成現有功能,也會考慮後續擴充套件。在結構設計上鬆耦合易讀易擴充套件,在領域實現上高內聚不對外暴漏實現細節不被外部干擾。而這就有點像家裡三居(MVC)室、四居(DDD)室的裝修,你不會允許幾十萬的房子把走線水管裸漏在外面,也不會允許把馬桶放到廚房,爐灶安裝到衛生間。
誰發明了設計模式? 設計模式的概念最早是由 克里斯托佛·亞歷山大
在其著作 《建築模式語言》
中首次提出的。 本書介紹了城市設計的 “語言”,提供了253個描述城鎮、鄰里、住宅、花園、房間及西部構造的模式, 而此類 “語言” 的基本單元就是模式。後來,埃裡希·伽瑪
、 約翰·弗利賽德斯
、 拉爾夫·約翰遜
和 理查德·赫爾姆
這四位作者接受了模式的概念。 1994 年, 他們出版了 《設計模式: 可複用物件導向軟體的基礎》
一書, 將設計模式的概念應用到程式開發領域中。
其實有一部分人並沒有仔細閱讀過設計模式的相關書籍和資料,但依舊可以編寫出優秀的程式碼。這主要是由於在經過眾多專案的錘鍊和對程式設計的不斷追求,從而在多年程式設計歷程上提煉出來的心得體會。而這份經驗最終會與設計模式提到的內容幾乎一致,同樣會要求高內聚、低耦合、可擴充套件、可複用。你可能也遇到類似的經歷,在學習一些框架的原始碼時,發現它裡的某些設計和你在做開發時一樣。
我怎麼學不會設計模式? 錢也花了,書也買了。程式碼還是一坨一坨的!設計模式是由多年的經驗提煉出來開發指導思想。就像我告訴你自行車怎麼騎、汽車怎麼開,但只要你沒跑過幾千公里,你能記住的只是理論,想上道依舊很慌!
所以,本設計模式專題系列開始,會帶著你使用設計模式的思想去優化程式碼。從而學習設計模式的心得並融入給自己。當然這裡還需要多加練習,一定是人車合一,才能站在設計模式的基礎上構建出更加合理的程式碼。
二、開發環境
-
JDK 1.8
-
Idea + Maven
-
涉及工程三個,可以通過關注公眾號:
bugstack蟲洞棧
,回覆原始碼下載
獲取。你會獲得一個連線開啟後的列表中編號18
:itstack-demo-design
工程 描述 itstack-demo-design-1-00 場景模擬工程,用於提供三組不同獎品的發放介面 itstack-demo-design-1-01 使用一坨程式碼實現業務需求,也是對ifelse的使用 itstack-demo-design-1-02 通過設計模式優化改造程式碼,產生對比性從而學習 - 1-00,1 代表著第一個設計模式,工廠方法模式
- 1-00,00 代表模擬的場景
- 1-01,01 代表第一種實現方案,後續 02 03 以此類推
二、工廠方法模式介紹
- 工廠方法模式,圖片來自 refactoringguru.cn
工廠模式又稱工廠方法模式,是一種建立型設計模式,其在父類中提供一個建立物件的方法, 允許子類決定例項化物件的型別。
這種設計模式也是 Java 開發中最常見的一種模式,它的主要意圖是定義一個建立物件的介面,讓其子類自己決定例項化哪一個工廠類,工廠模式使其建立過程延遲到子類進行。
簡單說就是為了提供程式碼結構的擴充套件性,遮蔽每一個功能類中的具體實現邏輯。讓外部可以更加簡單的只是知道呼叫即可,同時,這也是去掉眾多ifeslse
的方式。當然這可能也有一些缺點,比如需要實現的類非常多,如何去維護,怎樣減低開發成本。但這些問題都可以在後續的設計模式結合使用中,逐步降低。
三、模擬發獎多種商品
為了可以讓整個學習的案例更加貼近實際開發,這裡模擬網際網路中在營銷場景下的業務。由於營銷場景的複雜、多變、臨時的特性,它所需要的設計需要更加深入,否則會經常面臨各種緊急CRUD操作,從而讓程式碼結構混亂不堪,難以維護。
在營銷場景中經常會有某個使用者做了一些操作;打卡、分享、留言、邀請註冊等等,進行返利積分,最後通過積分在兌換商品,從而促活和拉新。
那麼在這裡我們模擬積分兌換中的發放多種型別商品,假如現在我們有如下三種型別的商品介面;
序號 | 型別 | 介面 |
---|---|---|
1 | 優惠券 | CouponResult sendCoupon(String uId, String couponNumber, String uuid) |
2 | 實物商品 | Boolean deliverGoods(DeliverReq req) |
3 | 第三方愛奇藝兌換卡 | void grantToken(String bindMobileNumber, String cardId) |
從以上介面來看有如下資訊:
- 三個介面返回型別不同,有物件型別、布林型別、還有一個空型別。
- 入參不同,發放優惠券需要仿重、兌換卡需要卡ID、實物商品需要發貨位置(物件中含有)。
- 另外可能會隨著後續的業務的發展,會新增其他種商品型別。因為你所有的開發需求都是隨著業務對市場的擴充而帶來的。
四、用一坨坨程式碼實現
如果不考慮任何擴充套件性,只為了儘快滿足需求,那麼對這麼幾種獎勵發放只需使用ifelse語句判斷,呼叫不同的介面即可滿足需求。可能這也是一些剛入門程式設計的小夥伴,常用的方式。接下來我們就先按照這樣的方式來實現業務的需求。
1. 工程結構
itstack-demo-design-1-01
└── src
├── main
│ └── java
│ └── org.itstack.demo.design
│ ├── AwardReq.java
│ ├── AwardRes.java
│ └── PrizeController.java
└── test
└── java
└── org.itstack.demo.design.test
└── ApiTest.java
- 工程結構上非常簡單,一個入參物件
AwardReq
、一個出參物件AwardRes
,以及一個介面類PrizeController
2. ifelse實現需求
public class PrizeController {
private Logger logger = LoggerFactory.getLogger(PrizeController.class);
public AwardRes awardToUser(AwardReq req) {
String reqJson = JSON.toJSONString(req);
AwardRes awardRes = null;
try {
logger.info("獎品發放開始{}。req:{}", req.getuId(), reqJson);
// 按照不同型別方法商品[1優惠券、2實物商品、3第三方兌換卡(愛奇藝)]
if (req.getAwardType() == 1) {
CouponService couponService = new CouponService();
CouponResult couponResult = couponService.sendCoupon(req.getuId(), req.getAwardNumber(), req.getBizId());
if ("0000".equals(couponResult.getCode())) {
awardRes = new AwardRes("0000", "發放成功");
} else {
awardRes = new AwardRes("0001", couponResult.getInfo());
}
} else if (req.getAwardType() == 2) {
GoodsService goodsService = new GoodsService();
DeliverReq deliverReq = new DeliverReq();
deliverReq.setUserName(queryUserName(req.getuId()));
deliverReq.setUserPhone(queryUserPhoneNumber(req.getuId()));
deliverReq.setSku(req.getAwardNumber());
deliverReq.setOrderId(req.getBizId());
deliverReq.setConsigneeUserName(req.getExtMap().get("consigneeUserName"));
deliverReq.setConsigneeUserPhone(req.getExtMap().get("consigneeUserPhone"));
deliverReq.setConsigneeUserAddress(req.getExtMap().get("consigneeUserAddress"));
Boolean isSuccess = goodsService.deliverGoods(deliverReq);
if (isSuccess) {
awardRes = new AwardRes("0000", "發放成功");
} else {
awardRes = new AwardRes("0001", "發放失敗");
}
} else if (req.getAwardType() == 3) {
String bindMobileNumber = queryUserPhoneNumber(req.getuId());
IQiYiCardService iQiYiCardService = new IQiYiCardService();
iQiYiCardService.grantToken(bindMobileNumber, req.getAwardNumber());
awardRes = new AwardRes("0000", "發放成功");
}
logger.info("獎品發放完成{}。", req.getuId());
} catch (Exception e) {
logger.error("獎品發放失敗{}。req:{}", req.getuId(), reqJson, e);
awardRes = new AwardRes("0001", e.getMessage());
}
return awardRes;
}
private String queryUserName(String uId) {
return "花花";
}
private String queryUserPhoneNumber(String uId) {
return "15200101232";
}
}
- 如上就是使用
ifelse
非常直接的實現出來業務需求的一坨程式碼,如果僅從業務角度看,研發如期甚至提前實現了功能。 - 那這樣的程式碼目前來看並不會有什麼問題,但如果在經過幾次的迭代和擴充,接手這段程式碼的研發將十分痛苦。重構成本高需要理清之前每一個介面的使用,測試迴歸驗證時間長,需要全部驗證一次。這也就是很多人並不願意接手別人的程式碼,如果接手了又被壓榨開發時間。那麼可想而知這樣的
ifelse
還會繼續增加。
3. 測試驗證
寫一個單元測試來驗證上面編寫的介面方式,養成單元測試的好習慣會為你增強程式碼質量。
編寫測試類:
@Test
public void test_awardToUser() {
PrizeController prizeController = new PrizeController();
System.out.println("\r\n模擬發放優惠券測試\r\n");
// 模擬發放優惠券測試
AwardReq req01 = new AwardReq();
req01.setuId("10001");
req01.setAwardType(1);
req01.setAwardNumber("EGM1023938910232121323432");
req01.setBizId("791098764902132");
AwardRes awardRes01 = prizeController.awardToUser(req01);
logger.info("請求引數:{}", JSON.toJSON(req01));
logger.info("測試結果:{}", JSON.toJSON(awardRes01));
System.out.println("\r\n模擬方法實物商品\r\n");
// 模擬方法實物商品
AwardReq req02 = new AwardReq();
req02.setuId("10001");
req02.setAwardType(2);
req02.setAwardNumber("9820198721311");
req02.setBizId("1023000020112221113");
Map<String,String> extMap = new HashMap<String,String>();
extMap.put("consigneeUserName", "謝飛機");
extMap.put("consigneeUserPhone", "15200292123");
extMap.put("consigneeUserAddress", "吉林省.長春市.雙陽區.XX街道.檀溪苑小區.#18-2109");
req02.setExtMap(extMap);
commodityService_2.sendCommodity("10001","9820198721311","1023000020112221113", extMap);
AwardRes awardRes02 = prizeController.awardToUser(req02);
logger.info("請求引數:{}", JSON.toJSON(req02));
logger.info("測試結果:{}", JSON.toJSON(awardRes02));
System.out.println("\r\n第三方兌換卡(愛奇藝)\r\n");
AwardReq req03 = new AwardReq();
req03.setuId("10001");
req03.setAwardType(3);
req03.setAwardNumber("AQY1xjkUodl8LO975GdfrYUio");
AwardRes awardRes03 = prizeController.awardToUser(req03);
logger.info("請求引數:{}", JSON.toJSON(req03));
logger.info("測試結果:{}", JSON.toJSON(awardRes03));
}
結果:
模擬發放優惠券測試
22:17:55.668 [main] INFO o.i.demo.design.PrizeController - 獎品發放開始10001。req:{"awardNumber":"EGM1023938910232121323432","awardType":1,"bizId":"791098764902132","uId":"10001"}
模擬發放優惠券一張:10001,EGM1023938910232121323432,791098764902132
22:17:55.671 [main] INFO o.i.demo.design.PrizeController - 獎品發放完成10001。
22:17:55.673 [main] INFO org.itstack.demo.test.ApiTest - 請求引數:{"uId":"10001","bizId":"791098764902132","awardNumber":"EGM1023938910232121323432","awardType":1}
22:17:55.674 [main] INFO org.itstack.demo.test.ApiTest - 測試結果:{"code":"0000","info":"發放成功"}
模擬方法實物商品
22:17:55.675 [main] INFO o.i.demo.design.PrizeController - 獎品發放開始10001。req:{"awardNumber":"9820198721311","awardType":2,"bizId":"1023000020112221113","extMap":{"consigneeUserName":"謝飛機","consigneeUserPhone":"15200292123","consigneeUserAddress":"吉林省.長春市.雙陽區.XX街道.檀溪苑小區.#18-2109"},"uId":"10001"}
模擬發貨實物商品一個:{"consigneeUserAddress":"吉林省.長春市.雙陽區.XX街道.檀溪苑小區.#18-2109","consigneeUserName":"謝飛機","consigneeUserPhone":"15200292123","orderId":"1023000020112221113","sku":"9820198721311","userName":"花花","userPhone":"15200101232"}
22:17:55.677 [main] INFO o.i.demo.design.PrizeController - 獎品發放完成10001。
22:17:55.677 [main] INFO org.itstack.demo.test.ApiTest - 請求引數:{"extMap":{"consigneeUserName":"謝飛機","consigneeUserAddress":"吉林省.長春市.雙陽區.XX街道.檀溪苑小區.#18-2109","consigneeUserPhone":"15200292123"},"uId":"10001","bizId":"1023000020112221113","awardNumber":"9820198721311","awardType":2}
22:17:55.677 [main] INFO org.itstack.demo.test.ApiTest - 測試結果:{"code":"0000","info":"發放成功"}
第三方兌換卡(愛奇藝)
22:17:55.678 [main] INFO o.i.demo.design.PrizeController - 獎品發放開始10001。req:{"awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3,"uId":"10001"}
模擬發放愛奇藝會員卡一張:15200101232,AQY1xjkUodl8LO975GdfrYUio
22:17:55.678 [main] INFO o.i.demo.design.PrizeController - 獎品發放完成10001。
22:17:55.678 [main] INFO org.itstack.demo.test.ApiTest - 請求引數:{"uId":"10001","awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3}
22:17:55.678 [main] INFO org.itstack.demo.test.ApiTest - 測試結果:{"code":"0000","info":"發放成功"}
Process finished with exit code 0
- 執行結果正常,滿足當前所有業務產品需求,寫的還很快。但!實在難以為維護!
五、工廠模式優化程式碼
接下來使用工廠方法模式來進行程式碼優化,也算是一次很小的重構。整理重構會你會發現程式碼結構清晰了、也具備了下次新增業務需求的擴充套件性。但在實際使用中還會對此進行完善,目前的只是抽離出最核心的部分體現到你面前,方便學習。
1. 工程結構
itstack-demo-design-1-02
└── src
├── main
│ └── java
│ └── org.itstack.demo.design
│ ├── store
│ │ ├── impl
│ │ │ ├── CardCommodityService.java
│ │ │ ├── CouponCommodityService.java
│ │ │ └── GoodsCommodityService.java
│ │ └── ICommodity.java
│ └── StoreFactory.java
└── test
└── java
└── org.itstack.demo.design.test
└── ApiTest.java
- 首先,從上面的工程結構中你是否一些感覺,比如;它看上去清晰了、這樣分層可以更好擴充套件了、似乎可以想象到每一個類做了什麼。
- 如果還不能理解為什麼這樣修改,也沒有關係。因為你是在通過這樣的文章,來學習設計模式的魅力。並且再獲取原始碼後,進行實際操作幾次也就慢慢掌握了
工廠模式
的技巧。
2. 程式碼實現
2.1 定義發獎介面
public interface ICommodity {
void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception;
}
- 所有的獎品無論是實物、虛擬還是第三方,都需要通過我們的程式實現此介面進行處理,以保證最終入參出參的統一性。
- 介面的入參包括;
使用者ID
、獎品ID
、業務ID
以及擴充套件欄位
用於處理髮放實物商品時的收穫地址。
2.2 實現獎品發放介面
優惠券
public class CouponCommodityService implements ICommodity {
private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class);
private CouponService couponService = new CouponService();
public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
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 (!"0000".equals(couponResult.getCode())) throw new RuntimeException(couponResult.getInfo());
}
}
實物商品
public class GoodsCommodityService implements ICommodity {
private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class);
private GoodsService goodsService = new GoodsService();
public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
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);
logger.info("請求引數[優惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
logger.info("測試結果[優惠券]:{}", isSuccess);
if (!isSuccess) throw new RuntimeException("實物商品發放失敗");
}
private String queryUserName(String uId) {
return "花花";
}
private String queryUserPhoneNumber(String uId) {
return "15200101232";
}
}
第三方兌換卡
public class CardCommodityService implements ICommodity {
private Logger logger = LoggerFactory.getLogger(CardCommodityService.class);
// 模擬注入
private IQiYiCardService iQiYiCardService = new IQiYiCardService();
public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
String mobile = queryUserMobile(uId);
iQiYiCardService.grantToken(mobile, bizId);
logger.info("請求引數[愛奇藝兌換卡] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
logger.info("測試結果[愛奇藝兌換卡]:success");
}
private String queryUserMobile(String uId) {
return "15200101232";
}
}
- 從上面可以看到每一種獎品的實現都包括在自己的類中,新增、修改或者刪除都不會影響其他獎品功能的測試,降低迴歸測試的可能。
- 後續在新增的獎品只需要按照此結構進行填充即可,非常易於維護和擴充套件。
- 在統一了入參以及出參後,呼叫方不在需要關心獎品發放的內部邏輯,按照統一的方式即可處理。
2.3 建立商店工廠
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
判斷,也可以使用switch
或者map
配置結構,會讓程式碼更加乾淨。 - 另外很多程式碼檢查軟體和編碼要求,不喜歡if語句後面不寫擴充套件,這裡是為了更加乾淨的向你體現邏輯。在實際的業務編碼中可以新增括號。
3. 測試驗證
編寫測試類:
@Test
public void test_commodity() throws Exception {
StoreFactory storeFactory = new StoreFactory();
// 1. 優惠券
ICommodity commodityService_1 = storeFactory.getCommodityService(1);
commodityService_1.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);
// 2. 實物商品
ICommodity commodityService_2 = storeFactory.getCommodityService(2);
Map<String,String> extMap = new HashMap<String,String>();
extMap.put("consigneeUserName", "謝飛機");
extMap.put("consigneeUserPhone", "15200292123");
extMap.put("consigneeUserAddress", "吉林省.長春市.雙陽區.XX街道.檀溪苑小區.#18-2109");
commodityService_2.sendCommodity("10001","9820198721311","1023000020112221113", extMap);
// 3. 第三方兌換卡(愛奇藝)
ICommodity commodityService_3 = storeFactory.getCommodityService(3);
commodityService_3.sendCommodity("10001","AQY1xjkUodl8LO975GdfrYUio",null,null);
}
結果:
模擬發放優惠券一張:10001,EGM1023938910232121323432,791098764902132
22:48:10.922 [main] INFO o.i.d.d.s.i.CouponCommodityService - 請求引數[優惠券] => uId:10001 commodityId:EGM1023938910232121323432 bizId:791098764902132 extMap:null
22:48:10.957 [main] INFO o.i.d.d.s.i.CouponCommodityService - 測試結果[優惠券]:{"code":"0000","info":"發放成功"}
模擬發貨實物商品一個:{"consigneeUserAddress":"吉林省.長春市.雙陽區.XX街道.檀溪苑小區.#18-2109","consigneeUserName":"謝飛機","consigneeUserPhone":"15200292123","orderId":"1023000020112221113","sku":"9820198721311","userName":"花花","userPhone":"15200101232"}
22:48:10.962 [main] INFO o.i.d.d.s.impl.GoodsCommodityService - 請求引數[優惠券] => uId:10001 commodityId:9820198721311 bizId:1023000020112221113 extMap:{"consigneeUserName":"謝飛機","consigneeUserAddress":"吉林省.長春市.雙陽區.XX街道.檀溪苑小區.#18-2109","consigneeUserPhone":"15200292123"}
22:48:10.962 [main] INFO o.i.d.d.s.impl.GoodsCommodityService - 測試結果[優惠券]:true
模擬發放愛奇藝會員卡一張:15200101232,null
22:48:10.963 [main] INFO o.i.d.d.s.impl.CardCommodityService - 請求引數[愛奇藝兌換卡] => uId:10001 commodityId:AQY1xjkUodl8LO975GdfrYUio bizId:null extMap:null
22:48:10.963 [main] INFO o.i.d.d.s.impl.CardCommodityService - 測試結果[愛奇藝兌換卡]:success
Process finished with exit code 0
- 執行結果正常,既滿足了業務產品需求,也滿足了自己對程式碼的追求。這樣的程式碼部署上線執行,內心不會恐慌,不會覺得半夜會有電話。
- 另外從執行測試結果上也可以看出來,在進行封裝後可以非常清晰的看到一整套發放獎品服務的完整性,統一了入參、統一了結果。
六、總結
- 從上到下的優化來看,工廠方法模式並不複雜,甚至這樣的開發結構在你有所理解後,會發現更加簡單了。
- 那麼這樣的開發的好處知道後,也可以總結出來它的優點;
避免建立者與具體的產品邏輯耦合
、滿足單一職責,每一個業務邏輯實現都在所屬自己的類中完成
、滿足開閉原則,無需更改使用呼叫方就可以在程式中引入新的產品型別
。但這樣也會帶來一些問題,比如有非常多的獎品型別,那麼實現的子類會極速擴張。因此也需要使用其他的模式進行優化,這些在後續的設計模式中會逐步涉及到。 - 從案例入手看設計模式往往要比看理論學的更加容易,因為案例是縮短理論到上手的最佳方式,如果你已經有所收穫,一定要去嘗試實操。
七、往期推薦
Java開發架構篇:初識領域驅動設計DDD落地
Java開發架構篇:DDD模型領域層決策規則樹服務設計
Java開發架構篇:領域驅動設計架構基於SpringCloud搭建微服務
11 萬字的位元組碼程式設計系列合集放送
八、彩蛋
CodeGuide | 程式設計師編碼指南 Go!
本程式碼庫是作者小傅哥多年從事一線網際網路 Java 開發的學習歷程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心內容。如果本倉庫能為您提供幫助,請給予支援(關注、點贊、分享)!