阿里妹導讀
本文記錄了作者從“代碼優(yōu)化”到“過度設(shè)計”的典型思考過程,這過程中涉及了很多Java的語法糖及設(shè)計模式的東西,很典型,能啟發(fā)思考,遂記錄下來。
有一天Review師妹的代碼,看到一行很難看的代碼,畢竟師妹剛開始轉(zhuǎn)JAVA,一些書寫小習慣還是要養(yǎng)成,所以錙銖必較還是有必要的,于是給出了一些優(yōu)化思路的建議,以及為什么要這么做。建議完后,我并沒有停下”追求極致“的腳步,隨著不斷的思考,發(fā)現(xiàn)這段代碼的優(yōu)化慢慢變得五花八門起來了,完成了一次“代碼優(yōu)化”到“過度設(shè)計”的典型思考過程,這過程中涉及了很多Java的語法糖及設(shè)計模式的東西,很典型,能啟發(fā)思考,遂記錄下來。
一切的開始
起初是一段很簡單的代碼,開始僅僅是將外域的一些標識符轉(zhuǎn)換為域內(nèi)的標識符。
public Integer parseSaleType(String saleTypeStr){ if(saleTypeStr == null || saleTypeStr.equals("")){ return null; } if(saleTypeStr.equals("JX")){ return 1; } return null; }邏輯上很簡單,實現(xiàn)的邏輯看上去也沒啥大問題,基本學校的老師也是這么教的。
語法規(guī)范
但是嘛,不好看也容易犯錯誤,雞蛋里挑骨頭也得挑,于是給出了幾個寫代碼的建議:
有函數(shù)式方法的盡量用
//saleTypeStr == null Objects.isNull(saleTypeStr)首先呢,雖然由于判斷null這么寫不會報錯,但是按照常量寫==前面的要求,應該倒過來寫。另外,這種有JDK原生函數(shù)式的判斷方法,還是優(yōu)先使用函數(shù)式的寫法,一來是有方法名比較直觀,另外也是方便之后熟練使用Lamada,別寫出 .filter(x -> null == x) 這樣的寫法,還是 .filter(Objects::isNull) 更可讀些。
判斷字符串為空不要自己寫
容易漏邏輯,盡量使用現(xiàn)成的方法
//if(saleTypeStr == null || saleTypeStr.equals("")) if(StringUtils.isBlank(saleTypeStr))雖然原方法里無論判不判斷空字符或者空格字符都不會影響最終方法的表征,但是從第一行想表達的判斷“字符串是不是為空”這件事來看,這行并不能判斷“空格字符”存在的情況,所以詞不達意,另外也趁機強化記憶下isBlank和isEmpty的區(qū)別。 org.apache.commons.lang3里有很多工具類,方法比較成熟邏輯也比較完整。 同理org.apache.commons.collections4.CollectionUtils還有一堆集合操作的工具。
equals判定,常量寫前面
//if(saleTypeStr.equals("JX")) if("JX".equals(saleTypeStr))雖然前面判斷過null,所以這里并不會報空指針,但是但凡之后書寫前面漏了,這里就開始報錯了。
少用魔法值,定義常量
private static final String JX_SALE_TYPE_STR = "JX"; private static final Integer JX_SALE_TYPE_INT = 1;但凡同一個魔法值在多處用,就怕漏改,所以收束定義在常量下,至少能保證全局引用的統(tǒng)一性。
無狀態(tài)方法,可選擇定義為類靜態(tài)
//public Integer parseSaleType(String saleTypeStr) public static Integer parseSaleType(String saleTypeStr)
方法本身跟所在類的實例對象狀態(tài)無關(guān),且不會誘發(fā)線程安全問題,故符合被定義為static的條件,可先于對象創(chuàng)建在方法區(qū),防止每個對象創(chuàng)建一次的時候,堆內(nèi)存創(chuàng)建一次。
邏輯簡化
語法的問題強調(diào)完,就得再琢磨琢磨這段邏輯需不需要這么多代碼來表述了,乍眼一看沒問題,但其實沒必要寫這么多。
明確主體邏輯
判斷入?yún)⒂行?-> 處理核心邏輯 -> 缺省返回,其實這個方法的構(gòu)建思路是非常標準且合乎常理的,思考習慣很好,只是在這個簡單的方法場景不免邏輯有些冗余。 其實再看這個方法,最核心的邏輯就是把字符串對應到數(shù)字上,其他不命中的情況返回null就可以了,那么簡化邏輯后,為空判定其實可以去掉,直接變?yōu)椋?
private static final String JX_SALE_TYPE_STR = "JX"; private static final Integer JX_SALE_TYPE_INT = 1; public static Integer parseSaleType(String saleTypeStr){ if(JX_SALE_TYPE_STR.equals(saleTypeStr)){ return JX_SALE_TYPE_INT; } return null; }
語法簡化:三元運算符
再仔細看下場景有沒有成熟的范式,【布爾式+返回值+非此即彼】,三元運算符可堪一用。
public static Integer parseSaleType(String saleTypeStr){ return JX_SALE_TYPE_STR.equals(saleTypeStr) ? JX_SALE_TYPE_INT : null; }
語法簡化:Optional
這個場景范式也滿足,【可能為空,有后續(xù)處理,有條件,有缺省值】,Optional也算完美契合。
public static Integer parseSaleType(String saleTypeStr){ Optional.ofNullable(saleTypeStr).filter(JX_SALE_TYPE_STR::equals).map(o -> JX_SALE_TYPE_INT).orElse(null); }
方法獨立存在的必要性討論
其實語法簡化到三元運算符和Optional這一步,如果一個方法體內(nèi)只有這一行,這個方法獨立存在的必要性的就開始存疑了,如果所有的轉(zhuǎn)換流程都能收束在工程中的某個環(huán)節(jié)上,且保證這個方法的引用僅存在一處,那么這一行代碼其實放在主干代碼上更好,防止來回跳轉(zhuǎn)的代碼閱讀障礙,當然這也僅僅是在現(xiàn)狀下的討論,如果存在且不僅限于以下幾種狀況時還得獨立出來:
未來除了一種邏輯分支外,還會擴展其他分支,并且有被擴展的可能;
雖然還是一種邏輯分支,但是判斷的內(nèi)容變長了,跟上下文和調(diào)用狀態(tài)有關(guān);
雖然還是一種邏輯分支,但是邏輯總在調(diào)整;
一處定義,多點引用;
繼續(xù)拓展:定義枚舉
“如無必要,勿增實體” 假如這個傳入的字符其實還有很多種,返回的映射也有很多種的時候,其實在這里繼續(xù)寫一堆常量定義就很不理智了。
值枚舉構(gòu)建
考慮繼續(xù)將入?yún)⒌乃锌赡芎统鰠⒌乃锌赡?,可以?gòu)建為兩組枚舉值,這樣所有的同簇常量就被放到一起了。
public enum SaleTypeStrEnum{ JX, // OTHERS ; } @AllArgsConstructor @Getter public enum SaleTypeIntEnum{ JX(1), // OTHERS ; private Integer code; }但是這個枚舉功能并不完善,因為從入?yún)⒂成錇镾aleTypeStrEnum,依然需要一段轉(zhuǎn)換的邏輯,需要用到 SaleTypeStrEnum::name 來判定傳參命中了哪個,所以這個邏輯不應該放在枚舉外,繼續(xù)補充:
public enum SaleTypeStrEnum{ JX, // OTHERS ; public static SaleTypeStrEnum getByName(String saleTypeStr){ for (SaleTypeStrEnum value : SaleTypeStrEnum.values()) { if(value.name().equals(saleTypeStr)){ return value; } } return null; } }方法有了,但是每次傳進來值都要遍歷整個枚舉,O(n)效率太低了,還是老規(guī)矩,空間換時間。
public enum SaleTypeStrEnum{ JX, // OTHERS ; /** * 預熱轉(zhuǎn)換關(guān)系到內(nèi)存 */ private static MapNAME_MAP = Arrays.stream(SaleTypeStrEnum.values()).collect(Collectors.toMap(SaleTypeStrEnum::name, Function.identity())); public static SaleTypeStrEnum getByName(String saleTypeStr){ return NAME_MAP.get(saleTypeStr); } }
這樣每次檢索就是O(1)了,那么最終方法體內(nèi)也能使用switch管理原本的if-else
public static Integer parseSaleType(String saleTypeStr){ switch(SaleTypeStrEnum.getByName(saleTypeStr)){ case JX:return SaleTypeIntEnum.JX.getCode(); // OTHERS default:return null; } }
關(guān)系枚舉構(gòu)建
再仔細思考下,其實這里在描述的內(nèi)容,無論是哪個枚舉描述的內(nèi)容都是同一件事物,方法本身就是描述兩個不同編碼的轉(zhuǎn)換關(guān)系,且轉(zhuǎn)換關(guān)系本身就是單向的,且映射路徑極度簡單,所以簡單化一點,可以直接構(gòu)建轉(zhuǎn)換關(guān)系枚舉?。
@Getter @AllArgsConstructor public enum SaleTypeRelEnum { // 不在分別定義兩類變量,而是直接定義變量映射關(guān)系 JX("JX", 1), // OTHERS ; private String fromCode; private Integer toCode; private static MapFROM_CODE_MAP = Arrays.stream(SaleTypeRelEnum.values()).collect(Collectors.toMap(SaleTypeRelEnum::getFromCode, Function.identity())); public static SaleTypeRelEnum get(String saleTypeStr){ return FROM_CODE_MAP.get(saleTypeStr); } public static Integer parseCode(String saleTypeStr){ return Optional.ofNullable(SaleTypeRelEnum.get(saleTypeStr)).map(SaleTypeRelEnum::getToCode).orElse(null); } }
如果將轉(zhuǎn)關(guān)系作為枚舉,那么從職責上劃分,轉(zhuǎn)換這個動作應該是封閉在枚舉內(nèi)的固有行為,而不該暴露在外,故原來對方法的引用其實應該轉(zhuǎn)為對關(guān)系枚舉中 SaleTypeEnum::parseCode 方法的引用,O(1)檢索且封閉性良好,同時支持更多簡單單向映射關(guān)系的管理,要是以后出現(xiàn)的新場景都是這種關(guān)系,那夠扛很久嘞。
繼續(xù)拓展:設(shè)計模式
枚舉的前提還是基于無狀態(tài)前提,如果轉(zhuǎn)換的的映射關(guān)系不再單純,變得復雜,枚舉的簡單映射管理就不work了。? “萬事不決,上設(shè)計模式”? 哎~就是玩兒~
策略模式-簡單實現(xiàn)
首先,依然將傳入的字符串作為路由依據(jù),但是傳入的內(nèi)容為了防止有未來擴展,所以構(gòu)造一個上下文,策略本身基于上下文來處理,借助上文定義的值枚舉做策略路由。
/** * 定義策略接口 */ public interface SaleTypeParseStrategy{ Integer parse(SaleTypeParseContext saleTypeParseContext); } /** * 策略實現(xiàn) */ public class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{ @Override public Integer parse(SaleTypeParseContext saleTypeParseContext) { return SaleTypeIntEnum.JX.getCode(); } } /** * 調(diào)用上下文 */ @Data public class SaleTypeParseContext{ private SaleTypeStrEnum saleTypeStr; private SaleTypeParseStrategy parseStrategy; public Integer pasre(){ return parseStrategy.parse(this); } } public static Integer parseSaleType(String saleTypeStr){ SaleTypeStrEnum saleTypeEnum = SaleTypeStrEnum.getByName(saleTypeStr); SaleTypeParseContext context = new SaleTypeParseContext(); context.setSaleTypeStr(saleTypeEnum); switch(saleTypeStr){ // 策略路由 case JX:context.setParseStrategy(new JxSaleTypeParseStrategy());break; // 繼續(xù)擴展 default:return null; } return context.parse(); }當然,如果是這種沒有上下文強依賴的策略,無論是靜態(tài)單例還是Spring單例都會是一個不錯的選擇。SaleTypeParseContext本身可以繼續(xù)擴展內(nèi)容和其他屬性繼續(xù)豐富參數(shù),策略實現(xiàn)中也可以繼續(xù)針對更多參數(shù)擴充邏輯。
策略工廠-手動容器
策略是個好東西,但是簡單實現(xiàn)下,這里依然將策略實現(xiàn)的路由過程交給了調(diào)用方來做,那么每增加一種實現(xiàn),調(diào)用點還要繼續(xù)改,要是恰好有若干調(diào)用點就完犢子了,并不優(yōu)雅,所以搞個中間層容器工廠,解耦一下依賴。
@Component public static class SaleTypeParseStrategyContainer{ public final static MapSTRATEGY_MAP = new HashMap<>(); @PostConstruct public void init(){ STRATEGY_MAP.put(SaleTypeStrEnum.JX, new JxSaleTypeParseStrategy()); // 繼續(xù)拓展 } public Integer parse(SaleTypeParseContext saleTypeParseContext){ return Optional.ofNullable(STRATEGY_MAP.get(saleTypeParseContext.getSaleTypeStr())).map(strategy-> strategy.parse(saleTypeParseContext)).orElse(null); } }
容器內(nèi)手動創(chuàng)建各個策略的實現(xiàn)的單例后進行托管,那調(diào)用方只需要去構(gòu)建上下文就好了,實際調(diào)用的方法更換為 SaleTypeParseStrategyContainer::parse,那后續(xù)無論策略如何豐富,調(diào)用方都不需要再感知這部分變化。后續(xù)出現(xiàn)了新的策略實現(xiàn),則在工廠內(nèi)繼續(xù)追加路由表即可。
注冊與發(fā)現(xiàn)&策略工廠-Spring容器
如果考慮到策略會依賴Spring的bean和其他有狀態(tài)對象,那么這里也可以改成Spring的注入模式,同時繼續(xù)將“支持哪種情況”由托管方容器移動至策略內(nèi)部,改成由策略實現(xiàn)自身去注冊到容器中。??
public interface SaleTypeParseStrategy{ Integer parse(SaleTypeParseContext saleTypeParseContext); // 所支持的情況 SaleTypeStrEnum support(); } @Component public class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{ @Override public Integer parse(SaleTypeParseContext saleTypeParseContext) { return SaleTypeIntEnum.JX.getCode(); } @Override public SaleTypeStrEnum support() { return SaleTypeStrEnum.JX; } } @Component public static class SaleTypeParseStrategyContainer{ public final static Map這樣的話,連容器都不用改了,追加策略實現(xiàn)的改動只與當前策略有關(guān),調(diào)用方和容器類都不需要感知了,但是缺點就在于如果有倆策略支持的情況相同,取到的是哪個就聽天由命了~STRATEGY_MAP = new HashMap<>(); @Autowired private List parseStrategyList; @PostConstruct public void init(){ parseStrategyList.stream().forEach(strategy-> STRATEGY_MAP.put(strategy.support(), strategy)); } public Integer parse(SaleTypeParseContext saleTypeParseContext){ return Optional.ofNullable(STRATEGY_MAP.get(saleTypeParseContext.getSaleTypeStr())).map(strategy-> strategy.parse(saleTypeParseContext)).orElse(null); } }
注冊與發(fā)現(xiàn)&責任鏈
當然如果不能事先知道“支持哪種情況”,只能在運行時判斷“是否支持”,將事前判定改為運行時判定,廣義責任鏈會是一個不錯的選擇,把所有策略排成一排,誰舉手說自己能處理就誰處理。??
public interface SaleTypeParseStrategy{ Integer parse(SaleTypeParseContext saleTypeParseContext); // 用于判斷是否支持 boolean support(SaleTypeParseContext saleTypeParseContext); } @Component public class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{ @Override public Integer parse(SaleTypeParseContext saleTypeParseContext) { return SaleTypeIntEnum.JX.getCode(); } @Override public boolean support(SaleTypeParseContext saleTypeParseContext) { return SaleTypeStrEnum.JX.equals(saleTypeParseContext.getSaleTypeStr()); } } @Component public static class SaleTypeParseStrategyContainer{ @Autowired private List這樣的實現(xiàn),依然可以將改動收束在策略本體上,修改相對集中,可以無耦地進行擴展。parseStrategyList; public Integer parse(SaleTypeParseContext saleTypeParseContext){ return parseStrategyList.stream() .filter(strategy->strategy.support(saleTypeParseContext)) .findAny() .map(strategy->strategy.parse(saleTypeParseContext)) .orElse(null); } }
其他拓展
以上還只是在JAVA語言內(nèi)去玩一些花樣,在當前這種場景下肯定是有過度設(shè)計的嫌疑,7行代碼可以縮到1行,也可以擴充到70行,所以說嘛: “用代碼行數(shù)來考量一個程序員是不太合適滴!~”? 當然了,也還可以繼續(xù)借助其他的中間件搞花樣,包括但不限于:
植入Diamond走走動態(tài)配置開關(guān)的思路;
植入QLExpress搞搞邏輯表達式的思路;
把策略實現(xiàn)改成HsfProvider走分布式調(diào)用思路;
借助一些成熟的網(wǎng)關(guān)走服務路由的的調(diào)用思路;
就不再此再過多展開了。
總結(jié)
筆記向的內(nèi)容帖子,用于活躍思維打開思路,沒啥高科技~
審核編輯:湯梓紅
-
JAVA
+關(guān)注
關(guān)注
19文章
2966瀏覽量
104700 -
字符串
+關(guān)注
關(guān)注
1文章
578瀏覽量
20506 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4327瀏覽量
62569 -
代碼
+關(guān)注
關(guān)注
30文章
4779瀏覽量
68521
原文標題:好好的“代碼優(yōu)化”是怎么一步步變成“過度設(shè)計”的
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論