文章目錄
- 前提
- 策略模式
- 思想
- 實現
- 如何拓展
- 模板方法
- 存在的問題
- 思想
- 實現
- 如何拓展
- 工廠模式
- 實現
- 問題及解決(解耦)
- 配置文件方式
- 使用注解
- 單例模式
- 實現方式
- 1,懶漢式(線程不安全)
- 2,懶漢式(線程安全)
- 3,餓漢式
- 4,雙重校驗鎖機制(面)
- 5,靜態內部類
- 6,枚舉
- 體現
- 享元模式
- 門面模式
前提
假設做一個需求,從文件中拿到數據并存在數據庫中,文檔有多種不同的類型,比如json,excel,csv等等。在做這個去求得在過程中,如何讓代碼變得優雅,可讀性高,耦合度低,易擴展。
策略模式
為解決上述問題,首先想到的是下面的代碼
public class XXX {public void export2Db(String filepath) {String type = getFileType(filepath);if ("csv".equals(type)) {// 讀取csv文件, 將數據保存到數據庫中, 此處省略500行代碼} else if ("json".equals(type)) {// 讀取json文件, 將數據保存到數據庫中, 此處省略500行代碼} else if ("excel".equals(type)) {// 讀取excel文件, 將數據保存到數據庫中, 此處省略500行代碼} else {throw new IllegalArgumentException("不支持該類型: " + type);}}
}
這里可以看到有很多問題,比如
- type使用String類型的魔法值, 沒用枚舉.
- 有幾個type就if判斷幾次, 假設新增txt文件類型, 又要修改代碼, 拓展性差.
- 代碼核心代碼都寫到一個方法中, 一些邏輯無法復用, 而且會導致方法代碼巨多, 可讀性差, 后續也不好維護.
思想
策略模式是多態最好的體現, 也是解決這種標簽類的最好的方式之一.
策略模式的定義為: 在策略模式定義了一系列策略類,并將每個具體實現封裝在獨立的類中,使得它們可以互相替換。通過使用策略模式,可以在運行時根據需要選擇不同的算法,而不需要修改調用端代碼。是一種用來解決很多if else的方式.
實現
在本需求中, 需要寫一個頂層的策略接口FileExport, 新增 export2Db抽象方法.
然后根據不同類型的導出方式, 編寫CsvExport, ExcelExport, JsonExport三個策略類實現FileExport接口.
這里給出類圖和具體代碼.
public interface FileExport {void export2Db(String filepath);
}
public class CsvExport implements FileExport{@Overridepublic void export2Db(String filepath) {// 讀取csv文件, 將數據保存到數據庫中, 此處省略具體代碼}
}
public class ExcelExport implements FileExport {@Overridepublic void export2Db(String filepath) {// 讀取excel文件, 將數據保存到數據庫中, 此處省略具體代碼}
}
public class JsonExport implements FileExport{@Overridepublic void export2Db(String filepath) {// 讀取json文件, 將數據保存到數據庫中, 此處省略具體代碼}
}
有其他類依賴于我們的策略類, 那么就可以這樣使用, 需要哪個直接傳入對應的FileExport對象即可.
class XXX {// 注意這里參數類型聲明為FileExport接口, 這就意味著可以傳入任意的FileExport實現類public static void fileExport2Db(FileExport fileExport, String filepath) {fileExport.export2Db(filepath);}public static void main(String[] args) {FileExport excelExport = new ExcelExport();fileExport2Db(excelExport, "文件路徑");
}
如何拓展
使用策略模式后, 如果后續需求變更, 需要拓展其他文件格式導出到數據庫, 比如yml文件導出到數據庫. 那么我們新增YmlExport類, 實現FileExport即可.
模板方法
存在的問題
那么, 目前的代碼就不存在問題了嗎? 當然不是, 我們來看策略模式常見的兩個問題
- 不同實現類中代碼重復(靠模板方法解決)
- 如果想要根據傳入參數動態使用某個策略類, 還是避免不了大量if else
第一個問題:
當我們要實現具體將某中文件數據導出到數據庫時, 可以把大致過程劃分為以下幾步
- 檢查參數中的filepath是否合法
- 路徑是否不為空
- 文件是否存在
- 文件類型是否和對應策略類類型一致
- 讀取文件數據到一個Java對象中
- 對數據進行處理,比如去除空格之類的,這里就是簡單模擬一下
- 注意, 有的文件讀取后需要處理, 有的不需要,這里假設json文件需要做額外處理, 但是csv和excel文件不需要讀取數據后做處理
- 保存到數據庫中
- 將處理后的數據轉為數據表對應的實體類
- 使用mybatis/jpa/jdbc等orm工具保存到數據庫中
通過上述的過程我們發現,每個策略類的具體實現經歷的大體步驟/框架都相同,只有少部分的代碼/邏輯不同,如果每個類都自己寫自己的具體實現,就會導致大量的重復代碼。
第二個問題:
什么是動態使用策略類?簡而言之, 就是根據傳入的參數, 或者根據某些情況來決定使用哪個策略類來處理.
現在只能傳入FileExport類型的參數,如果我要傳入String類型的filePath或者其他標識文件類型的參數,就又會導致因判斷屬于哪個FileExport而產生if-else,代碼如下
public class XXX {public void import2Db(String filepath) {String fileType = getFileType(filepath);FileExport fileExport;if ("csv".equals(fileType)) {fileExport = new CsvExport();fileExport.export2Db(filepath);} else if ("json".equals(fileType)) {fileExport = new JsonExport();fileExport.export2Db(filepath);} else if ("excel".equals(fileType)) {fileExport = new ExcelExport();fileExport.export2Db(filepath);} else {throw new IllegalArgumentException("不支持該類型: " + fileType);}}
}
思想
接下來, 我們用模板方法模式來解決第一個問題, 也就是不同實現類中的代碼重復問題。
模板方法模式會在抽象類或者接口中定義一個算法的整體流程, 該流程中會調用不同的方法. 這些方法的具體實現交給不同的子類完成. 也就是說它適合整體流程固定, 具體細節不同的場景.
實現
定義一個抽象類來當模板類
- 具體方法void check(String filepath): 檢查filepath是否合法
- 具體方法 void fileDataExport2Db(FileData fileData): 導出數據到數據庫
- 實現void export2Db(String filepath): 調用以上四個抽象方法來完成文件導出到數據庫
- 抽象方法needProcessData():是否需要進行數據處理
- 抽象方法 FileData readFile(String filepath): 來讀取文件數據
- 抽象方法 FileData processData(FileData fileData): 來處理數據
public interface FileExport {void export2Db(String filepath);
}
public abstract class AbstractFileExport implements FileExport {@Overridepublic void export2Db(String filepath) {check(filepath);FileData fileData = readFile(filepath);// 鉤子函數, 子類決定是否需要對數據進行處理if (needProcessData()) {fileData = processData(fileData);}fileDataExport2Db(fileData);}protected void check(String filepath) {// 檢查filepath是否為空if (StrUtil.isBlank(filepath)) {throw new IllegalArgumentException("filepath為空");}// 檢查filepath是否存在, 是否為文件File file = new File(filepath);if (!file.exists() || !file.isFile()) {throw new IllegalArgumentException("filepath不存在或者不是文件");}// 檢查文件類型是否為子類可以處理的類型 (用了hutool的FileTypeUtil工具)String type = FileTypeUtil.getType(file);if (!Objects.equals(getFileType(), type)) {throw new IllegalArgumentException("文件類型異常: " + type);}}/*** 數據類型轉換并保存到數據庫, 這是通用操作, 所以寫在父類中*/protected void fileDataExport2Db(FileData fileData) {System.out.println("將處理后的數據轉為數據表對應的實體類");System.out.println("使用mybatis/jpa/jdbc等orm工具保存到數據庫中");}/*** 如果子類要處理數據, needProcessData()返回true, 并重新該方法*/protected FileData processData(FileData fileData) {throw new UnsupportedOperationException();}/*** 獲取子類能處理的文件類型, check()方法會用到*/protected abstract String getFileType();/*** 鉤子函數, 讓子類決定是否需要處理數據*/protected abstract boolean needProcessData();protected abstract FileData readFile(String filepath);
}
public class JsonExport extends AbstractFileExport {private static final String FILE_TYPE = "json";@Overrideprotected String getFileType() {return FILE_TYPE;}@Overrideprotected boolean needProcessData() {return false;}protected FileData readFile(String filepath) {System.out.println("以json方式讀取filepath中的文件");System.out.println("將讀取后的結果轉為通用的FileData類型");return new FileData();}
}
大量重復代碼和流程都被抽取到父類中了. 策略模式中出現的代碼重復問題就解決了.
如何拓展
和之前類似, 如果后續需求變更, 需要拓展其他文件格式導出到數據庫, 比如yml文件導出到數據庫. 那么我們新增YmlExport類, 繼承AbstractFileExport即可.
由于AbstractFileExport規定了統一流程, 且提供了 check(), fileDataExport2Db()等方法, 所以后續拓展起來代碼量也會更少, 更方便.
工廠模式
前面還剩下一個問題,就是根據傳入的參數動態的調用。通過工廠+枚舉類來實現。
工廠模式就是用來創建對象的,可以根據參數的不同返回不同的實例。
三種工廠模式的區別-CSDN博客
這里使用簡單工廠模式
實現
枚舉類
@Getter
@AllArgsConstructor
@ToString
public enum FileType {JSON("json"),CSV("csv");private final String type;private static final Map<String, FileType> VALUE_MAP = Arrays.stream(values()).collect(Collectors.toMap(FileType::getType,Function.identity(),(existing, replacement)->replacement));public static FileType stringParseObject(String fileType) {if(!VALUE_MAP.containsKey(fileType)){throw new IllegalArgumentException("不支持的文件類型");}return VALUE_MAP.get(fileType);}
}
工廠類
public class FileExportFactory {private static final Map<FileType, FileExport> CACHE = new HashMap<>();static {CACHE.put(FileType.JSON, new JsonExport());CACHE.put(FileType.CSV, new CsvExport());}public static FileExport getFileExport(FileType fileType) {if (!CACHE.containsKey(fileType)) {throw new IllegalArgumentException("找不到對應類型:" + fileType.getType());}return CACHE.get(fileType);}public static FileExport getFileExport(String type) {FileType fileType = FileType.from(type);return getFileExport(fileType);}
}
問題及解決(解耦)
可以發現,這種情況下如果要增加新的新的文件類型,那么就需要更改FileExportFactory工廠類的代碼,違反了OOP原則中的開閉原則(當應用需求發生改變的時候,我們盡量不要修改源代碼,可以對其進行擴展,擴展的功能塊不會影響到原來的功能塊)。
解決方法
spring的解決方法有兩種
- @Component/@Bean,使用注解方式,動態添加新的文件類型
- spring.factories,使用kv鍵值對,配置了需要自動裝配類的全類名
配置文件方式
在resource文件夾下的yml配置文件中定義需要用到的全類名,然后讀取出來。也可以通過反射拿到所有實現FileExport接口的類,然后篩選拿到需要用到的類。
這里是在枚舉類中定義好相應的全類名,這樣在工廠類中可以直接拿到。理由:實現類很少,操作簡便。
枚舉類
@Getter
@AllArgsConstructor
@ToString
public enum FileType {JSON("json", "com.luxiya.design.JsonExport"),CSV("csv","com.luxiya.design.CsvExport");private final String type;private final String className;private static final Map<String, FileType> VALUE_MAP = Arrays.stream(values()).collect(Collectors.toMap(FileType::getType,Function.identity(),(existing, replacement)->replacement));public static FileType stringParseObject(String fileType) {if(!VALUE_MAP.containsKey(fileType)){throw new IllegalArgumentException("不支持的文件類型");}return VALUE_MAP.get(fileType);}@SneakyThrowspublic FileExport classNameParseObject() {Class<?> clazz = Class.forName(this.getClassName());return (FileExport) clazz.newInstance();}
}
工廠類
public class FileExportFactory {private static final Map<FileType, FileExport> Cache;static {Cache = Arrays.stream(FileType.values()).map(fileType -> new Pair<>(fileType, fileType.classNameParseObject())).collect(Collectors.toMap(Pair::getKey,Pair::getValue,(existing, replacement)-> replacement));}public static FileExport getFileExport(FileType fileType) {if (!Cache.containsKey(fileType)) {throw new IllegalArgumentException("不支持的文件類型");}return Cache.get(fileType);}public static FileExport getFileExport(String fileType) {FileType fileTypeNew = FileType.stringParseObject(fileType);System.out.println(fileTypeNew);return getFileExport(fileTypeNew);}
}
這樣如果新增YmlExport類,增加實現類,然后在枚舉類中修改。
使用注解
使用注解實現解耦的流程大概如下
- 定義注解,并在相應的類上添加注解。
- 通過反射機制拿到添加了注解的類,放入工廠。
定義注解,并在JsonExport和CsvExport類上添加該注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FileExportComponent {
}
工廠類拿到所需類
public class FileExportFactory {private static final Map<FileType, FileExport> Cache;static {Set<Class<?>> classes = ClassUtil.scanPackage("com.luxiya.design", FileExport.class::isAssignableFrom);Cache = classes.stream().filter(ClassUtil::isNormalClass).filter(clazz -> AnnotationUtil.hasAnnotation(clazz, FileExportComponent.class)).map(ReflectUtil::newInstance).map(fileExport -> (FileExport) fileExport).collect(Collectors.toMap(FileExport::getSupportType,Function.identity(),(existing, replacement) -> replacement));}public static FileExport getFileExport(FileType fileType) {if (!Cache.containsKey(fileType)) {throw new IllegalArgumentException("不支持的文件類型");}return Cache.get(fileType);}public static FileExport getFileExport(String fileType) {FileType fileTypeNew = FileType.stringParseObject(fileType);System.out.println(fileTypeNew);return getFileExport(fileTypeNew);}
}
單例模式
保證一個類只有一個實例,并提供一個全局訪問他的訪問點,避免一個全局使用的類頻繁的創建與銷毀。
實現方式
1,懶漢式(線程不安全)
**是否 Lazy 初始化:**是
**是否多線程安全:**否
**實現難度:**易
**描述:**這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因為沒有加鎖 synchronized,所以嚴格意義上它并不算單例模式。
這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
2,懶漢式(線程安全)
**是否 Lazy 初始化:**是
**是否多線程安全:**是
**實現難度:**易
**描述:**這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。
- 優點:第一次調用才初始化,避免內存浪費。
- 缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
- getInstance() 的性能對應用程序不是很關鍵(該方法使用不太頻繁)
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
3,餓漢式
**是否 Lazy 初始化:**否
**是否多線程安全:**是
**實現難度:**易
**描述:**這種方式比較常用,但容易產生垃圾對象。
- 優點:沒有加鎖,執行效率會提高。
- 缺點:類加載時就初始化,浪費內存。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; }
}
4,雙重校驗鎖機制(面)
**是否 Lazy 初始化:**是
**是否多線程安全:**是
**實現難度:**較復雜
**描述:**這種方式采用雙鎖機制,安全且在多線程情況下能保持高性能。
getInstance() 的性能對應用程序很關鍵。
private static volatile Singleton singleton;private Singleton(){}public static Singleton getInstance(){if(singleton == null){synchronized (Singleton.class){if(singleton == null){singleton = new Singleton();}}}return singleton;}
5,靜態內部類
**是否 Lazy 初始化:**是
**是否多線程安全:**是
利用 ClassLoader 的特性:
- 類的靜態變量在第一次加載類時初始化,JVM 保證這一過程是線程安全的。
- 靜態內部類(如
SingletonHolder
)不會隨外部類(Singleton
)的加載而加載,只有在被顯式調用時才加載。
public class Singleton {private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}private Singleton() {}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}
6,枚舉
**是否 Lazy 初始化:**否
**是否多線程安全:**是
**實現難度:**易
**描述:**這種實現方式還沒有被廣泛采用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。
public enum Singleton { INSTANCE; public void whateverMethod() { }
}
體現
上述需求中,其實FileFactory工廠類的Map存儲了所有FileExport的實現類,所用代碼中用到的都是Map中的實現類,就是單例模式。
且用到的是枚舉創建的對象,而且不會被反射和反序列化破壞。
享元模式
通過共享對象來減少系統對象的數量,本質就是緩存對象,降低內存消耗。
享元(Flyweight)的核心思想很簡單:如果一個對象實例一經創建就不可變,那么反復創建相同的實例就沒有必要,直接向調用方返回一個共享的實例就行,這樣即節省內存,又可以減少創建對象的過程,提高運行速度。
享元模式在Java標準庫中有很多應用。我們知道,包裝類型如Byte
、Integer
都是不變類,因此,反復創建同一個值相同的包裝類型是沒有必要的。以Integer
為例,如果我們通過Integer.valueOf()
這個靜態工廠方法創建Integer
實例,當傳入的int
范圍在-128
~+127
之間時,會直接返回緩存的Integer
實例:
// 享元模式
public class Main {public static void main(String[] args) throws InterruptedException {Integer n1 = Integer.valueOf(100);Integer n2 = Integer.valueOf(100);System.out.println(n1 == n2); // true}
}
對于Byte
來說,因為它一共只有256個狀態,所以,通過Byte.valueOf()
創建的Byte
實例,全部都是緩存對象。
因此,享元模式就是通過工廠方法創建對象,在工廠方法內部,很可能返回緩存的實例,而不是新創建實例,從而實現不可變實例的復用。
其實FileFactory工廠類的Map就是共享對象,運用到了享元模式。
門面模式
一文搞懂設計模式—門面模式-CSDN博客
門面模式(Facade Pattern)也叫做外觀模式,是一種結構型設計模式。它提供一個統一的接口,封裝了一個或多個子系統的復雜功能,并向客戶端提供一個簡單的調用方式。通過引入門面,客戶端無需直接與子系統交互,而只需要通過門面來與子系統進行通信。
角色 | 職責 |
---|---|
門面(Facade) | 提供統一接口,封裝子系統的功能調用,隱藏內部細節。 |
子系統(Subsystem) | 實現具體功能的多個模塊或類,不直接對外暴露,由門面協調調用。 |
客戶端(Client) | 通過門面對象間接調用子系統功能,無需依賴具體子系統類。 |
簡單門面類
public class FileExportClient {public static void exportToDb(String filePath){String type = FileTypeUtil.getTypeByPath(filePath);FileExport fileExport = FileExportFactory.getFileExport(type);fileExport.export(filePath);}public static void exportToDb(File file){String filePath = file.getAbsolutePath();exportToDb(filePath);}}