設計模式(Design Pattern)是前輩們對程式碼開發經驗的總結,是解決一系列特定問題的套路。
它不是語法規定,而是一套用來提高程式碼複用性,可讀性,可維護性,穩健性,安全性的解決方案
設計模式的雛形:
1995年,GOF(Gang of Four,四人/四人幫)合作出版了《設計模式:可複用的物件導向軟體的基礎》一書,共收集了23種設計模式,從此有了設計模式的歷程碑,人稱【GoF設計模式】
設計模式的本質是物件導向設計的實際應用,是對類的封裝,繼承,多型,以及類的關聯,和組合關係的充分理解
使用設計模式有以下優點:
開閉原則:對擴充套件開放,對修改關閉
里氏替換原則:繼承必須確保父類別的所擁有的性質在子類依舊成立
依賴倒置原則:面向介面程式設計,不要面向實現程式設計
單一職責原則:控制類的粒度,將物件解耦,提高其內聚性
介面隔離原則:為各個類建立它們專需的介面
迪米特法則:只與你的朋友交談,不和陌生人說話
合成複用原則:儘量優先使用組合和聚合的方式實現類之間的關係,其次才考慮繼承來實現
注意:OOP的七大原則,多用於設計階段,需要分清設計和實現的區別
實現了建立者和排程者的分離
原來的排程者即是建立者,類就在自己的專案中,且可看原始碼,所以要使用的時候可以直接new出來,這種方式建立物件需要自己十分的瞭解這個類,如需要哪些引數,清楚內部的實現細節
在大型專案的設計中,都是面向介面程式設計,對於排程者,它只知道此介面的內容,和有一些實現類,並不知道實現類的具體細節,如果自己建立物件,很大概率會被抽象介面給整蒙,所以工廠模式出現了,它用於實現物件的建立,建立物件的細節都由工廠模式解決(也就是架構師),普通開發者只用知道自己使用的實現類是那個工廠提供的,然後在工廠內拿取物件,不必自己建立,而只是利用工廠排程
詳細工廠的分類:
理論上,工廠模式滿足:開閉原則,依賴倒轉原則,迪米特法則
但是,實際工作中以效率和業務開發為主,不一定完全滿足,這取決於效率和理論的衝突
工廠模式的核心本質:
範例化物件不在使用new關鍵字,用工廠代替
將選擇實現類,建立實現類物件統一管理和控制,從而將呼叫者和實現類解耦
簡單工廠模式也叫靜態工廠模式,指的是工廠中的程式碼塊都是寫死的,動態的拓展類需要在工廠中新增程式碼塊來完成物件的建立
介面,Animal:
public interface Animal { void getName(); }
通過此介面拓展出的實現類:
public class Cat implements Animal{ @Override public void getName() { System.out.println("貓類,實現Animal介面"); } }
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);
指向如上程式碼報錯:
意思是不能使用反射建立列舉物件