99精品伊人亚洲|最近国产中文炮友|九草在线视频支援|AV网站大全最新|美女黄片免费观看|国产精品资源视频|精彩无码视频一区|91大神在线后入|伊人终合在线播放|久草综合久久中文

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

軟件設(shè)計(jì)哲學(xué) 于延保代碼改造中的實(shí)踐

京東云 ? 來(lái)源:京東保險(xiǎn) 王奕龍 ? 作者:京東保險(xiǎn) 王奕龍 ? 2024-10-11 14:44 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

作者:京東保險(xiǎn) 王奕龍

本文主要給大家分享軟件設(shè)計(jì)中的兩個(gè)理念,為什么我稱(chēng)軟件設(shè)計(jì)是“理念”而不是“方法”或“原則”呢?這個(gè)想法主要受《A philosophy of software design》的影響,它將軟件設(shè)計(jì)稱(chēng)為“哲學(xué)”,而哲學(xué)本身沒(méi)有嚴(yán)格的定論,同樣地,我覺(jué)得軟件設(shè)計(jì)是每個(gè)開(kāi)發(fā)者的理念,相同功能的迭代,往往會(huì)有不同的看法或思想,也所謂每個(gè)人的代碼風(fēng)格,所以本次分享不求同,只求能給大家?guī)?lái)一點(diǎn)啟發(fā)。兩個(gè)理念如下:

沒(méi)有一蹴而就的設(shè)計(jì):軟件設(shè)計(jì)不會(huì)停止,需要隨著功能迭代(增量開(kāi)發(fā))更新現(xiàn)有設(shè)計(jì),因?yàn)樵诘^(guò)程中相關(guān)開(kāi)發(fā)、業(yè)務(wù)經(jīng)驗(yàn)不斷累積,必然會(huì)產(chǎn)生更好的設(shè)計(jì)方式,最初的設(shè)計(jì)通常不是最好的

堅(jiān)持“深”模塊設(shè)計(jì):“深”這個(gè)觀點(diǎn)來(lái)自 《A philosophy of software design》,它將每個(gè)模塊看作一個(gè)矩形,如下圖所示:

wKgaoWcIyU2ANWoaAACUAeaInlg195.png


矩形的面積代表模塊提供的功能,頂部邊緣代表模塊公開(kāi)出的接口,邊緣長(zhǎng)度代表接口的復(fù)雜性,越長(zhǎng)接口越復(fù)雜。設(shè)計(jì)較好的模塊比較深,因?yàn)樗诤?jiǎn)單的接口后隱藏了許多功能,其內(nèi)部的復(fù)雜性只有一小部分對(duì)開(kāi)發(fā)者可見(jiàn)。堅(jiān)持深模塊設(shè)計(jì)也就意味著提供調(diào)用簡(jiǎn)單但功能強(qiáng)大的接口。

這兩個(gè)理念,我想通過(guò)實(shí)際的業(yè)務(wù)開(kāi)發(fā):延保補(bǔ)購(gòu)功能迭代來(lái)敘述,在開(kāi)始之前我想先給大家簡(jiǎn)單介紹一下什么是延保的補(bǔ)購(gòu):

延保補(bǔ)購(gòu)是購(gòu)買(mǎi)商品時(shí)未購(gòu)買(mǎi)延保,事后再為該商品購(gòu)買(mǎi)延保的行為。比如雙十一購(gòu)買(mǎi)了一部手機(jī),期望能用五年,但是下單時(shí),沒(méi)有一起購(gòu)買(mǎi)延保,之后再去補(bǔ)購(gòu) “五年只換不修” 的延保便是延保的補(bǔ)購(gòu)。

目前在京東商城“延保服務(wù)”頻道頁(yè)能進(jìn)行補(bǔ)購(gòu),大家可以在商城搜索 “延保”點(diǎn)擊“京東延?!碧D(zhuǎn):

wKgZoWcIyU6AKLnXAAQJeK_s_As354.png

業(yè)務(wù)邏輯介紹

為方便大家理解,我將其中的邏輯做了一些簡(jiǎn)化,在查詢(xún)用戶可補(bǔ)購(gòu)的訂單時(shí),它會(huì)執(zhí)行如下邏輯:

/** * AddBuy 作為補(bǔ)購(gòu)的代碼命名定義,定義補(bǔ)購(gòu)相關(guān)門(mén)面接口 */ public class AddBuyFacadeServiceImpl implements AddBuyFacadeService { // 補(bǔ)購(gòu)相關(guān)訂單查詢(xún) Service @Resource private AddBuyOrderQueryService orderQueryService; // 補(bǔ)購(gòu)相關(guān)延保查詢(xún) Service @Resource private AddBuyYbQueryService ybQueryService; // 延保結(jié)果構(gòu)建 Service @Resource private AddBuyBuildService buildService; /** * 查詢(xún)多條可補(bǔ)購(gòu)延保的訂單信息 */ @Override public List queryList(AddBuyListRequest req) { // 1. 查詢(xún)主商品訂單 OrderInfoRequest orderInfoRequest = new OrderInfoRequest(); orderInfoRequest.setUserNo(req.getUserNo()); // ... List orderInfoList = orderQueryService.listOrderInfo(orderInfoRequest); // 2. 查詢(xún)這些訂單可購(gòu)買(mǎi)的延保信息 YbInfoRequest ybInfoRequest = new YbInfoRequest(); ybInfoRequest.setUserNo(req.getUserNo()); // ... // key: orderNo value: ybInfoList Map> orderNoYbListMap = ybQueryService.listYbInfo(ybInfoRequest); // 3. 封裝訂單和推薦延保信息 return buildService.buildRecommendInfo(orderInfoList, orderNoYbListMap); } }

執(zhí)行步驟如下:

先查詢(xún)主商品訂單

查詢(xún)這些訂單可購(gòu)買(mǎi)的延保信息

封裝訂單和推薦延保信息

補(bǔ)購(gòu)功能會(huì)被推廣到很多不同的 渠道 使用,渠道指的是補(bǔ)購(gòu)功能推廣的范圍,包括微信(WEI_XIN)和PLUS客服推廣(PLUS)等等:

wKgZomcIyU-AOZ02AAN1qjbq8GY502.png

這個(gè)渠道值會(huì)在查詢(xún)做標(biāo)記,如下所示為補(bǔ)購(gòu)查詢(xún)請(qǐng)求對(duì)象 AddBuyListRequest:

public class AddBuyListRequest { private String userNo; // 渠道值信息 private String channel; // ... }

功能迭代

初期功能并沒(méi)有針對(duì)渠道參數(shù)做校驗(yàn)和管理,也就是說(shuō)前端傳什么后端就接什么,導(dǎo)致出現(xiàn)了異常的XXX未知渠道。為了對(duì)渠道進(jìn)行管控,并根據(jù)現(xiàn)有渠道做個(gè)性化補(bǔ)購(gòu),現(xiàn)在便需要在此基礎(chǔ)上迭代“渠道管控”的功能:

渠道值校驗(yàn):校驗(yàn)未知的渠道值,對(duì)現(xiàn)有渠道進(jìn)行管理

個(gè)性化補(bǔ)購(gòu):指定渠道查詢(xún) 固定品類(lèi) 的延保等定制化邏輯,比如規(guī)定渠道 “PLUS” 渠道只能查詢(xún) “手機(jī)” 品類(lèi)的延保

為滿足功能,首先定義 ChannelConfig 渠道配置類(lèi),其中包含如下字段:

public class ChannelConfig { /** * 渠道 */ private String channel; /** * 要查詢(xún)的主品一級(jí)類(lèi)目編碼 */ private List mainFirstCategoryCodeList; // ... }

初版設(shè)計(jì)

初版設(shè)計(jì)時(shí),定義 channelConfigMap 以 key: channel value: config 的形式維護(hù)所有渠道值,在方法 queryList 執(zhí)行時(shí),會(huì)先校驗(yàn)渠道的存在,存在的話繼續(xù)執(zhí)行邏輯,并把 config 對(duì)象 透?jìng)?到各個(gè)查詢(xún)方法中,以執(zhí)行篩選指定品類(lèi)等定制化邏輯,如下:

wKgaomcIyU-AcYdzAAHU55xpMWc998.png

在方法 queryList 中,可以發(fā)現(xiàn)渠道配置幾乎 貫穿了接口邏輯執(zhí)行的始終,雖然對(duì)該方法的改動(dòng)并不大,但對(duì)于其中的查詢(xún)方法 listOrderInfo 和 listYbInfo 來(lái)說(shuō),會(huì)使它們的方法復(fù)用變得困難。

如果想復(fù)用其中查詢(xún)訂單的接口(其中第 1 步邏輯):

List listOrderInfo(OrderInfoRequest orderInfoRequest, ChannelConfig config);

在構(gòu)建查詢(xún)?nèi)雲(yún)⒌臅r(shí)候就難免不對(duì)渠道配置 ChannelConfig config 參數(shù)產(chǎn)生疑問(wèn):“這個(gè)入?yún)⒌淖饔檬鞘裁础??點(diǎn)進(jìn)去看它的字段:如果發(fā)現(xiàn)有很多字段,那么開(kāi)發(fā)者大概率不會(huì)去了解每個(gè)字段的含義,并且也沒(méi)辦法記住這么多字段的作用是什么,這就會(huì)導(dǎo)致開(kāi)發(fā)者直接去看 listOrderInfo 方法的實(shí)現(xiàn),分析其中哪些字段是有用的,哪些是沒(méi)用的。這使得一件本可以簡(jiǎn)單查看方法入?yún)?OrderInfoRequest 便能復(fù)用該方法的事情變得異常復(fù)雜,需要了解太多查詢(xún)訂單信息之外的知識(shí),大大增加了認(rèn)知負(fù)荷。

從本質(zhì)上去考慮:“在調(diào)用查詢(xún)訂單接口時(shí),我們需要了解‘渠道配置’相關(guān)的內(nèi)容嗎?”,顯然是不需要的,否則為通用查詢(xún)接口帶來(lái)的復(fù)雜性就太高了,進(jìn)而使得接口變“淺”。

那么該如何避免這些問(wèn)題呢?最初我想到兩種簡(jiǎn)單的辦法:

詳細(xì)的配置注釋?zhuān)簩⑴渲玫拿總€(gè)字段描述的足夠清楚,那么使用該接口的研發(fā)人員不需要去了解代碼實(shí)現(xiàn)便能知道根據(jù)渠道如何添加一個(gè)合適的配置入?yún)?/p>

查詢(xún)方法實(shí)現(xiàn)中添加配置默認(rèn)值兜底:解決調(diào)用者不傳該參數(shù)或者某些字段為空的特殊情況,降低復(fù)用難度
但是這樣并不解決根本問(wèn)題,入?yún)⒅邪琅渲靡廊粫?huì)暴露其帶來(lái)的復(fù)雜性。如果開(kāi)發(fā)者要復(fù)用該 listOrderInfo 方法并篩選特定品類(lèi)的訂單,那么需要寫(xiě)如下邏輯:

/** * 復(fù)用 listOrderInfo 方法樣例 */ public void reuseExample(Request req) { OrderInfoRequest orderInfoRequest = new OrderInfoRequest(); orderInfoRequest.setUserNo(req.getUserNo()); ChannelConfig config = new ChannelConfig(); // 賦值指定的品類(lèi)編碼 config.setMainFirstCategoryCodeList(Collections.singleton("1234")); List orderInfoList = orderQueryService.listOrderInfo(orderInfoRequest, config); // ... }

一旦篩選條件中涉及渠道配置中的字段,都要?jiǎng)?chuàng)建一個(gè)渠道配置 ChannelConfig 對(duì)象,都要去了解這個(gè)類(lèi)中定義了哪些字段。

這時(shí)候我就在想,那么為何不將渠道配置 ChannelConfig 中的字段都提出來(lái)放到另一個(gè)參數(shù) OrderInfoRequest 中呢?那么這樣在入?yún)⒅斜隳懿辉賯魅肭琅渲昧?,如下所示?/p>

wKgZomcIyVCADnctAAHf-7dCyPQ526.png

這樣當(dāng)開(kāi)發(fā)者復(fù)用 listOrderInfo 方法時(shí),只需關(guān)注 OrderInfoRequest 對(duì)象并為相關(guān)字段賦值即可,這樣通用的訂單查詢(xún)接口便無(wú)需再關(guān)注渠道配置相關(guān)的內(nèi)容了。

但是到這里還沒(méi)完,有一點(diǎn)值得考慮:渠道配置信息 ChannelConfig 作為 不可變的對(duì)象,并不應(yīng)該被公開(kāi)出來(lái),而且在一般情況下,開(kāi)發(fā)者會(huì)習(xí)慣使用 Lombok 的注解 @Data 為類(lèi)做標(biāo)注,如下:

@Data public class ChannelConfig { // ... }

這樣它每個(gè)字段的寫(xiě)(set)操作都是公開(kāi)(public)的,一旦對(duì)原本不可變的數(shù)據(jù)進(jìn)行修改,那么因此產(chǎn)生的問(wèn)題將非常難排查。相對(duì)應(yīng)地,在《重構(gòu)》一書(shū)中,提到過(guò)類(lèi)似觀點(diǎn):“對(duì)于所有可變的數(shù)據(jù),只要它的作用域超出單個(gè)方法,我就會(huì)將其封裝起來(lái),只允許通過(guò)方法訪問(wèn),數(shù)據(jù)的作用域越大,封裝就越重要,因?yàn)檫@樣能夠很清楚的知道哪些地方讀了這些數(shù)據(jù)或?qū)懥诉@些數(shù)據(jù),如果我們想避免其他開(kāi)發(fā)者修改這個(gè)對(duì)象的話,那么就可以不公開(kāi)出 set 方法”。

對(duì) @Data 的觀點(diǎn):@Data 注解實(shí)際上有些被濫用,在面向?qū)ο蟮拈_(kāi)發(fā)中,通常我們都會(huì)把類(lèi)內(nèi)字段聲明為 private,但是又在類(lèi)上標(biāo)記 @Data 注解,為每個(gè)字段生成 Getter 和 Setter 方法,使得 private 失效。雖然多數(shù)時(shí)候并不會(huì)引發(fā)問(wèn)題,但是更好的做法應(yīng)該是針對(duì)字段指定 Getter 和 Setter 方法,而不是泛泛的生成全部,特別是如果要定義某些不可變的字段時(shí),要尤為注意。此外,當(dāng)整個(gè)對(duì)象都不可變時(shí),每次獲取該對(duì)象時(shí)返回它的深拷貝也是很有必要的,否則其被修改后,引發(fā)的線上問(wèn)題非常難定位和排查,這個(gè)對(duì)象被使用的越多,則越需要警惕。

所以,我們需要將渠道配置對(duì)象隱藏起來(lái)。

信息隱藏

為了不暴露 ChannelConfig 對(duì)象,定義渠道配置服務(wù) ChannelConfigService,并提供校驗(yàn)渠道的方法,如下:

public interface ChannelConfigService { /** * 校驗(yàn)渠道是否存在,否則拋出異常 * * @throws ChannelNotExistException 渠道不存在異常 */ void checkChannelExist(String channel) throws ChannelNotExistException; // ... } @Service public class ChannelConfigServiceImpl implements ChannelConfigService { /** * 渠道配置 key: channel value: config */ private HashMap channelConfigMap; @Override public void checkChannelExist(String channel) throws ChannelNotExistException { if (StringUtils.isBlank(channel) || !channelConfigMap.containsKey(channel)) { throw new ChannelNotExistException("渠道: " + req.getChannel() + " 不存在"); } } }

這樣將保存渠道配置的 Map channelConfigMap 就下沉到了 ChannelConfigServiceImpl 實(shí)現(xiàn)中,queryList 方法中將不再暴露 ChannelConfig 對(duì)象,校驗(yàn)渠道值邊以方法的形式,如下:

wKgaomcIyVGARQrXAAGyiA5rE-I422.png

同樣地,封裝品類(lèi)查詢(xún)條件等與配置相關(guān)的邏輯也需要被隱藏起來(lái),因?yàn)橐呀?jīng)沒(méi)有渠道配置 config對(duì)象了,那么這部分該如何處理呢?

我是這樣做的:在渠道配置服務(wù)中 ChannelConfigService 定義獲取品類(lèi)配置的方法 getMainFirstCategoryCodeList,這樣便無(wú)需公開(kāi)渠道配置對(duì)象了:

public interface ChannelConfigService { /** * 獲取主品類(lèi)目編碼配置,默認(rèn)值為空列表對(duì)象 */ List getMainFirstCategoryCodeList(String channel); // ... } @Service public class ChannelConfigServiceImpl implements ChannelConfigService { /** * 渠道配置 key: channel value: config */ private HashMap channelConfigMap; // ... @Override public List getMainFirstCategoryCodeList(String channel) { ChannelConfig config = channelConfigMap.get(channel); // 默認(rèn)返回空 List if (config == null || CollectionUtils.isEmpty(config.getMainFirstCategoryCodeList())) { return Collections.emptyList(); } return Collections.unmodifiableList(config.getMainFirstCategoryCodeList()); } }

開(kāi)發(fā)者只需根據(jù)接口公開(kāi)出的方法來(lái)獲取相應(yīng)的配置信息即可,并不需要對(duì)渠道配置對(duì)象 ChannelConfig 做了解,改動(dòng)如下:

wKgZomcIyVKAZx4RAAGwDrAJ-Os377.png

做過(guò)頭了

還有一種方式是將渠道配置服務(wù) ChannelConfigService 注入到訂單查詢(xún)服務(wù) AddBuyOrderQueryService 中,并添加渠道配置相關(guān)的處理邏輯:

@Service public class AddBuyOrderQueryServiceImpl implements AddBuyOrderQueryService { @Resource private ChannelConfigService configService; @Override public List listOrderInfo(OrderInfoRequest orderInfoRequest) { // ... // 過(guò)濾一級(jí)品類(lèi),如果沒(méi)有指定則取渠道配置的一級(jí)品類(lèi)配置 List firstCategoryCodeList; if (CollectionUtils.isNotEmpty(orderInfoRequest.getMainFirstCategoryCodeList())) { firstCategoryCodeList = orderInfoRequest.getMainFirstCategoryCodeList(); } else { firstCategoryCodeList = configService.getMainFirstCategoryCodeList(orderInfoRequest.getChannel()); } processMainFistCategory(firstCategoryCodeList); // ... } }

雖然這樣做將渠道配置相關(guān)信息隱藏得更深,幾乎不暴露到補(bǔ)購(gòu)查詢(xún) queryList 邏輯中,如下:

wKgaomcIyVOAO2b8AAKZ07eqRp4095.png

但是有一個(gè)問(wèn)題需要考慮:訂單查詢(xún)接口屬于通用查詢(xún)接口,將渠道配置服務(wù) ChannelConfigService 下沉到其中,便使訂單和渠道的知識(shí)發(fā)生耦合,并且在邏輯中存在依賴(lài),渠道配置的改動(dòng)可能會(huì)影響通用的訂單查詢(xún)。這樣做,可能就有些過(guò)頭了。

擴(kuò)展性設(shè)計(jì)對(duì)復(fù)雜度的管理

隨著業(yè)務(wù)發(fā)展,有新的服務(wù)方提供延保的查詢(xún)服務(wù)(對(duì)應(yīng)代碼中步驟 2),這些服務(wù)需要接入現(xiàn)有補(bǔ)購(gòu)邏輯中,并根據(jù)渠道的不同,查詢(xún)不同的延保服務(wù)。

wKgZomcIyVWAYLcsAAFu21u9HGk906.png

這是非常典型的策略模式應(yīng)用場(chǎng)景,原有延保查詢(xún)服務(wù)和新增的延保查詢(xún)服務(wù)都將作為不同的策略來(lái)實(shí)現(xiàn)。借助策略模式實(shí)現(xiàn)擴(kuò)展性并不困難,常見(jiàn)的有兩種實(shí)現(xiàn)方法,但是它們對(duì)策略帶來(lái)的復(fù)雜性處理是不同的:

第一種保持延保查詢(xún)服務(wù) AddBuyYbQueryService 公開(kāi)的方法不變,使用靜態(tài)代理模式在其實(shí)現(xiàn)中 AddBuyYbQueryServiceImpl 借助 HashMap 保存所有策略,并根據(jù)渠道的不同執(zhí)行不同的策略:

public interface AddBuyYbQueryService { Map> listYbInfo(YbInfoRequest ybInfoRequest); } /** * 實(shí)現(xiàn) ApplicationContextAware 用于注入 ApplicationContext 獲取想要的策略(Bean) * 實(shí)現(xiàn) InitializingBean 用于在應(yīng)用啟動(dòng)時(shí),根據(jù)策略類(lèi)型 AddBuyYbQueryStrategy 加載所有的策略 */ @Service public class AddBuyYbQueryServiceImpl implements AddBuyYbQueryService, ApplicationContextAware, InitializingBean { private ApplicationContext applicationContext; private HashMap nameServiceMap; // 渠道配置 Service @Resource private ChannelConfigService configService; @Override public Map> listYbInfo(YbInfoRequest ybInfoRequest) { String ybType = configService.getYbType(ybInfoRequest.getChannel()); // 通過(guò)定義枚舉實(shí)現(xiàn) ybType 與具體策略實(shí)現(xiàn)的關(guān)聯(lián) YbQueryStrategyEnum strategyEnum = YbQueryStrategyEnum.parseByType(ybType); AddBuyYbQueryStrategy ybQueryStrategy = nameServiceMap.get(strategyEnum.getQueryServiceName()); return ybQueryStrategy.listYbInfo(ybInfoRequest); } @Override public void afterPropertiesSet() { nameServiceMap = new HashMap(); nameServiceMap.putAll(applicationContext.getBeansOfType(AddBuyYbQueryStrategy.class)); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }

策略 AddBuyYbQueryStrategy 接口方法簽名與延保查詢(xún)服務(wù) AddBuyYbQueryService 中方法簽名一致,它們的類(lèi)關(guān)系圖如下所示:

wKgaomcIyVaAdoKeAAgUQYFMZsA706.png

延保查詢(xún)服務(wù) AddBuyYbQueryService 的 listYbInfo 方法不變,那么 原方法 queryList 邏輯也不需要改變,這樣便將策略帶來(lái)的復(fù)雜度隱藏了起來(lái)。

第二種是創(chuàng)建策略上下文 AddBuyYbQueryStrategyContext,將對(duì)應(yīng)的策略管理起來(lái)并通過(guò)調(diào)用 getSpecificStrategy 直接暴露具體的策略,如下:

public interface AddBuyYbQueryStrategyContext { AddBuyYbQueryStrategy getSpecificStrategy(YbInfoRequest ybInfoRequest); }

那么這樣對(duì)原方法的改動(dòng)如下:

wKgZomcIyVeAR8gMAAHfCJ9X3Rc799.png

這樣會(huì)將使用策略模式的復(fù)雜度暴露到原方法 queryList 中,實(shí)際上開(kāi)發(fā)者在這個(gè)方法中不需要了解策略該如何分配等相關(guān)邏輯。

封裝推薦延保信息的邏輯(其中第 3 步)由于引入了不同類(lèi)型的延保服務(wù),也需要根據(jù)不同的延保對(duì)象類(lèi)型適配相應(yīng)的策略,同樣需要使用策略模式,但實(shí)現(xiàn)邏輯類(lèi)似,不再贅述,不過(guò)不同點(diǎn)是該步驟一并使用了模板方法模式:

封裝訂單和推薦延保信息的 buildRecommendInfo 方法主要步驟為封裝訂單信息(initialMainOrderInfo)和封裝推薦的延保信息(initialRecommendYbInfo),其中封裝訂單信息的邏輯是通用的,而封裝推薦的延保信息步驟需要分別處理不同的延保類(lèi)型,所以借助了模板方法模式。將后者其定義為抽象方法,由子類(lèi)不同的策略去分別實(shí)現(xiàn),抽象模板類(lèi)如下:

public abstract class AbstractAddBuyBuildService implements AddBuyBuildStrategy { @Override public List buildRecommendInfo(List orderInfoList, Map> orderNoYbListMap) { List res = new ArrayList(orderNoYbListMap.size()); // Group By OrderNo Map orderNoOrderInfoMap = orderInfoList.stream().collect(Collectors.toMap(OrderInfo::getOrderid, x -> x)); // orderNoBindListEntry: (key: 訂單號(hào);value: 訂單對(duì)應(yīng)的延保信息) for (Map.Entry> orderNoYbListEntry : orderNoYbListMap.entrySet()) { // 訂單信息 OrderInfo orderInfo = orderNoOrderInfoMap.get(orderNoYbListEntry.getKey()); // 延保信息 List ybList = orderNoYbListEntry.getValue(); AddBuyResult element = new AddBuyResult(); // 1. 封裝訂單信息 MainOrderInfo mainOrderInfo = initialMainOrderInfo(orderInfo); element.setMainOrderInfo(mainOrderInfo); // 2. 封裝推薦的延保信息 List recommendYbInfo = initialRecommendYbInfo(ybList); element.setRecommendYbInfoList(recommendYbInfo); res.add(element); } return res; } // 封裝訂單信息作為私有方法,被各個(gè)不同的策略復(fù)用 private MainOrderInfo initialMainOrderInfo(OrderInfo orderInfo) { // ... } // 抽象初始化推薦延保的方法,用于子類(lèi)實(shí)現(xiàn)不同的策略 protected abstract List initialRecommendYbInfo(List ybList); }

類(lèi)關(guān)系圖如下:

wKgaomcIyVqAF2u4AA0T4Qha9xA310.png

這樣便能實(shí)現(xiàn)通用邏輯的復(fù)用。使用模板方法模式并不復(fù)雜,實(shí)現(xiàn)這種模式需要借助繼承(extends),而繼承在設(shè)計(jì)原則中被強(qiáng)調(diào)“少用繼承,多用組合”,而且在一些軟件設(shè)計(jì)相關(guān)的書(shū)中也會(huì)經(jīng)??吹綄?duì)繼承的詬病,比如在《程序員修煉之道》中便將其稱(chēng)為“繼承稅”,并且舉了一個(gè)非常好玩的例子:

你想要一根香蕉,但得到的卻是一只拿著香蕉的大猩猩,甚至還有整個(gè)森林

其表達(dá)的意思也不難理解:強(qiáng)調(diào)繼承使父類(lèi)中的大量信息發(fā)生泄露,讓維護(hù)在每個(gè)類(lèi)中的知識(shí)在繼承關(guān)系之間 “波動(dòng)”,暴露了太多的知識(shí)出來(lái),做不到抽象和信息隱藏。一方面會(huì)使子類(lèi)獲得太多無(wú)關(guān)的知識(shí),另一方面如果在子類(lèi)中大量使用這些通用的部分,便會(huì)使得耦合加深,父類(lèi)中信息變更可能為子類(lèi)帶來(lái)意想不到的后果。

wKgZoWcIyV6Acqx3AAPXC4ODFhg219.png

以如上繼承關(guān)系為例,如果父類(lèi)中某些內(nèi)容發(fā)生變更,子類(lèi)中對(duì)其使用的話,那么可能會(huì)引起子類(lèi)行為的改變,而如果這種改變并不引起編譯期異常的話,便很難發(fā)現(xiàn),使得代碼的可維護(hù)性大大降低。那么不用繼承該怎么辦呢?常見(jiàn)的觀點(diǎn)有兩個(gè):

使用接口實(shí)現(xiàn)來(lái)代替類(lèi)的繼承,保證多態(tài)又不會(huì)造成信息的緊耦合

使用組合代替繼承:比如想要香蕉,那么直接將包含香蕉的類(lèi)注入進(jìn)來(lái),不再通過(guò)繼承去獲取了

但是,我覺(jué)得繼承也并不能被一票否決,在 Java 源碼中常用容器的實(shí)現(xiàn)里,都是有抽象層的(AbstractList, AbstractMap 等等),通過(guò)繼承它們,實(shí)現(xiàn)了大量代碼復(fù)用,為各種不同容器的實(shí)現(xiàn)提供了很多方便之處。所以,我覺(jué)得繼承能被應(yīng)用需要具備以下前提條件:

保持不變性:父類(lèi)中抽象出來(lái)的供復(fù)用的通用方法、字段保持不變

控制繼承樹(shù)的高度:繼承樹(shù)高度越高引入的復(fù)雜度越大,所以需要控制樹(shù)高,限制一層繼承關(guān)系,那么復(fù)雜度便可控

總結(jié)

我覺(jué)得軟件設(shè)計(jì)更應(yīng)該站在代碼閱讀者的角度上,考慮如何降低復(fù)雜度,設(shè)計(jì)更深的模塊,并隨著功能迭代,不斷更新現(xiàn)有設(shè)計(jì),而并不是將注意力放在如何改動(dòng)更簡(jiǎn)單上,代碼的堆疊可能會(huì)導(dǎo)致復(fù)雜性不斷累積,以至于在不能滿足業(yè)務(wù)功能迭代時(shí),花更多的時(shí)間去重構(gòu)。

審核編輯 黃宇

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 軟件設(shè)計(jì)
    +關(guān)注

    關(guān)注

    3

    文章

    63

    瀏覽量

    18072
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4900

    瀏覽量

    70768
收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    普華基礎(chǔ)軟件入選2025國(guó)汽車(chē)行業(yè)可持續(xù)發(fā)展實(shí)踐案例

    近日,由中國(guó)汽車(chē)工業(yè)協(xié)會(huì)主辦的2025國(guó)汽車(chē)論壇在上海召開(kāi),會(huì)議同期發(fā)布了“2025國(guó)汽車(chē)行業(yè)可持續(xù)發(fā)展實(shí)踐案例”評(píng)選結(jié)果。普華基礎(chǔ)軟件“開(kāi)源安全車(chē)控操作系統(tǒng)小滿EasyXMen”
    的頭像 發(fā)表于 07-17 17:47 ?392次閱讀

    ArkUI-X跨平臺(tái)應(yīng)用改造指南

    復(fù)用的一套代碼)是要遠(yuǎn)遠(yuǎn)多于差異性代碼(A包和B包需各自進(jìn)行設(shè)計(jì),無(wú)法復(fù)用)的。開(kāi)發(fā)者在后續(xù)的開(kāi)發(fā)和維護(hù)過(guò)程,每次都需要同時(shí)修改分別部署A包和B包的共性
    發(fā)表于 06-16 23:05

    鴻蒙5開(kāi)發(fā)寶藏案例分享---點(diǎn)擊完成時(shí)分析

    DevEco Studio運(yùn)行性能檢測(cè) ./gradlew appanalyzer --test-type performance 一鍵檢測(cè)完成時(shí)是否達(dá)標(biāo) 生成詳細(xì)診斷報(bào)告 支持兼容性/UX/最佳實(shí)踐
    發(fā)表于 06-12 17:03

    超低功耗MCU軟件設(shè)計(jì)技巧與選型

    與開(kāi)發(fā)應(yīng)用生態(tài)的沉淀,大力節(jié)省超低功耗選型設(shè)計(jì)成本以及開(kāi)發(fā)周期.如何做好超低功耗mcu嵌入式軟件設(shè)計(jì)與選型是本文講述的主要內(nèi)容.
    的頭像 發(fā)表于 04-12 17:19 ?614次閱讀
    超低功耗MCU<b class='flag-5'>軟件設(shè)計(jì)</b>技巧與選型

    嵌入式軟件開(kāi)發(fā)遺留代碼的挑戰(zhàn)

    遺留代碼通常難以集成到其他系統(tǒng)、適配新數(shù)據(jù)格式或部署到現(xiàn)代平臺(tái)及云端托管環(huán)境。相關(guān)代碼可能已不再提供安全更新和補(bǔ)丁,供應(yīng)商或開(kāi)源社區(qū)的支持也可能逐漸減少甚至消失。然而,如果使用遺留代碼不可避免,本文給出一些最佳
    的頭像 發(fā)表于 02-26 10:05 ?414次閱讀
    嵌入式<b class='flag-5'>軟件</b>開(kāi)發(fā)<b class='flag-5'>中</b>遺留<b class='flag-5'>代碼</b>的挑戰(zhàn)

    Altium Designer15.0軟件設(shè)計(jì)方法和安裝

    電子發(fā)燒友網(wǎng)站提供《Altium Designer15.0軟件設(shè)計(jì)方法和安裝.pdf》資料免費(fèi)下載
    發(fā)表于 01-22 17:22 ?0次下載
    Altium Designer15.0<b class='flag-5'>軟件設(shè)計(jì)</b>方法和安裝

    用FilterPro Desktop軟件設(shè)計(jì)高通電路如何設(shè)置Q值?

    請(qǐng)問(wèn)用FilterPro Desktop軟件設(shè)計(jì)高通電路如何設(shè)置Q值?
    發(fā)表于 10-23 08:28

    軟件干貨】Android應(yīng)用進(jìn)程如何活?

    在Android應(yīng)用程序,為了保證應(yīng)用的正常運(yùn)行和穩(wěn)定性,有時(shí)需要對(duì)應(yīng)用進(jìn)程進(jìn)行活。以下是一些實(shí)現(xiàn)進(jìn)程活的方法:
    的頭像 發(fā)表于 10-15 17:05 ?1221次閱讀
    【<b class='flag-5'>軟件</b>干貨】Android應(yīng)用進(jìn)程如何<b class='flag-5'>保</b>活?

    基于FPA的軟件工作量綜合評(píng)估研究與實(shí)踐

    方法基于軟件系統(tǒng)工作量估算法的融合,基于FPA評(píng)估法與專(zhuān)家經(jīng)驗(yàn)估算相結(jié)合,綜合評(píng)估軟件系統(tǒng)工作量,并將該方法實(shí)踐多個(gè)項(xiàng)目中,結(jié)果得到了用戶的認(rèn)可,表明了綜合評(píng)估方法的有效性和可操作性
    發(fā)表于 10-15 10:45 ?0次下載

    BQ79606A-Q1軟件設(shè)計(jì)參考

    電子發(fā)燒友網(wǎng)站提供《BQ79606A-Q1軟件設(shè)計(jì)參考.pdf》資料免費(fèi)下載
    發(fā)表于 09-29 10:07 ?1次下載
    BQ79606A-Q1<b class='flag-5'>軟件設(shè)計(jì)</b>參考

    BQ79616-Q1軟件設(shè)計(jì)參考

    電子發(fā)燒友網(wǎng)站提供《BQ79616-Q1軟件設(shè)計(jì)參考.pdf》資料免費(fèi)下載
    發(fā)表于 09-03 10:37 ?5次下載
    BQ79616-Q1<b class='flag-5'>軟件設(shè)計(jì)</b>參考

    BQ79600-Q1軟件設(shè)計(jì)參考

    電子發(fā)燒友網(wǎng)站提供《BQ79600-Q1軟件設(shè)計(jì)參考.pdf》資料免費(fèi)下載
    發(fā)表于 08-30 10:09 ?0次下載
    BQ79600-Q1<b class='flag-5'>軟件設(shè)計(jì)</b>參考

    「重構(gòu):改善既有代碼的設(shè)計(jì)」實(shí)戰(zhàn)篇

    代碼層面的整理,它更是一種軟件開(kāi)發(fā)的哲學(xué),強(qiáng)調(diào)持續(xù)改進(jìn)和適應(yīng)變化的重要性。 ? ? 書(shū)中通過(guò)詳細(xì)的案例分析和代碼示例,將理論與實(shí)踐巧妙地融合
    的頭像 發(fā)表于 08-14 10:42 ?611次閱讀
    「重構(gòu):改善既有<b class='flag-5'>代碼</b>的設(shè)計(jì)」實(shí)戰(zhàn)篇

    TLV3201電流檢測(cè)電路的時(shí)應(yīng)該怎么算?

    TLV3201請(qǐng)教一下大佬。這種電流檢測(cè)電路的時(shí)應(yīng)該怎么算?這里的時(shí)指的是從輸入電流發(fā)生變化到比較器輸出反饋的時(shí)間。 我的理解是放大器的建立時(shí)間+比較器的傳輸時(shí)嘛?比如按照以下放大器和比較器手冊(cè)
    發(fā)表于 07-31 07:19

    軟件設(shè)計(jì)哲學(xué):新“代碼整潔之道”

    工作三年以來(lái)一直對(duì)寫(xiě)出設(shè)計(jì)優(yōu)雅且可讀性較好的代碼抱有執(zhí)念,最初接觸到的關(guān)于代碼整潔和軟件設(shè)計(jì)的書(shū)是《代碼整潔之道》,這本書(shū)大概在我入職半年時(shí)讀完,并在很長(zhǎng)的一段時(shí)間內(nèi)將其中談到的“每個(gè)
    的頭像 發(fā)表于 07-22 12:18 ?492次閱讀
    <b class='flag-5'>軟件設(shè)計(jì)</b><b class='flag-5'>哲學(xué)</b>:新“<b class='flag-5'>代碼</b>整潔之道”