CTO:別再寫一摞if-else了,再寫開除!
代碼潔癖狂們!看到一個類中有幾十個 if-else 是不是很抓狂?設計模式學了用不上嗎?面試的時候問你,你只能回答最簡單的單例模式,問你有沒有用過反射之類的高級特性,回答也是否嗎?
這次就讓設計模式(模板方法模式+工廠模式)和反射助你消滅 if-else!真的是開發(fā)中超有用的干貨啊!
1.那個坑貨
某日,碼農(nóng)胖滾豬接到上級一個需求,這個需求牛逼了,一站式智能報表查詢平臺,支持 MySQL、Pgxl、TiDB、Hive、Presto、Mongo 等眾多數(shù)據(jù)源,想要啥數(shù)據(jù)都能通通給你查出來展示,對于業(yè)務人員數(shù)據(jù)分析有重大意義!
雖然各個數(shù)據(jù)源的參數(shù)校驗、查詢引擎和查詢邏輯都不一樣,但是胖滾豬對這些框架都很熟悉,這個難不倒她,她只花了一天時間就都寫完了。
領導胖滾熊也對胖滾豬的效率表示了肯定??墒呛镁安婚L,第三天,領導閑著沒事,準備做一下 Code Review,可把胖滾熊驚呆了,一個類里面有近 30 個 if-else 代碼,我滴個媽呀,這可讓代碼潔癖狂崩潰了。
- // 檢驗入?yún)⒑戏ㄐ?nbsp;
- Boolean check = false;
- if(DataSourceEnum.hive.equals(dataSource)){
- check = checkHiveParams(params);
- } else if(DataSourceEnum.tidb.equals(dataSource)){
- check = checkTidbParams(params);
- } else if(DataSourceEnum.mysql.equals(dataSource)){
- check = checkMysqlParams(params);
- } // else if ....... 省略pgxl、presto等
- if(check){
- if(DataSourceEnum.hive.equals(dataSource)){
- list = queryHive(params);
- } else if(DataSourceEnum.tidb.equals(dataSource)){
- list = queryTidb(params);
- } else if(DataSourceEnum.mysql.equals(dataSource)){
- list = queryMysql(params);
- } // else if ....... 省略pgxl、presto等
- }
- //記錄日志
- log.info("用戶={} 查詢數(shù)據(jù)源={} 結(jié)果size={}",params.getUserName(),params.getDataSource(),list.size());
2.模板模式來救場
首先我們來分析下,不管是什么數(shù)據(jù)源,算法結(jié)構(gòu)(流程)都是一樣的:
- 校驗參數(shù)合法性
- 查詢
- 記錄日志
這不就是說模板一樣、只不過具體細節(jié)不一樣,沒錯吧?讓我們來看看設計模式中模板方法模式的定義吧:
模板方法模式:定義一個操作中的算法的框架,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。通俗的講,就是將子類相同的方法,都放到其抽象父類中。
我們這需求不就和模板方法模式差不多嗎?因此我們可以把模板抽到父類(抽象類)中。至于特定的步驟實現(xiàn)不一樣,這些特殊步驟,由子類去重寫就好了。
廢話不多說了,我們先把父類模板寫好吧,完全一樣的邏輯是記錄日志,這步在模板寫死就好。
至于檢驗參數(shù)和查詢,這兩個方法各不相同,因此需要置為抽象方法,由子類去重寫。
- public abstract class AbstractDataSourceProcesser <T extends QueryInputDomain> {
- public List<HashMap> query(T params){
- List<HashMap> list = new ArrayList<>();
- //檢驗參數(shù)合法性 不同的引擎sql校驗邏輯不一樣
- Boolean b = checkParam(params);
- if(b){
- //查詢
- list = queryData(params);
- }
- //記錄日志
- log.info("用戶={} 查詢數(shù)據(jù)源={} 結(jié)果size={}",params.getUserName(),params.getDataSource(),list.size());
- return list;
- }
- //抽象方法 由子類來實現(xiàn)特定邏輯
- abstract Boolean checkParam(T params);
- abstract List<HashMap> queryData(T params);
- }
這段代碼非常簡單。但是為了照顧新手,還是想解釋一個東西:T 這個玩意。叫泛型,因為不同數(shù)據(jù)源的入?yún)⒉灰粯?,所以我們使用泛型?/p>
但是他們也有公共的參數(shù),比如用戶名。因此為了不重復冗余,更好的利用公共資源及規(guī)范入?yún)?,在泛型的設計上,我們可以有一個泛型上限,
- public class QueryInputDomain<T> {
- public String userName;//查詢用戶名
- public String dataSource;//查詢數(shù)據(jù)源 比如mysql\tidb等
- public T params;//特定的參數(shù) 不同的數(shù)據(jù)源參數(shù)一般不一樣
- }
- public class MysqlQueryInput extends QueryInputDomain{
- private String database;//數(shù)據(jù)庫
- public String sql;//sql
- }
接下來就輪到子類出場了,通過上面的分析,其實也很簡單了,不過是繼承父類,重寫 checkParam() 和 queryData() 方法。
下面以 MySQL 數(shù)據(jù)源為例,其他數(shù)據(jù)源也都一樣的套路:
- @Component("dataSourceProcessor#mysql")
- public class MysqlProcesser extends AbstractDataSourceProcesser<MysqlQueryInput>{
- @Override
- public Boolean checkParam(MysqlQueryInput params) {
- System.out.println("檢驗mysql參數(shù)是否準確");
- return true;
- }
- @Override
- public List<HashMap> queryData(MysqlQueryInput params) {
- List<HashMap> list = new ArrayList<>();
- System.out.println("開始查詢mysql數(shù)據(jù)");
- return list;
- }
- }
這樣一來,所有的數(shù)據(jù)源,都自成一體,擁有一個只屬于自己的類,后續(xù)要擴展數(shù)據(jù)源、或者要修改某個數(shù)據(jù)源的邏輯,都非常方便和清晰了。
說實話,模板方法模式太簡單了,抽象類這東西也太基礎普遍了,一般應屆生都會知道的。
但是對于初入職場的新人來說,還真不太能果斷應用在實際生產(chǎn)中。因此提醒各位:一定要有一個抽象思維,避免代碼冗余重復。
另外,要再啰嗦幾句,即使工作有幾年的工程師也很容易犯一個錯誤。就是把思維局限在今天的需求。
比如老板一開始只給你一個 MySQL 數(shù)據(jù)源查詢的需求,壓根沒有 if-else,可能你就不會放在心上,直接在一個類中寫死,不會考慮到后續(xù)的擴展。
直到后面越來越多的新需求,你才恍然大悟,要全部重構(gòu)一番,這樣浪費自己的時間了。
因此提醒各位:做需求不要局限于今天,要考慮到未來。從一開始就做到高擴展性,后續(xù)需求變更和維護就非常爽了。
3.工廠模式來救場
- #Phone類:手機標準規(guī)范類(AbstractProduct)
- public interface Phone {
- void make();
- }
- #MiPhone類:制造小米手機(Product1)
- public class MiPhone implements Phone {
- public MiPhone() {
- this.make();
- }
- @Override
- public void make() {
- System.out.println("make xiaomi phone!");
- }
- }
- #IPhone類:制造蘋果手機(Product2)
- public class IPhone implements Phone {
- public IPhone() {
- this.make();
- }
- @Override
- public void make() {
- System.out.println("make iphone!");
- }
- }
但是模板模式還是沒有完全解決胖滾豬的 if-else,因為需要根據(jù)傳進來的 dataSource 參數(shù),判斷由哪個 service 來實現(xiàn)查詢邏輯,現(xiàn)在是這么寫的:
- if(DataSourceEnum.hive.equals(dataSource)){
- list = queryHive(params);
- } else if(DataSourceEnum.tidb.equals(dataSource)){
- list = queryTidb(params);
- }
那么這種 if-else 應該怎么去干掉呢?我想先跟你講講工廠模式的那些故事。
工廠模式:工廠方法模式是一種創(chuàng)建對象的模式,它被廣泛應用在 jdk 中以及 Spring 和 Struts 框架中。它將創(chuàng)建對象的工作轉(zhuǎn)移到了工廠類。
為了呼應一下工廠兩字,我特意舉一個代工廠的例子讓你理解,這樣你應該會有更深刻的印象。
以手機制造業(yè)為例。我們知道有蘋果手機、小米手機等等,每種品牌的手機制造方法必然不相同,我們可以先定義好一個手機標準接口,這個接口有 make() 方法,然后不同型號的手機都繼承這個接口:
- #Phone類:手機標準規(guī)范類(AbstractProduct)
- public interface Phone {
- void make();
- }
- #MiPhone類:制造小米手機(Product1)
- public class MiPhone implements Phone {
- public MiPhone() {
- this.make();
- }
- @Override
- public void make() {
- System.out.println("make xiaomi phone!");
- }
- }
- #IPhone類:制造蘋果手機(Product2)
- public class IPhone implements Phone {
- public IPhone() {
- this.make();
- }
- @Override
- public void make() {
- System.out.println("make iphone!");
- }
- }
現(xiàn)在有某手機代工廠:【天霸手機代工廠】??蛻糁粫嬖V該工廠手機型號,就要匹配到不同型號的制作方案,那么代工廠是怎么實現(xiàn)的呢?
其實也很簡單,簡單工廠模式(還有抽象工廠模式和工廠方法模式,有興趣可以了解下)是這么實現(xiàn)的:
- #PhoneFactory類:手機代工廠(Factory)
- public class PhoneFactory {
- public Phone makePhone(String phoneType) {
- if(phoneType.equalsIgnoreCase("MiPhone")){
- return new MiPhone();
- }
- else if(phoneType.equalsIgnoreCase("iPhone")) {
- return new IPhone();
- }
- }
- }
這樣客戶告訴你手機型號,你就可以調(diào)用代工廠類的方法去獲取到對應的手機制造類。
你會發(fā)現(xiàn)其實也不過是 if-else,但是把 if-else 抽到一個工廠類,由工廠類統(tǒng)一創(chuàng)建對象,對我們的業(yè)務代碼無入侵,不管是維護還是美觀上都會好很多。
首先,我們應該在每個特定的 dataSourceProcessor(數(shù)據(jù)源執(zhí)行器),比如 MysqlProcesser、TidbProcesser 中添加 Spring 容器注解 @Component。
該注解我想應該不用多解釋了吧,重點是:我們可以把不同數(shù)據(jù)源都搞成類似的 bean name,形如 dataSourceProcessor#數(shù)據(jù)源名稱,如下兩段代碼:
- @Component("dataSourceProcessor#mysql")
- public class MysqlProcesser extends AbstractDataSourceProcesser<MysqlQueryInput>{
- @Component("dataSourceProcessor#tidb")
- public class TidbProcesser extends AbstractDataSourceProcesser<TidbQueryInput>{
這樣有什么好處呢?我可以利用 Spring 幫我們一次性加載出所有繼承于 AbstractDataSourceProcesser 的 Bean ,形如 Map
Key 是 Bean 的名稱,而 Value 則是對應的 Bean:
- @Service
- public class QueryDataServiceImpl implements QueryDataService {
- @Resource
- public Map<String, AbstractDataSourceProcesser> dataSourceProcesserMap;
- public static String beanPrefix = "dataSourceProcessor#";
- @Override
- public List<HashMap> queryData(QueryInputDomain domain) {
- AbstractDataSourceProcesser dataSourceProcesser = dataSourceProcesserMap.get(beanPrefix + domain.getDataSource());
- //省略query代碼
- }
- }
可能你還是不太理解,我們直接看一下運行效果:
①dataSourceProcesserMap 內(nèi)容如下所示,存儲了所有數(shù)據(jù)源 Bean,Key 是 Bean 的名稱、而 Value 則是對應的 Bean:
②我只需要通過 key(即前綴+數(shù)據(jù)源名稱=beanName),就能匹配到對應的執(zhí)行器了。
比如當參數(shù) dataSource 為 tidb 的時候,key 為 dataSourceProcessor#tidb。
根據(jù) key 可以直接從 dataSourceProcesserMap 中獲取到 TidbProcesser:
4.反射來救場
還是直接來看看代碼吧:
- public static String classPrefix = "com.lyl.java.advance.service.";
- AbstractDataSourceProcesser sourceGenerator =
- (AbstractDataSourceProcesser) Class.forName
- (classPrefix+DataSourceEnum.getClasszByCode(domain.getDataSource()))
- .newInstance();
需要注意的是,該種方法是通過 className 來獲取到類的實例,而前端傳參肯定是不會傳 className 過來的。
因此可以用到枚舉類,去定義好不同數(shù)據(jù)源的類名:
- public enum DataSourceEnum {
- mysql("mysql", "MysqlProcesser"),
- tidb("tidb", "TidbProcesser");
- private String code;
- private String classz;
5.總結(jié)
有些童鞋總覺得設計模式用不上,因為平時寫代碼除了 CRUD 還是 CRUD,面試的時候問你設計模式,你只能回答最簡單的單例模式,問你有沒有用過反射之類的高級特性,回答也是否。
其實不然,Java 這 23 種設計模式,每一個都是經(jīng)典。今天我們就用模板方法模式+工廠模式(或者反射)解決了讓人崩潰的 if-else。
后續(xù)對于設計模式的學習,也應該多去實踐,從真實的項目中找到用武之地,你才算真正把知識占為己有了。
本篇文章的內(nèi)容和技術(shù)點雖然很簡單,但旨在告訴大家應該要有一個很好的代碼抽象思維。杜絕在代碼中出現(xiàn)一大摞 if-else 或者其他爛代碼。
即使你有很好的代碼抽象思維,做需求開發(fā)的時候,也不要局限于當下,只考慮現(xiàn)在,要多想想未來的擴展性。
就像你談戀愛一樣,只考慮當下的是渣男,考慮到未來的,才算是一個負責任的人,"愿世界沒有渣男"!
作者:Liuyanling
編輯:陶家龍
出處:轉(zhuǎn)載自微信公眾號胖滾豬學編程(ID:bdstar_lyl)