? ? ? ? GoF是 “Gang of Four”(四人幫)的簡稱,它們是指4位著名的計算機科學家:Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides。他們合作編寫了一本非常著名的關于設計模式的書籍《Design Patterns: Elements of Reusable Object-Oriented Software》(設計模式:可復用的面向對象軟件元素)。這本書在軟件開發領域具有里程碑式的地位,對面向對象設計產生了深遠影響。
? ? ? ? GoF提出了23種設計模式,將它們分為三大類:
1、創建型模式(Creational Patterns):這類模式主要關注對象的創建過程。它們分別是:
- 單例模式(Singleton)
- 工廠方法模式(Factory Method)
- 抽象工廠模式(Abstract Factory)
- 建造者模式(Builder)
- 原型模式(Prototype)
2、結構型模式(Structural Patterns):這類模式主要關注類和對象之間的組合。分別是:
- 適配器模式(Adapter)
- 橋接模式(Bridge)
- 組合模式(Composite)
- 裝飾模式(Decorator)
- 外觀模式(Facade)
- 享元模式(Flyweight)
- 代理模式(Proxy)
3、行為型模式(Behavior Patterns):這類模式主要關注對象之間的通信。它們分別是:
- 職責鏈模式(Chain of Responsibility)
- 命令模式(Command)
- 解釋器模式(Interpreter)
- 迭代器模式(Iterator)
- 中介者模式(Mediator)
- 備忘錄模式(Memento)
- 觀察者模式(Observer)
- 狀態模式(State)
- 策略模式(Strategy)
- 模板方法模式(Template Method)
- 訪問者模式(Visitor)
? ? ? ? 這些設計模式為面向對象軟件設計提供了一套可復用的解決方案。掌握和理解這些模式有助于提高軟件開發人員的編程技巧和設計能力。
????????
一、單例設計模式
? ? ? ?單例設計模式(Singleton Design Pattern)理解起來非常簡單。一個類只允許創建一個對象(或者實例),那這個類就是單例類,這種設計模式就叫單例設計模式,簡稱單例模式。
? ? ? ? 1、為什么要使用單例
? ? ? ? 1.1 表示全局唯一?
? ? ? ? 如果有些數據在系統種應該且有只能保存一份,那就應該設計為單例類。如:
? ? ? ? 配置類:在系統中,我們只有一個配置文件,當配置文件被加載到內存之后,應該被映射為一個唯一的【配置實例】,此時就可以使用單例,當然也可以不用。
? ? ? ? 全局計數器:我們使用一個全局的計數器進行數據統計、生成全局遞增ID等功能。若計數器不唯一,很有可能產生統計無效,ID重復等。
public class GlobalCounter {private AtomicLong atomicLong = new AtomicLong(0);private static final GlobalCounter instance = new GlobalCounter();// 私有化無參構造器private GlobalCounter() {}public static GlobalCounter getInstance() {return instance;}public long getId() {return atomicLong.incrementAndGet();}}// 查看當前的統計數量long courrentNumber = GlobalCounter.getInstance().getId();
? ? ? ? 以上代碼也可以實現全局ID生成器的代碼。
????????
? ? ? ? 1.2 處理資源訪問沖突
? ? ? ? 如果讓我們設計一個日志輸出的功能。如下:
public class Logger {private String basePath = "D://info.log";private FileWriter writer;public Logger() {File file = new File(basePath);try {writer = new FileWriter(file, true); //true表示追加寫入} catch (IOException e) {throw new RuntimeException(e);}}public void log(String message) {try {writer.write(message);} catch (IOException e) {throw new RuntimeException(e);}}public void setBasePath(String basePath) {this.basePath = basePath;}}
? ? ? ? 我們可能會這樣使用:
@RestController("user")public class UserController {public Result login(){
// 登錄成功Logger logger = new Logger();logger.log("tom logged in successfully.");
// ...return new Result();}}
? ? ? ? 這樣寫會產生如下的問題:多個logger實例,在多個線程中,同時操作同一個文件。就可能產生相互覆蓋的問題。因為tomcat處理每一個請求都會使用一個新的線程。此時日志文件就成了一個共享資源,但凡是多線程訪問共享資源,我們都要考慮并發修改產生的問題。
? ? ? ? 時間處理的方法有很多,其中之一就是可以加鎖:
- 如果使用單個實例輸出日志,鎖【this】即可。
- 如果要保證JVM級別防止日志文件訪問沖突,鎖【class】即可。
- 如果要保證集群服務級別的防止日志文件訪問沖突,加分布式鎖即可。
? ? ? ? 如果我們是一個簡單工程,對日志輸入要求不高。單例模式的解決思路就十分合適,既然同一個Logger無法并行輸出到一個文件中,那么針對這個日志文件創建多個logger實例也就失去了意義,如果工程要求我們所有的日志輸出到同一個日志文件中,這樣其實并不需要創建大量的Logger實例,這樣的好處有:
- 一方面節省內存空間。
- 另一方面節省系統文件句柄(對于操作系統來說,文件句柄也是一種資源,不能隨便浪費)。
?????????按照這個設計思路,實現Logger單例類。具體代碼如下所示:
public class Logger {private String basePath = "D://log/";private static Logger instance = new Logger();private FileWriter writer;private Logger() {File file = new File(basePath);try {writer = new FileWriter(file, true); //true表示追加寫入} catch (IOException e) {throw new RuntimeException(e);}}public static Logger getInstance(){return instance;}public void log(String message) {try {writer.write(message);} catch (IOException e) {throw new RuntimeException(e);}}public void setBasePath(String basePath) {this.basePath = basePath;}}
? ? ? ? 除此之外,并發隊列(比如Java中的BlockingQueue)也可以解決這個問題:多個線程同時往并發隊列里寫日志,一個單獨的線程負責將并發隊列中的數據寫入到日志文件。這種方式實現起來也稍微有點復雜。當然,我們還可將其延申至消息隊列處理分布式系統的日志。
? ? ? ?2、如何實現一個單例
? ? ? ? 常見的單例設計模式,有如下五種寫法,在編寫單例代碼的時候要注意以下幾點:
? ? ? ? 1)構造器需要私有化。
? ? ? ? 2)暴露一個公共的獲取單例對象的接口
? ? ? ? 3)是否支持懶加載(延遲加載)
? ? ? ? 4)是否線程安全
? ? ? ? 2.1 餓漢式
? ? ? ? 餓漢式的實現方式比較簡單。在類加載的時候,在instance 靜態實例就已經創建并初始化好了,所以,instance實例的創建過程是線程安全的。從名字中我們也可以看出這一點。具體的代碼如下所示:
public class EagerSingleton {private static Singleton instance = new Singleton();private Singleton (){}public static Singleton getInstance() {return instance;}}
? ? ? ? 事實上,餓漢式的寫法在工作上反而應該被提倡,面試中不問,只是因為它簡單。很多人覺得餓漢式不能支持懶加載,即使不使用也會浪費資源,一方面是內存資源,一方面會增加初始化的開銷。
? ? ? ? 1、現代計算機不缺這一個對象的內存
? ? ? ? 2、如果一個實例初始化的過程復雜那更加應該放在啟動時處理,避免卡頓或者構造問題發生在運行時,滿足fail-fast 的設計原則。
? ? ? ? 2.2 懶漢式
? ? ? ? 有餓漢式,對應的,就有懶漢式。懶漢式相對于餓漢式的優勢是支持延遲加載,具體的代碼實現如下所示:
public class LazySingleton {private static Singleton instance;private Singleton (){}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}}
? ? ? ? 以上的寫法本質上是有問題,當面對大量并發請求時,其實是無法保證其單例的特點的,很有可能會超過一個線程同時執行了 new Singleton();
? ? ? ? 當然解決它的方案也很簡單,加鎖唄:
public class Singleton {private static Singleton instance;private Singleton (){}public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}}
? ? ? ? 以上的寫法確實可以保證jvm中有且僅有一個單例實例存在,但是方法上加鎖會極大的降低獲取單例對象的并發度。同一時間只有一個線程可以獲取單例對象,為了解決以上的方案就有第三種寫法。
? ? ? ? 2.3 雙重檢查鎖
????????餓漢式不支持延遲加載,懶漢式有性能問題,不支持高并發。那我們再來看一種既支持延遲加載、又支持高并發的單例實現方式,也就是雙重檢測實現方式:
? ? ? ? 在這種實現方式中,只要instance被創建之后,即便再調用 getInstance() 函數也不會再進入到加鎖邏輯中了。所以,這種實現方式解決了懶漢式并發度低的問題。具體的代碼實現如下所示:
public class DclSingleton {// volatile如果不加可能會出現半初始化的對象// 現在用的高版本的 Java 已經在 JDK 內部實現中解決了這個問題(解決的方法很簡單,只要把對象 new 操作和初始化操作設計為原子操作,就自然能禁止重排序),為了兼容性我們加上private volatile static Singleton singleton;private Singleton (){}public static Singleton getInstance() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();}}}return singleton;}}
? ? ? ? 2.4 靜態內部類
? ? ? ? 我們再來看一種比雙重檢測更加簡單的實現方法,那就是利用Java的靜態內部類。它們有點類似餓漢式,但又能做到了延遲加載。代碼實現:
public class InnerSingleton {/** 私有化構造器 */private Singleton() {}/** 對外提供公共的訪問方法 */public static Singleton getInstance() {return SingletonHolder.INSTANCE;}/** 寫一個靜態內部類,里面實例化外部類 */private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}}
? ? ? ? SingletonHolder 是一個靜態內部類,當外部為Singleton 被加載的時候,并不會創建SingletonHolder 實例對象。只有當調用 getInstance() 方法時,SingleHolder 才會被加載,這個時候才會創建 instance 。insance 唯一性、創建過程的線程安全性,都有jvm來保證。所以,這種實現方法既保證了線程安全,又能做到延遲加載。
????????
????????
? ? ? ? 2.5 枚舉
? ? ? ? 最后介紹一種最簡單的實現方式,基于枚舉類型的單例實現。這種實現方式通過java枚舉類型本身的特性,保證了實例創建的線程安全性和實例的唯一性。具體的代碼如下所示:
? ? ? ? 這是最簡單的實現,因為枚舉類中,每一額枚舉項本身就是一個單例的:
public enum EnumSingleton {INSTANCE;}
? ? ? ? 更通用的寫法如下:
public class EnumSingleton {private Singleton(){}public static enum SingletonEnum {EnumSingleton;private EnumSingleton instance = null;private SingletonEnum(){instance = new Singleton();}public EnumSingleton getInstance(){return instance;}}}
? ? ? ? 事實上我們還可以將單例項作為枚舉的成員變量,我們的累加器可以這樣編寫:
public enum GlobalCounter {INSTANCE;private AtomicLong atomicLong = new AtomicLong(0);public long getNumber() {return atomicLong.incrementAndGet();}}
? ? ? ? 這種寫法是Head-first 中推薦的寫法,他除了可以和其他的方式一樣實現單例,他還能有效的防止反射入侵。
? ? ? ? 2.6 反射入侵
? ? ? ? 事實上,想要阻止其他人構造實例僅僅私有化構造器還是不夠的,因為我們還可以使用反射獲取私有構造器進行構造,當然使用枚舉的方式是可以解決這個問題的,對于其他的書寫方案,我們通過下邊的方式解決:
public class Singleton {private volatile static Singleton singleton;private Singleton (){if(singleton != null)throw new RuntimeException("實例:【"+ this.getClass().getName() + "】已經存在,該實例只允許實例化一次");}public static Singleton getInstance() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();}}}return singleton;}}
? ? ? ? 此時方法如下:
@Testpublic void testReflect() throws NoSuchMethodException,InvocationTargetException, InstantiationException, IllegalAccessException {Class<DclSingleton> clazz = DclSingleton.class;Constructor<DclSingleton> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);boolean flag = DclSingleton.getInstance() == constructor.newInstance();log.info("flag -> {}",flag);}
? ? ? ? 結果如下:
? ? ? ? 2.7 序列化與反序列化安全
? ? ? ? 事實上,到目前為止,我們的單例依然是由漏洞的,看如下代碼:
@Testpublic void testSerialize() throws IllegalAccessException,NoSuchMethodException, IOException, ClassNotFoundException {// 獲取單例并序列化Singleton singleton = Singleton.getInstance();FileOutputStream fout = new FileOutputStream("D://singleton.txt");ObjectOutputStream out = new ObjectOutputStream(fout);out.writeObject(singleton);// 將實例反序列化出來FileInputStream fin = new FileInputStream("D://singleton.txt");ObjectInputStream in = new ObjectInputStream(fin);Object o = in.readObject();log.info("他們是同一個實例嗎?{}",o == singleton);}
? ? ? ? 我們發現,即使我們廢了九牛二虎之力還是沒能阻止他返回false,結果如下:
? ? ? ? ?readResolve()方法可以用于替換從流中讀取的對象,在進行反序列化時,會嘗試執行readResolve()方法,并將返回值作為反序列化的結果,而不會克隆一個新的實例,保證jvm中僅僅有一個實例存在:
public class Singleton implements Serializable {// 省略其他的內容public static Singleton getInstance() {}// 需要加這么一個方法public Object readResolve(){return singleton;}}
? ? ? ? 3、源碼應用?
? ? ? ? 事實上、我們在JDK或者其他的通用框架中很少能看到標準的單例設計模式,這也就意味著他確實很經典,但嚴格的單例設計模式確實有它的問題和局限性,我們先看看在源碼中的一些案例。
? ? ? ? 3.1 jdk中的單例
? ? ? ? jdk中有一個類是一個標準單例模式 -> Runtime類,該類封裝了運行時的環境,每個Java應用程序都有一個Runtime類實例,使應用程序能夠與其運行的環境相連接。一般不能實例化一個Runtime 對象,應用程序也不能創建自己的Runtime 類實例。但可以通過getRuntime 方法獲取當前Runtime 運行時對象的引用。
public class Runtime {// 典型的餓漢式private static final Runtime currentRuntime = new Runtime();private static Version version;public static Runtime getRuntime() {return currentRuntime;}/** Don't let anyone else instantiate this class */private Runtime() {}public void exit(int status) {@SuppressWarnings("removal")SecurityManager security = System.getSecurityManager();if (security != null) {security.checkExit(status);}Shutdown.exit(status);}public Process exec(String command) throws IOException {return exec(command, null, null);}public native long freeMemory();public native long maxMemory();public native void gc();}
? ? ? ? 測試用例:
@Testpublic void testRunTime() throws IOException {Runtime runtime = Runtime.getRuntime();Process exec = runtime.exec("ping 127.0.0.1");InputStream inputStream = exec.getInputStream();byte[] buffer = new byte[1024];int len;while ((len = inputStream.read(buffer)) > 0 ){System.out.println(new String(buffer,0,len, Charset.forName("GBK")));}long maxMemory = runtime.maxMemory();log.info("maxMemory-->{}", maxMemory);}
? ? ? ?3.2 Mybatis 中的單例
? ? ? ? Mybatis 中的org.apache.ibatis.io.VFS? 使用到了單例模式。VFS就是Virtual File System的意思,mybatis 通過VFS來查找指定路徑下的資源。查看VFS一級它的實現類,不難發現,VFS的角色就是對更“底層”的查找指定資源的方法的封裝,將復雜的“底層”操作封裝到易于使用的高層模塊中,方便使用者使用。
? ? ? ? 省略了和單例無關的其他代碼,并思考它使用了哪一種形式的單例:
public class public abstract class VFS {// 使用了內部類private static class VFSHolder {static final VFS INSTANCE = createVFS();@SuppressWarnings("unchecked")static VFS createVFS() {
// ...省略創建過程return vfs;}}public static VFS getInstance() {return VFSHolder.INSTANCE;}}
????????
@Testpublic void testVfs() throws IOException {DefaultVFS defaultVFS = new DefaultVFS();
// 1、加載classpath下的文件List<String> list = defaultVFS.list("com/ydlclass");log.info("list --> {}" ,list);
// 2、加載jar包中的資源list = defaultVFS.list(new URL("file://D:/software/repository/com/mysql/mysqlconnector-j/8.0.32/mysql-connector-j-8.0.32.jar"),"com/mysql/cj/jdbc" );log.info("list --> {}" ,list);}
? ? ? ? 4、單例存在的問題
? ? ? ? 盡管單例是一個很經典的設計模式,但在實際的開發中,我們也很少按照嚴格的定義去使用它,以上的知識大多似乎為了理解個面試而使用和學習,有些人甚至認為單例是一種反模式(ant-pattern),壓陣就不推薦使用。
? ? ? ? 大部分情況下,我們在項目中使用單例,都是用它來表示一些全局唯一類,比如配置信息類、連接池類、ID生成器類。單例模式書寫簡潔、使用方便,在代碼中,我們不需要創建對象。但是,這種使用方法有點類似硬編碼(hard code),會帶來諸多問題,所以我們一般會使用spring的單例容器作為替代方案。那單例究竟存在哪些問題呢?
? ? ? ? 4.1 無法支持面向對象編程
? ? ? ? OOP的三大特性是封裝、繼承、多態。單例將構造私有化,直接導致的結果就是無法成為其他類的父類,這就相當于直接放棄了繼承和多態的特性,相當于損失了可以應對未來需求變化的擴展性,以后一旦有擴展需求,比如寫一個類似的具有絕大部分相同功能的單例,我們不得不新建一個雷同的單例。
? ? ? ? 4.2 極難的橫向擴展
? ? ? ? 單例類只能有一個對象實例。如果未來某一天,一個實例無法滿足現在的需求,當需要創建多個實例時,就必須對源代碼進行修改,無法友好的擴展。
? ? ? ? 例如,在系統設計初期,我們覺得應該有一個數據庫連接池,這樣能方便我們控制對數據庫連接資源的消耗,所以我們把數據庫連接池類設置成了單例類。但之后我們發現,系統中有些sql運行得非常慢。這些sql語句在執行得時候,長時間占用數據庫連接池連接資源,導致其他sql請求無法響應。為了解決這個問題,我們希望將慢sql與其他sql隔離開來執行。為了實現這樣的目的,我們可以在系統中創建兩個數據庫連接池,慢sql獨享一個數據庫連接池,其他sql獨享另外一個數據庫連接池,這樣就能避免慢sql影響到其他sql的執行。
????????如果我們將數據庫連接池設計成單例類,顯然就無法適應這樣的需求變更,也就是說,單例類在某些情況下會影響代碼的擴展性、靈活性。所以,數據庫連接池、線程池這類的資源池,最好還是不要設計成單例類。實際上,一些開源的數據庫連接池、線程池也確實沒有設計成單例類。
? ? ? ? 5、不同作用范圍的單例
? ? ? ? 首先看一下單例的定義:“一個類只允許創建唯一一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫做單例設計模式,簡稱單例模式”。
? ? ? ? 定義中提到,“一個類只允許創建唯一 一個對象”。那對象的唯一性的作用范圍是什么呢?在標準的單例設計模式中,其單例是進程唯一的,也就意味著一個項目啟動,在其整個運行環境中只能有一個實例。
? ? ? ? 事實上,在實際的工作當中,我們能夠看到極多(只有一個實例的情況),但是大多并不是標準的單例設計模式,如:
? ? ? ? 1)使用ThreadLocal 實現的線程級別的單一實例
? ? ? ? 2)使用spring實現的容器級別的單一是實例。
? ? ? ? 3)使用分布式鎖實現的集群狀態的唯一實例。
? ? ? ? 以上的情況都不是標準的單例設計模式,但我們可以將其看做單例設計模式的擴展,我們以前兩種情況為例進行介紹。
? ? ? ? 5.1 線程級別的單例
? ? ? ? 剛才說了單例類對象是進程唯一的,一個進程只能有一個單例對象。如何實現一個線程唯一的單例呢?
? ? ? ? 如果在不允許使用ThreadLocal 的時候我們可能想到如下的解決方案,定義一個全局的線程安全的ConcurrentHashMap,以線程id為key,以實例為value,每個線程的存取都從共享的map中進行操作,代碼如下:
public class Connection {private static final ConcurrentHashMap<Long, Connection> instances= new ConcurrentHashMap<>();private Connection() {}public static Connection getInstance() {Long currentThreadId = Thread.currentThread().getId();instances.putIfAbsent(currentThreadId, new Connection());return instances.get(currentThreadId);}}
? ? ? ? 事實上ThreadLocal的原理也大致如此:
? ? ? ? 項目中的ThreadLocal的使用場景:
? ? ? ? 1)在spring使用ThreadLcoal對當前線程和一個連接資源進行綁定,實現事務管理:
public abstract class TransactionSynchronizationManager {// 本地線程中保存了當前的連接資源,key(datasource)--> value(connection)private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");// 保存了當前線程的事務同步器private static final ThreadLocal<Set<TransactionSynchronization>>synchronizations = new NamedThreadLocal<>("Transaction synchronizations");// 保存了當前線程的事務名稱private static final ThreadLocal<String> currentTransactionName =new NamedThreadLocal<>("Current transaction name");// 保存了當前線程的事務是否只讀private static final ThreadLocal<Boolean> currentTransactionReadOnly =new NamedThreadLocal<>("Current transaction read-only status");// 保存了當前線程的事務隔離級別private static final ThreadLocal<Integer> currentTransactionIsolationLevel =new NamedThreadLocal<>("Current transaction isolation level");// 保存了當前線程的事務的活躍狀態private static final ThreadLocal<Boolean> actualTransactionActive =new NamedThreadLocal<>("Actual transaction active");}
? ? ? ? 2)在spring中使用RequestContextHolder ,可以再一個線程中輕松的獲取request、response和session。如果將我們在靜態方法,切面中想獲取一個request 對象就可以使用這個類。
public abstract class RequestContextHolder {private static final ThreadLocal<RequestAttributes> requestAttributesHolder =new NamedThreadLocal("Request attributes");private static final ThreadLocal<RequestAttributes>inheritableRequestAttributesHolder = newNamedInheritableThreadLocal("Request context");@Nullablepublic static RequestAttributes getRequestAttributes() {RequestAttributes attributes =(RequestAttributes)requestAttributesHolder.get();if (attributes == null) {attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();}return attributes;}}
????????ServletRequestAttributes:
public class ServletRequestAttributes extends AbstractRequestAttributes {public static final String DESTRUCTION_CALLBACK_NAME_PREFIX =ServletRequestAttributes.class.getName() + ".DESTRUCTION_CALLBACK.";protected static final Set<Class<?>> immutableValueTypes = new HashSet(16);private final HttpServletRequest request;@Nullableprivate HttpServletResponse response;@Nullableprivate volatile HttpSession session;private final Map<String, Object> sessionAttributesToUpdate;}
public abstract class PageMethod {protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();protected static boolean DEFAULT_COUNT = true;}
? ? ? ? 5.2 容器范圍的單例
? ? ? ? 有時候我們將單例的作用范圍由進程切換到一個容器,可能會更加方便進行單例對象的管理。這也是spring作為java生態大哥大核心思想。spring通過提供一個單例容器,來確保一個實例在容器級別單例,并且可以在容器啟動時完成初始化,它的優勢如下:
? ? ? ? 1)所有的bean 以單例形式存在于容器中,避免大量的對象被創建,造成jvm內存抖動嚴重,頻繁gc
? ? ? ? 2)程序啟動時,初始化單例bean,滿足fast-fail,將所有構建過程的異常暴露在啟動時,而非運行時。更加安全。
? ? ? ? 3)緩存了所有單例bean,啟動的過程相當于預熱的過程,運行時不必進行對象創建,效率更高。
? ? ? ? 4)容器管理bean的生命周期,結合依賴注入使得解耦更加徹底、擴展性無敵。
? ? ? ?
????????5.3 日志中的多例
? ? ? ? 在日志框架中,我們可以通過LoggerFactory.getLogger("ydl")方法獲取一個實例:
@Testpublic void testLogger(){Logger ydl = LoggerFactory.getLogger("ydl");Logger ydl2 = LoggerFactory.getLogger("ydl");Logger ydlclass = LoggerFactory.getLogger("ydlclass");log.info("ydl == ydl2 -->{}", ydl == ydl2);log.info("ydl == ydlclass --> {}", ydl == ydlclass);}
? ? ? ? 其結果如下:
? ? ? ? 我們發現,如果我們使用相同的名字,它會返回同一個實例,否則就是另一個實例,這其實就是一個多例,一個類可以創建多個對象,但是個數是有限制的,他可是是具體約定好的個數,比如5,也可以按照類型的個數創建。
????????這種多例模式有點類似工廠模式。它跟工廠模式的不同之處是,多例模式創建的對象都是同一個類的對象,而工廠模式創建的是不同子類的對象。實際上,它還有點類似享元模式,兩者的區別等到我們講到享元模式的時候再來分析。除此之外,實際上,枚舉類型也相當于多例模式,一個類型只能對應一個對象,一個類可以創建多個對象。
二、工廠設計模式
? ? ? ? 一般情況下,工廠模式分為三種更加細分的類型:簡單工廠、工廠方法和抽象工廠。
? ? ? ? 在GoF的《設計模式》一書中,它將簡單工廠模式看作是工廠方法模式的一種特例,所以工廠模式只被分成了工廠方法和抽象工廠兩類。實際上,前面一種分類方法更加常見。
? ? ? ? 在這三種細分的工廠模式中,簡單工廠、工廠方法原理比較簡單,在實際的項目中也比較常用。而抽象工廠的原理稍微復雜點,在實際的項目中相對也不常用。
? ? ? ? 1、如何實現工廠模式
? ? ? ? 1)簡單工廠(Simple Factory)
? ? ? ? 簡單工廠叫做靜態工廠方法模式(Static Factory Method Pattern).學習此設計模式時,我們會從一個案例不斷優化帶著大家領略工廠設計模式的魅力。
? ? ? ? 現在有一個場景,我們需要一個資源加載器,他要根據不同的url進行資源加載,但是如果我們將所有的加載實現代碼全部封裝在了一個load方法中,就會導致一個類很大,同時擴展性也很查,但想要添加新的前綴解析其他類型的url時,發現需要修改大量的源代碼,我們的代碼如下:
? ? ? ? 定義兩個需要之后會用到的類,非常簡單:
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Resource {private String url;
}
public class ResourceLoadException extends RuntimeException{public ResourceLoadException() {super("加載資源是發生問題。");}public ResourceLoadException(String message) {super(message);}
}
? ? ? ? 源碼如下:
public class ResourceLoader {public Resource load(String filePath) {String prefix = getResourcePrefix(filePath);Resource resource = null;if("http".equals(type)){
// ..發起請求下載資源... 可能很復雜return new Resource(url);} else if ("file".equals(type)) {
// ..建立流,做異常處理等等return new Resource(url);} else if ("classpath".equals(type)) {
// ...return new Resource(url);} else {return new Resource("default");}return resource;}private String getPrefix(String url) {if(url == null || "".equals(url) || !url.contains(":")){throw new ResourceLoadException("此資源url不合法.");}String[] split = url.split(":");return split[0];}
}
? ? ? ? 上邊的案例,存在很多的if分支,如果分支數量不多,且不需要擴展,這壓根的編寫方式當然沒錯,然而在實際的工作場景中,我們的業務代碼可能會很多,分支邏輯也可能十分復雜,這個時候簡單工廠設計模式就要發揮作用了。
? ? ? ? 我們只需要創建一個工廠類,將創建資源的能力交給工廠即可:
public class ResourceFactory {public static Resource create(String type,String url){if("http".equals(type)){
// ..發起請求下載資源... 可能很復雜return new Resource(url);} else if ("file".equals(type)) {
// ..建立流,做異常處理等等return new Resource(url);} else if ("classpath".equals(type)) {
// ...return new Resource(url);} else {return new Resource("default");}}
}
? ? ? ? 有了上面的工廠類之后,我們的住喲啊邏輯就會簡化:
public class ResourceLoader {public Resource load(String url){
// 1、根據url獲取前綴String prefix = getPrefix(url);
// 2、根據前綴處理不同的資源return ResourceFactory.create(prefix,url);}private String getPrefix(String url) {if(url == null || "".equals(url) || !url.contains(":")){throw new ResourceLoadException("此資源url不合法.");}String[] split = url.split(":");return split[0];}
}
? ? ? ? 這就是簡單工廠設計模式,提取一個工廠類,工廠會根據傳入的不同的類型,創建不同的產品。好處如下:
? ? ? ? 將創建對象的過程交給工廠類、其他業務需要某個產品時,直接使用create(方法名不重要)創建即可這樣的好處時:
? ? ? ? 1)工廠將創建的過程進行封裝,不需要關系創建的細節,更加符合面向對象思想
? ? ? ? 2)這樣主要業務邏輯不會被創建對象的代碼干擾,代碼更易閱讀
? ? ? ? 3)產品的創建可以獨立測試,更將容易測試
? ? ? ? 4)獨立的工廠類只負責創建產品,更加符合單一原則
????????絕大部分工廠類都是以“Factory”單詞結尾,但也不是必須的,比如 Java 中的DateFormat、Calender。除此之外,工廠類中創建對象的方法一般都是 create 開頭,比如代碼中的 createParser(),但有的也命名為 getInstance()、createInstance()、newInstance(),有的甚至命名為 valueOf()(比如 Java String 類的 valueOf() 函數)等等,這個我們根據具體的場景和習慣來命名就好。
? ? ? ? 2)工廠方法(Factory Method)
? ? ? ? 如果有一天,我們if分支邏輯不斷膨脹,有變為腫瘤代碼的可能,就有必要將if分支邏輯去掉,那又該怎么辦呢?比較經典的處理方法就是利用多態。按照多態的實現思路,對上面的代碼進行重構。我們會為每一個Resource 創建一個獨立的工廠類,形成一個個小作坊,將每一個實例的創建過程交給工廠類完成,重構之后的代碼如下所示:
? ? ? ? 我們將生產資源的工廠類進行抽象:
public interface IResourceLoader {Resource load(String url);
}
? ? ? ? 并為每一種資源創建與之匹配的實現:
public class ClassPathResourceLoader implements IResourceLoader {@Overridepublic Resource load(String url) {
// 中間省略復雜的創建過程return new Resource(url);}
}
public class FileResourceLoader implements IResourceLoader {@Overridepublic Resource load(String url) {
// 中間省略復雜的創建過程return new Resource(url);}
}
public class HttpResourceLoader implements IResourceLoader {@Overridepublic Resource load(String url) {
// 中間省略復雜的創建過程return new Resource(url);}
}
public class FtpResourceLoader implements IResourceLoader {@Overridepublic Resource load(String url) {
// 中間省略復雜的創建過程return new Resource(url);}
}
public class DefaultResourceLoader implements IResourceLoader {@Overridepublic Resource load(String url) {
// 中間省略復雜的創建過程return new Resource(url);}
}
? ? ? ? 這就是共工廠方法模式的典型代碼實現。這樣當我們新增一種讀取資源的方式時,只需要新增一個實現,并實現IResourceLoader 接口即可。所以,工廠方式模式比起簡單工廠模式更加符合開閉原則。
? ? ? ?也就是說,我們有很多資源需要加載,可能是各種各樣的加載方式,比如http,比如file文件,因為加載的方式不同,所以我們要寫不同的方法來加載,工廠模式的意思就是說,我們先出一個接口,里面有一個方法就是加載資源的方法?load ,然后你加載的資源不一樣,就根據這個接口實現不同的加載方法,也就是將每個方法隔離開。如果后面要加載不同的資源,就實現接口,重新寫不同的加載方法。
? ? ? ? 上面的方法實現完之后,我們就可以將邏輯改成直接調用對應的工廠實現類了:
public class ResourceLoader {public Resource load(String url){// 1、根據url獲取前綴String prefix = getPrefix(url);ResourceLoader resourceLoader = null;// 2、根據前綴選擇不同的工廠,生產獨自的產品// 版本一if("http".equals(prefix)){resourceLoader = new HttpResourceLoader();} else if ("file".equals(prefix)) {resourceLoader = new FileResourceLoader();} else if ("classpath".equals(prefix)) {resourceLoader = new ClassPathResourceLoader()} else {resourceLoader = new DefaultResourceLoader();}return resourceLoader.load(url);}private String getResourcePrefix(String filePath) {if (filePath == null || "".equals(filePath)) {throw new RuntimeException("The file path is illegal");}filePath = filePath.trim().toLowerCase();String[] split = filePath.split(":");if (split.length > 1) {return split[0];} else {return "classpath";}}
}
? ? ? ? 其實上面的代碼還比較復雜,我們可以將工廠方法的實現類加入到緩存中,簡化調用邏輯:
? ? ? ? 例如一:將實現類放入map集合:
private static Map<String,IResourceLoader> resourceLoaderCache = newHashMap<>(8);
// 版本二static {resourceLoaderCache.put("http",new HttpResourceLoader());resourceLoaderCache.put("file",new FileResourceLoader());resourceLoaderCache.put("classpath",new ClassPathResourceLoader());resourceLoaderCache.put("default",new DefaultResourceLoader());}
? ? ? ? 然后調用邏輯變成如下所示:根據map集合調用,獲取對應實現類。
public Resource load(String url){// 1、根據url獲取前綴String prefix = getPrefix(url);return resourceLoaderCache.get(prefix).load(url);}
? ? ? ? 當然也可以將這些實現類放入在配置類中,在配置類中進行加載:
http=com.ydlclass.factoryMethod.resourceFactory.impl.HttpResourceLoader
file=com.ydlclass.factoryMethod.resourceFactory.impl.FileResourceLoader
classpath=com.ydlclass.factoryMethod.resourceFactory.impl.ClassPathResourceLoader
default=com.ydlclass.factoryMethod.resourceFactory.impl.DefaultResourceLoader
? ? ? ? 這樣就可以在static中這樣編碼:通過加載文件,來加載對應實現類,然后如果有新的實現類,修改對應文件即可。
static {InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("resourceLoader.properties");Properties properties = new Properties();try {properties.load(inputStream);for (Map.Entry<Object,Object> entry : properties.entrySet()){String key = entry.getKey().toString();Class<?> clazz = Class.forName(entry.getValue().toString());IResourceLoader loader = (IResourceLoader)clazz.getConstructor().newInstance();resourceLoaderCache.put(key,loader);}} catch (IOException | ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {throw new RuntimeException(e);}}
? ? ? ? 上面工廠方法的實現邏輯,實際上我們工廠方法一般會生成一個對應的產品,我們一般會將整個產品線也進行抽象:
public abstract class AbstractResource {private String url;public AbstractResource(){}public AbstractResource(String url) {this.url = url;}protected void shared(){System.out.println("這是共享方法");}/*** 每個子類需要獨自實現的方法* @return 字節流*/public abstract InputStream getInputStream();
}
? ? ? ? 具體的產品需要繼承這個抽象類,實現自己的產品邏輯:
public class ClasspathResource extends AbstractResource {public ClasspathResource() {}public ClasspathResource(String url) {super(url);}@Overridepublic InputStream getInputStream() {return null;}
}
? ? ? ? 然后我們修改對應的工廠方法,變成在工廠中生成對應的產品:
public class ClassPathResourceLoader implements IResourceLoader {@Overridepublic AbstractResource load(String url) {// 中間省略復雜的創建過程return new ClasspathResource(url);}
}
? ? ? ? 這樣調用方法就變成了:
@Testpublic void testFactoryMethod(){String url = "file://D://a.txt";ResourceLoader resourceLoader = new ResourceLoader();AbstractResource resource = resourceLoader.load(url);log.info("resource --> {}",resource.getClass().getName());}
? ? ? ? 梳理一下邏輯:
? ? ? ? 首先有兩個部分,一個工廠方法,一個產品方法,這樣個方法都有自己的抽象類也就是接口,其中工廠方法中會調用對應的產品方法,但是工廠方法的返回值是產品的抽象類,這就意味著工廠方法沒有直接跟對應的產品方法實例進行依賴,而是依賴的產品方法的抽象類。每增加一個產品,一個工廠,只需要實現各自對應的接口即可。
? ? ? ? 3)抽象工廠(Abstract Factory)