一、前言
? ? ? ? hello大家好,本次打算簡單聊一下裝飾者模式,其實寫有關設計模式的內容還是蠻有挑戰性的,首先呢就是小永哥實力有限擔心說不明白,其次設計模式是為了解決某些問題場景,在當前技術生態圈如此完善的情況下,能遇到使用設計模式的時候其實還是蠻少見的,而且我一直認為解決方案本身沒有好壞之分,只有經過取舍適合當前的才是最好的。任何事物都是雙刃劍,包括設計模式,在沒有適合場景的情況下強行使用反而會造成過度設計,這樣就不好了。
? ? ? ? 不過呢前幾天小永哥確實遇到了一個小場景能用一下裝飾者模式,不過作為演示呢我就不完整復刻那些復雜的代碼了,咱來點簡易的類比一下,盡量以少的代碼能說明白。
二、業務場景
? ? ? ? 當時的業務場景是一個Excel導入功能,我們都知道導入時,需要讀取逐行讀取Excel數據,每行數據又要對應列的數據讀取出來賦值給實體類對象,每一行就是一個實體類對象,最后將這些實體類對象設置到提前聲明好的list集合中并調用批量保存完成導入,簡易導入請看下述代碼。
package com.relation;import com.alibaba.fastjson2.JSON;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 20:20*/
public class ImportTest {@Testpublic void test() throws IOException {ArrayList<Map<String,Object>> mapList = new ArrayList<>();// 導入的源文件String path = "E:\\test\\demo.xlsx";// 工作薄對象Workbook workbook = WorkbookFactory.create(new FileInputStream(path));// 工作表 sheetSheet sheet = workbook.getSheetAt(0);// 行int rows = sheet.getPhysicalNumberOfRows();DataFormatter dataFormatter = new DataFormatter();// 行的頭信息,第一行,可以不處理for (int i = 1; i < rows; i++) {Row row = sheet.getRow(i);Map<String,Object> demoData = new HashMap<>();// 第一個單元格,因為是序號,可以不要// 從第二個單元格開始獲取名稱String name = dataFormatter.formatCellValue(row.getCell(1));demoData.put("name",name);// 第三個單元格獲取編號String code = dataFormatter.formatCellValue(row.getCell(2));demoData.put("code",code);// 把組裝好的 student對象,存入集合mapList.add(demoData);}System.out.println(JSON.toJSONString(mapList));}
}
?????????以上就是一個簡易的excel上傳代碼,運行測試類也能從excel中獲取到數據,一個很正常和普通的小需求,想來以各位老鐵的本事,這些都不叫事。下面有請我們T哥給我上點強度。
三、來自T哥的折磨
? ? ? ? T哥:一般找我來都沒啥好事,不過能給你填點堵我還是很樂意的。
? ? ? ? 小白:T哥你客氣了,有事您吩咐,沒有困難我制造困難也給你整明白兒的。
? ? ? ? T哥:好了,廢話少說,為了減少用戶手輸的錯誤率,你看能不能給表格內容去空格呀,防止用戶不小心在前后多輸空格然后又檢查不出來。
? ? ? ? 小白:那太簡單了,你就瞧好吧......
? ? ? ? 3.1、去空格需求的實現。
????????很簡單嘛,給設置值的位置調用一下String自帶的trim()方法不就好了,很輕松呀。
? ? ? ? 3.2、來自T哥的新需求。
????????T哥:這么快就實現了,不愧是專業的java開發人員,那個什么,客戶要求除去空格之外,需要將每個數據都加上一個前綴AAA。這個能實現吧。
? ? ? ? 小白:好吧,也不是什么問題。
我們可以看到小白很快又搞定了,但是小白陷入了沉思。
? ? ? ? 小白:不對呀,為什么我每次都要改所有單元格設置的位置呢?我得想辦法寫個方法把這些集中起來,這樣我只需要在第一次修改的時候改動位置稍微多一點,以后每次我修改集中起來的代碼就好了。說干就干。
????????
package com.relation;import com.alibaba.fastjson2.JSON;
import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 20:20*/
public class ImportTest {@Testpublic void test() throws IOException {// 將Excel文件導入到數據庫中ArrayList<Map<String,Object>> mapList = new ArrayList<>();// 導入的源文件String path = "E:\\test\\demo.xlsx";// 工作薄對象Workbook workbook = WorkbookFactory.create(new FileInputStream(path));// 工作表 sheetSheet sheet = workbook.getSheetAt(0);// 行int rows = sheet.getPhysicalNumberOfRows();DataFormatter dataFormatter = new DataFormatter();// 行的頭信息,第一行,可以不處理for (int i = 1; i < rows; i++) {Row row = sheet.getRow(i);Map<String,Object> demoData = new HashMap<>();// 第一個單元格,因為是序號,可以不要// 從第二個單元格開始獲取名稱String name = dataFormatter.formatCellValue(row.getCell(1));demoData.put("name",getStr(name));// 第三個單元格獲取編號String code = dataFormatter.formatCellValue(row.getCell(2));demoData.put("code",getStr(code));// 把組裝好的 student對象,存入集合mapList.add(demoData);}System.out.println(JSON.toJSONString(mapList));}private String getStr(String data){if(StringUtils.isEmpty(data)){return data;}return "AAA"+data.trim();}
}
????????小白抽取了一個方法,專門用來處理表格內容,好像比之前要好很多了。
? ? ? ? T哥:你進步了,已經開始能自查并完善自己的代碼了......
? ? ? ? 小白:當然了,每次都改那么多地方,我累,能少改干嘛要多改呢?
? ? ? ? T哥:那既然你這么說,那你抽取getStr方法之后,不還是得在第一次修改的時候,逐個找到每個獲取表格的位置去替換為使用getStr方法嗎?
? ? ? ? 小白:還好吧,只有兩處而已,有必要再折騰嗎?
? ? ? ? T哥:沒錯,現在確實是只有名稱和編碼兩個屬性,那萬一有幾十個屬性呢?你也要一個個去修改嗎?不是你自己說能少寫一行代碼就不多寫一行嗎?
? ? ? ? 小白:那好吧,我再想想辦法,畢竟我是一個面向對象工程師......
? ? ? ? 3.3、面向對象方式改造
package com.relation;import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 21:47*/
public class DataFormatterExpand extends DataFormatter {@Overridepublic String formatCellValue(Cell cell) {String cellValue = super.formatCellValue(cell);if(StringUtils.isEmpty(cellValue)){return cellValue;}return "AAA"+cellValue.trim();}
}
package com.relation;import com.alibaba.fastjson2.JSON;
import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 20:20*/
public class ImportTest {@Testpublic void test() throws IOException {// 將Excel文件導入到數據庫中ArrayList<Map<String,Object>> mapList = new ArrayList<>();// 導入的源文件String path = "E:\\test\\demo.xlsx";// 工作薄對象Workbook workbook = WorkbookFactory.create(new FileInputStream(path));// 工作表 sheetSheet sheet = workbook.getSheetAt(0);// 行int rows = sheet.getPhysicalNumberOfRows();DataFormatterExpand dataFormatter = new DataFormatterExpand();// 行的頭信息,第一行,可以不處理for (int i = 1; i < rows; i++) {Row row = sheet.getRow(i);Map<String,Object> demoData = new HashMap<>();// 第一個單元格,因為是序號,可以不要// 從第二個單元格開始獲取名稱String name = dataFormatter.formatCellValue(row.getCell(1));demoData.put("name",name);// 第三個單元格獲取編號String code = dataFormatter.formatCellValue(row.getCell(2));demoData.put("code",name);// 把組裝好的 student對象,存入集合mapList.add(demoData);}System.out.println(JSON.toJSONString(mapList));}
}
?
? ? ? ? 我們可以看到,小白擴展了一個類并繼承了DataFormatter并重寫了formatCellValue方法,先調用父類的formatCellValue,然后再對獲取的值進行統一擴展完成任務。
? ? ? ? T哥:很好,一個非常標準的OO設計。
? ? ? ? 小白:當然了,我可是一個專業的OO程序員。
? ? ? ? T哥:其實做到這一步已經設計的很不錯了,代碼改動也少,變化的代碼也抽取出去了,那還有可以改進的地方嗎?
? ? ? ? 小白:我不是很明白,現在不是很完美了嗎?為什么還要改進?
? ? ? ? T哥:如果針對此功能需求到此為止,那你的設計可以說完美,但是如果后續還有變化呢?你還能優雅保持你的擴展性嗎?
? ? ? ? 小白:我想是可以的。
? ? ? ? T哥:好的,那咱們繼續,
? ? ? ? 3.4、T哥的BUFF疊加
? ? ? ? T哥:來新需求了,我們需要再加上BBB后綴。
? ? ? ? 小白:這簡單,我繼續修改一下。
? ? ? ? T哥:新需求來了,要求把AAA前綴去掉......
? ? ? ? 小白:T哥,你怕不是玩死我吧?
? ? ? ? T哥:你說的什么話,不是你讓我來給你提需求嗎?
? ? ? ? 小白:我*******
? ? ? ? T哥:我*******
? ? ? ? 然后T哥和小白打起來了.........
? ? ? ? 小永哥:小白你先放手,這事兒哥幫你辦了,你先消消氣。
? ? ? ? 3.5、小永哥的改造
package com.relation.expand;import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 21:47*/
public class DataFormatterTrimExpand extends DataFormatter {private DataFormatter dataFormatter;public void setDataFormatter(DataFormatter dataFormatter) {this.dataFormatter = dataFormatter;}@Overridepublic String formatCellValue(Cell cell) {String cellValue = dataFormatter.formatCellValue(cell);if(StringUtils.isEmpty(cellValue)){return cellValue;}return cellValue.trim();}
}
????????
package com.relation.expand;import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 21:47*/
public class DataFormatterPrefixExpand extends DataFormatter {private DataFormatter dataFormatter;public void setDataFormatter(DataFormatter dataFormatter) {this.dataFormatter = dataFormatter;}@Overridepublic String formatCellValue(Cell cell) {String cellValue = dataFormatter.formatCellValue(cell);if(StringUtils.isEmpty(cellValue)){return cellValue;}return "AAA"+cellValue;}
}
package com.relation.expand;import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 21:47*/
public class DataFormatterSuffixExpand extends DataFormatter {private DataFormatter dataFormatter;public void setDataFormatter(DataFormatter dataFormatter) {this.dataFormatter = dataFormatter;}@Overridepublic String formatCellValue(Cell cell) {String cellValue = dataFormatter.formatCellValue(cell);if(StringUtils.isEmpty(cellValue)){return cellValue;}return cellValue+"BBB";}
}
package com.relation.expand;import org.apache.poi.ss.usermodel.DataFormatter;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 22:27*/
public class DataFormatterFactory {public static DataFormatter createDataFormatter(){DataFormatter dataFormatter = new DataFormatter();//創建去空格擴展類DataFormatterTrimExpand dataFormatterTrimExpand = new DataFormatterTrimExpand();//創建加前綴擴展類DataFormatterPrefixExpand dataFormatterPrefixExpand = new DataFormatterPrefixExpand();//創建加后綴擴展類DataFormatterSuffixExpand dataFormatterSuffixExpand = new DataFormatterSuffixExpand();//按需求進行組合//將原生DataFormatter設置到去空格擴展類來達到去空格功能dataFormatterTrimExpand.setDataFormatter(dataFormatter);//將去空格擴展類設置到加前綴擴展類,來給去空格后的值加前綴dataFormatterPrefixExpand.setDataFormatter(dataFormatterTrimExpand);//將加前綴擴展類設置到加后綴擴展類,給加完前綴值在加上后綴dataFormatterSuffixExpand.setDataFormatter(dataFormatterPrefixExpand);return dataFormatterSuffixExpand;}
}
package com.relation;import com.alibaba.fastjson2.JSON;
import com.relation.expand.DataFormatterFactory;
import com.relation.expand.DataFormatterTrimExpand;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;/*** @author huhy* @version 1.0* @Description:* @ClassName Date:2025/5/10 20:20*/
public class ImportTest {@Testpublic void test() throws IOException {// 將Excel文件導入到數據庫中ArrayList<Map<String,Object>> mapList = new ArrayList<>();// 導入的源文件String path = "E:\\test\\demo.xlsx";// 工作薄對象Workbook workbook = WorkbookFactory.create(new FileInputStream(path));// 工作表 sheetSheet sheet = workbook.getSheetAt(0);// 行int rows = sheet.getPhysicalNumberOfRows();DataFormatter dataFormatter = DataFormatterFactory.createDataFormatter();// 行的頭信息,第一行,可以不處理for (int i = 1; i < rows; i++) {Row row = sheet.getRow(i);Map<String,Object> demoData = new HashMap<>();// 第一個單元格,因為是序號,可以不要// 從第二個單元格開始獲取名稱String name = dataFormatter.formatCellValue(row.getCell(1));demoData.put("name",name);// 第三個單元格獲取編號String code = dataFormatter.formatCellValue(row.getCell(2));demoData.put("code",code);// 把組裝好的 student對象,存入集合mapList.add(demoData);}System.out.println(JSON.toJSONString(mapList));}
}
我們可以看到,經過小永哥的改造,前綴和后綴都加上了,但是好像代碼量多了很多。
? ? ? ? 小白:永哥,你這是什么名堂,為啥你改造完以后代碼量反而更多了呢?
? ? ? ? 小永哥:你說的對,初期確實代碼量比你的OO設計要多了不少,待我給你好好介紹一下。
????????3.6、代碼解析
? ? ? ? 1)、如下圖所示,我們為每一種擴展都創建了對應的實現類。
? ? ? ? 2)、我們改造了擴展類,我們加了一個DataFormatter類型的成員變量,我們不再調用父類的方法,而是調用成員變量的方法。
? ? ? ? 3)、這樣做的好處是,成員變量可以是原生的類,也可是我們的擴展類,這樣的話,我們就可以按照需求靈活對擴展類進行組合。
? ? ? ? 4)、我們創建了一個簡易的工廠DataFormatterFactory來生成DataFormatter,將我們組合的邏輯封裝在工廠類的createDataFormatter方法中,從此刻開始,我們已經將所有變化的東西,從我們的主業務邏輯中抽取了出來。
? ? ? ? 5)、經過一系列的改造,我們的代碼可擴展性和可維護性已經大大的提高了,需求變化時,如果我們已經有對應的擴展類,則直接進行組合即可,如果沒有對應的擴展類,我們可以創建新的擴展類,并進行新的組合。
? ? ? ? T哥:小永哥有兩下子哈,小白你能總結一下你小永哥這么做符合哪些原則嗎?
? ? ? ? 小白:首先,小永哥對每一種擴展創建對應的實現類,滿足了單一職責。對于新的擴展,創建新的實現類,而不對業務代碼進行修改,符合開閉原則,但好像對工廠類有改動,好像又沒有很完美的符合開閉原則。
? ? ? ? T哥:你說的很對,開閉原則是對修改關閉,對新增開放以大大減少對已有穩定代碼的改動,以減少需求變化對系統的影響,但是要完全做到開閉原則很難,我們在開發過程中或多或少都會涉及到對原有代碼的改動,這個是不可避免的,小永哥可以說將代碼改動控制在很小范圍之內了,如果出現問題,排查范圍也會小很多。
? ? ? ? 小永哥:不好意思,我插一句哈,在擴展類的改造中,我使用了組合模式,利用組合模式松耦合的特性來提升系統的彈性,小白這一點你要牢記,有時候有一個比是一個要更好,說的更直白一點,就是組合模式要比繼承的靈活性更強。
? ? ? ? 小白:永哥,我記住了。
????????3.7、總結
? ? ? ? 經過了一系列的改造,我們已經將代碼改造好了,特別是最后一次改造,運用的就是經典的裝飾模式。通過自由組合的方式對原始的類進行層層裝飾,從而達到我們的目的。
四、結語
? ? ? ? 又耗費了不少腦細胞,寫了這么多希望可以通過這個簡單的案例將裝飾模式能聊清楚,裝飾模式雖然很經典,但是在小永哥的心中,威力最大的還是策略模式,特別是策略模式+模版方法模式,威力巨大,能靈活多變的應對繁瑣的業務變化,小永哥好好構思一下業務場景,爭取下把和大家聊聊這兩種設計模式以及這兩種設計模式的組合體,本次就到這了,謝謝大家,晚安。