一.什麼是設計模式
設計模式(Design Pattern)是前輩們對程式碼開發經驗的總結,是解決一系列特定問題的套路。
它不是語法規定,而是一套用來提高程式碼複用性,可讀性,可維護性,穩健性,安全性的解決方案
設計模式的雛形:
1995年,GOF(Gang of Four,四人/四人幫)合作出版了《設計模式:可複用的物件導向軟體的基礎》一書,共收集了23種設計模式,從此有了設計模式的歷程碑,人稱【GoF設計模式】
設計模式的本質是物件導向設計的實際應用,是對類的封裝,繼承,多型,以及類的關聯,和組合關係的充分理解
使用設計模式有以下優點:
- 可以提高程式設計師的思維能力,程式設計和設計能力
- 使得程式設計更加標準化,程式碼編制更容易加工,從而縮短軟體開發週期
- 使設計程式碼重用性高,可讀性高,可靠性高,靈活性,可維護性強
OOP的七大原則:
開閉原則:對擴充套件開放,對修改關閉
- 指的是在物件導向的設計中,當有新的需求時,不會優先改變原始碼,而是透過其它方式(繼承,多型等)在原始碼的基礎上擴充新功能
里氏替換原則:繼承必須確保父類的所擁有的性質在子類依舊成立
- 指的是在程式設計中,對於子類繼承父類,子類中父類的屬性和方法都能正常使用,子類需要新的需求就自己寫,不要直接重寫父類的方法,如果為了重寫父類已有的方法而繼承,對於程式的複用性會大打折扣
依賴倒置原則:面向介面程式設計,不要面向實現程式設計
- 指的是在程式設計中,不應該力求於怎麼實現這個功能,應該先思考有那些方法,各自負責什麼,實現的細節交由實現類,抽象功能交給介面,更深層次就是面向抽象程式設計,不要面向實現程式設計
單一職責原則:控制類的粒度,將物件解耦,提高其內聚性
- 指的是一個類就專注於實現好一個功能就行了,就像一個方法就實現一個細節一樣,如使用者登入,想要一個方法負責密碼校對又負責檢測使用者名稱是否存在,就是一個方法幹了多件事,可以把檢測使用者是否存在抽象為另一個方法,然後呼叫它,這樣類的粒度就低了,粒度越高,程式碼越可能出現問題
介面隔離原則:為各個類建立它們專需的介面
- 指的是在程式設計的時候,需要對一個介面對應一個或多個實現類,它們負責的模組可以很小,但是需要專一,不要多個功能都冗餘在一個介面內部,應該實現專一功能,然後可以多個實現類來實現更小的細節
迪米特法則:只與你的朋友交談,不和陌生人說話
- 指的是兩個類需要交流時應該透過一箇中間類,不要讓它們兩個類直接交流,如使用者登入時需要密碼校對(A類的功能),校對前需要進行使用者名稱檢測是否村在(B類的功能),它們之間有耦合關係,但是程式設計中不能將B類塞到A類中,而是需要一個C類,將B類和A類組合,然後實現此功能,好處是,A類B類保持純粹,壞處是多了一個開銷C類
合成複用原則:儘量優先使用組合和聚合的方式實現類之間的關係,其次才考慮繼承來實現
- 指的是在類的關係中多用組合和聚合的方式設計類,組合優於繼承,如果你只想使用父類的方法,而很少或根本不再設計新的方法屬性,就肯定要使用組合,如果是需要大面積更改父類方法,或者重構父類,則使用繼承
注意:OOP的七大原則,多用於設計階段,需要分清設計和實現的區別
二.工廠模式
實現了建立者和排程者的分離
原來的排程者即是建立者,類就在自己的專案中,且可看原始碼,所以要使用的時候可以直接new出來,這種方式建立物件需要自己十分的瞭解這個類,如需要哪些引數,清楚內部的實現細節
在大型專案的設計中,都是面向介面程式設計,對於排程者,它只知道此介面的內容,和有一些實現類,並不知道實現類的具體細節,如果自己建立物件,很大機率會被抽象介面給整蒙,所以工廠模式出現了,它用於實現物件的建立,建立物件的細節都由工廠模式解決(也就是架構師),普通開發者只用知道自己使用的實現類是那個工廠提供的,然後在工廠內拿取物件,不必自己建立,而只是利用工廠排程
詳細工廠的分類:
- 簡單工廠模式
- 工廠方法模式
- 抽象工廠模式
理論上,工廠模式滿足:開閉原則,依賴倒轉原則,迪米特法則
但是,實際工作中以效率和業務開發為主,不一定完全滿足,這取決於效率和理論的衝突
工廠模式的核心本質:
例項化物件不在使用new關鍵字,用工廠代替
將選擇實現類,建立實現類物件統一管理和控制,從而將呼叫者和實現類解耦
簡單工廠模式
簡單工廠模式也叫靜態工廠模式,指的是工廠中的程式碼塊都是寫死的,動態的擴充類需要在工廠中新增程式碼塊來完成物件的建立
介面,Animal:
public interface Animal { void getName(); }
透過此介面擴充出的實現類:
- cat類
public class Cat implements Animal{ @Override public void getName() { System.out.println("貓類,實現Animal介面"); } }
- dog類
public class Dog implements Animal{ @Override public void getName() { System.out.println("狗類,實現Animal類"); } }
普通的建立物件的方式,透過new關鍵字實現:
//普通的建立物件方式 Cat cat = new Cat(); Dog dog = new Dog();
這種方式使用的前提是建立者對類的內部結構要熟悉,清楚需要什麼引數才能建立物件,我們例子的實現類簡單,肯定用new關鍵字很適用,但是這一期主要講工廠模式,所以我們看看工廠模式怎麼建立物件
簡單構造一個簡單工廠來建立物件(重理解):
public class AnimalFactory { public static Animal getAnimal(String name){ if (name.equals("cat")){ return new Cat(); } else if (name.equals("dog")) { return new Dog(); }else { return null; } } }
如上就是普通工廠的寫法,它是講已有的類先寫入工廠中,這就導致工廠的實現類被寫死了,如果新增一個擴充類,就需要改變普通工廠的原始碼,這很顯然不符合開閉原則
工廠模式拿取物件:
//工廠模式建立物件 Animal cat1 = AnimalFactory.getAnimal("cat"); Animal dog1 = AnimalFactory.getAnimal("dog");
新建一個Mouse實現類:
public class Mouse implements Animal { @Override public void getName() { System.out.println("老鼠類,實現於Animal類"); } }
需要改變普通工廠模式的程式碼:
public class AnimalFactory { public static Animal getAnimal(String name){ if (name.equals("cat")){ return new Cat(); } else if (name.equals("dog")) { return new Dog(); }else if (name.equals("mouse")) { //新增的擴充實現類 return new Mouse(); }else { return null; } } }
每次新增擴充類都需要改變普通工廠類的原因:普通工廠是拿取物件的必經之路,是和其它實現類的唯一聯絡
普通工廠模式生產物件略圖:
工廠方法模式
工廠方法模式支援實現類的橫向擴充,它在普通工廠模式的基礎上,增加工廠模式介面,對於每個實現類有專門的介面,
也就是說實現類,實現介面的具體細節,而工廠實現類實現的是工廠模式的建立物件
- 優點是可以橫向擴充業務,不需要改變已經有的工廠模式來融入
- 缺點是程式碼量直接翻倍,冗餘比較大
介面Animal:
public interface Animal { void getName(); }
Animal工廠介面:
public interface AnimalFactory { Animal getAnimal(); }
介面實現類:
public class Cat implements Animal { @Override public void getName() { System.out.println("貓類,實現Animal介面"); } }
工廠介面實現類:
public class CatFactory implements AnimalFactory{ @Override public Animal getAnimal() { return new Cat(); } }
如上,每個實現類都有它專有的工廠實現類,使得每個實現類都是專門的工廠來加工的,它們各個工廠實現類都是獨立存在的互相解耦,所以要建立物件現在就需要去找它們對應的工廠
這樣構建工廠的好處是,橫向的新增業務,如果現在新增一個業務只需要實現類實現Animal介面,它對應的工廠實現工廠介面,和其它工廠是獨立存在的,不需要改變已有的工廠
能實現橫向擴充的關鍵在於,介面和工廠介面都不是關鍵路徑了,而是約束實現類的組成
工廠方法模式建立物件:
//方法工廠模式拿取物件 Animal cat = new CatFactory().getAnimal(); Animal dog = new DogFactory().getAnimal();
工廠方法模式生產物件略圖:
三.抽象工廠模式
抽象工廠模式也是工廠模式的一種,但是它的特點和普通工廠模式,工廠方法模式的機制都是不同的
抽象工廠模式圍繞一個超級工廠,其它工廠的建立都是由這個超級工廠約束的
定義:抽象工廠模式提供了一個建立一系列相關或相互依賴物件的介面,無需指定它們具體的類
優點:
- 具體產品在應用層隔離,無需關心建立細節
- 將一個系列的產品統一到一起實現
缺點:
- 產品簇新增產品困難
- 增加了系統抽象性和理解難度
產品介面:
phone
//產品介面,具體的實現細節交給廠商 public interface PhoneProduct { void getPhoneName(); void getNumber(); void getProduct(); }
router
//產品介面,具體的實現細節交給廠商 public interface RouterProduct { void getRouterName(); void getRouterNumber(); void getRouterProduct(); }
抽象工廠介面,工廠都需要實現此介面:
//抽象工廠,所有的工廠都需要實現這個超級工廠 public interface AbstractFactory { PhoneProduct phone(); RouterProduct router(); }
普通工廠:
XiaoMi:
public class MiFactory implements AbstractFactory{ @Override public PhoneProduct phone() { return new MiPhone(); } @Override public RouterProduct router() { return new MiRouter(); } }
HuaWei:
public class HWFactory implements AbstractFactory{ @Override public PhoneProduct phone() { return new HuaWeiPhone(); } @Override public RouterProduct router() { return new HuaWeiRouter(); } }
抽象工廠模式生產物件略圖:
三種工廠模式總結
簡單工廠模式:雖然某種程度上不符合設計模式,但是實際應用最多
工廠方法模式:不修改已有類的情況下,透過新增工廠實現類的擴充
抽象工廠模式:不可以新增產品,但是可以新增產品簇或者說,不建議修改已經寫好的抽象工廠介面,但是實現抽象工廠介面的普通工廠可以橫向擴充
四.單例模式
單例模式指的是在建立物件的時候,只允許全域性存在一個物件,從而達到資源共享的目的
實現單例模式的方式一共有兩種:
- 餓漢式單例
- 懶漢式單例
餓漢式單例
餓漢式單例的特點是將一個類的構造器私有化,不讓外部的程式手動的建立物件
而這個類的物件則使用靜態方法獲取,由程式載入初始化的時候就開始建立,然後伴隨程式的結束為止
//餓漢式單例模式 public class HungryInstance { //私有化構造器,不允許外部類任意建立物件 private HungryInstance(){ } //建立靜態物件,在類初始化時就被建立物件 private static HungryInstance hungry=new HungryInstance(); //外部類利用方法拿取物件,不由外部類自主建立物件 public static HungryInstance getHungry(){ return hungry; } }
餓漢式單例模式有一個缺點,也就是此類的物件是靜態的,它和程式載入順序有關係,靜態的程式碼塊會和程式初始化一起載入,所以有可能此類如果所需空間很大但是使用不平凡,會白佔很多空間
如我們此類需要申請一片記憶體空間:
private String[] s1=new String[1000]; private String[] s2=new String[1000]; private String[] s3=new String[1000]; private String[] s4=new String[1000];
如上,這片空間會在程式初始化就被佔用,且一直存在到程式結束,如果這個單例本身使用很少,記憶體開銷就很不合算
懶漢式單例
懶漢式單例也需要將構造器私有,避免外部類建立物件
懶漢式不是再使用靜態屬性來建立物件,而是透過方法呼叫,由方法建立
如果沒使用此方法就並不會存在此物件,如果使用了此方法就建立一個物件
然後加一個檢測機制,呼叫此方法時,如果物件存在就直接返回物件,避免建立,如果不存在,則當場建立一個
//懶漢式單例 public class LazyInstance { //私有化構造器,避免外部類建立物件 private LazyInstance(){ } private LazyInstance lazy; //y由呼叫方法建立物件,被呼叫才會被建立,沒被呼叫物件就不存在 public LazyInstance getLazy(){ if (lazy==null){ lazy = new LazyInstance(); return lazy; }else { return lazy; } } }
懶漢式單例也有自己的一個問題,那就是多執行緒的情況下,檢測機制太簡單,單例會被破壞
原因是上面方法建立物件的操作不是原子性,建立物件的過程:1.分配記憶體空間,2.執行構造方法,初始化物件,3.把物件指向這個空間
建立物件的順序是123,132都可能,如果多個執行緒同時來拿物件只有還沒進行到第3步,都會預設沒有物件,但實際情況是已經有執行緒正在建立了,所以就會導致多個執行緒建立了多個物件
解決方式,加鎖(synchronized)
//由呼叫方法建立物件,被呼叫才會被建立,沒被呼叫物件就不存在 public static LazyInstance getLazy() { if (lazy == null) { //加上執行緒同步機制,當物件不存在時將此類資源鎖住 synchronized (LazyInstance.class) { if (lazy == null) { lazy = new LazyInstance(); return lazy; } } } return lazy; }
加上同步機制後,在建立物件時,會將類資源鎖住,先獲得鎖的執行緒就就去建立物件,其它執行緒只能等待此執行緒釋放鎖
當物件建立完成後,其它執行緒先後獲得鎖,但是物件此時已經被最先拿到鎖的執行緒建立了,所以其它執行緒都不能建立物件而是直接返回已經建立好的物件
靜態內部類單例
這是使用了Java靜態內部類的特點,它可以直接拿到外部類的靜態資源,然後又不會直接被初始化載入,它和餓漢式有異曲同工之妙
餓漢式是在程式載入時就初始化一個物件出來,而它需要在被呼叫時才能拿到物件,由於建立物件的類中,又是final修飾,所以在呼叫方法的時候不會多建立物件
//靜態內部類 public class StaticClass { //私有化構造器 private StaticClass(){ } //返回靜態內部類的屬性 public static StaticClass getInstance(){ return InnerClass.sc; } //靜態內部類負責建立外部類的物件 public static class InnerClass{ private static final StaticClass sc = new StaticClass(); } }
上面三種方式的缺點
只要有反射機制存在,以上三種方式建立物件都是不安全的
反射機制使得私有的構造器依舊可以被拿到,反射機制面前就沒有私有的屬性了,我們可以使用反射機制來建立物件
//透過反射拿取類的構造器 Constructor<LazyInstance> lazy = LazyInstance.class.getDeclaredConstructor(null); //設定構造器的熟悉為可訪問 lazy.setAccessible(true); //透過反射拿取構造器建立物件 LazyInstance lazy1 = lazy.newInstance(); LazyInstance lazy2 = lazy.newInstance(); //展示hashcode System.out.println(lazy1);//LazyInstance@4554617c System.out.println(lazy2);//LazyInstance@74a14482
如上,透過反射機制將構造器再次變為公有屬性以後,已經可以透過外部類繼續建立物件
所以這種基於類的單例模式大多都是不安全的,關鍵在於Java的反射機制使得構造器無法真正的私有化
但是如果有能拒絕反射機制的方式,閣下又如何應對呢?接下來的列舉類值得一看
列舉類單例
列舉類的特點:
列舉類的構造器都是私有的(無論是否顯式表達,都是私有的),因此列舉類不能對外建立物件
public enum EnumInstance { //例項物件 Instance; //私有構造器,不管是否顯示私有化都是私有的,改為公有編譯錯誤 private EnumInstance(){ } //拿取物件例項方法 public EnumInstance getInstance(){ return Instance; } }
試試用反射取改變構造器屬性為公有
//列舉的構造器不是無參構造,Idea和JavaP命令都反編譯為無參構造,而真正的構造器為引數為String和int Constructor<EnumInstance> ei = EnumInstance.class.getDeclaredConstructor(String.class, int.class); //設定構造器為公共屬性 ei.setAccessible(true); //透過構造器建立物件 EnumInstance e1 = ei.newInstance(); EnumInstance e2 = ei.newInstance(); //展示hashcode System.out.println(e1); System.out.println(e2);
指向如上程式碼報錯:
意思是不能使用反射建立列舉物件