GOF23--23種設計模式(一)

回憶也交給時間發表於2023-12-04

一.什麼是設計模式

設計模式(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的反射機制使得構造器無法真正的私有化

但是如果有能拒絕反射機制的方式,閣下又如何應對呢?接下來的列舉類值得一看

列舉類單例

列舉類的特點:

列舉類的構造器都是私有的(無論是否顯式表達,都是私有的),因此列舉類不能對外建立物件

can't deserialize enum" :不能透過反射拿取列舉類
列舉類直接拒絕反射機制,從根本上杜絕了反射更改構造器屬性為公有的情況
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);

指向如上程式碼報錯:

 意思是不能使用反射建立列舉物件