1. 程式人生 > 實用技巧 >java設計模式--建立型模式

java設計模式--建立型模式

前言

建立型模式:單例模式,抽象工廠模式,原型模式,建造者模式,工廠模式

正文

1.單例模式

單例模式特點:

  1. 保證整個軟體系統中,對某個類只能存在一個物件例項
  2. 該類提供了一個全域性訪問點供外部獲取該例項,其拓展就是有限多例模式
  3. 該類必須由單例類自行建立

優點:

  • 保證記憶體中只有一個例項,減少了記憶體開銷
  • 避免對資源的多重佔用
  • 可以優化和共享資源的訪問

缺點:

  • 單例模式一般沒有介面,擴充套件困難。如果需要擴充套件,則需要修改原來的程式碼,會違背開閉原則
  • 單例模式功能程式碼一般都會寫在一個類中,如果涉及不合理,容易違背單一職責原則

應用場景:

  • 頻繁的建立一些類,使用單例模式可以降低系統壓力,減少GC
  • 對於只需要一個例項的物件
  • 例項建立時間較長,並且經常使用
  • 某些物件需要頻繁的例項化,並且頻繁的銷燬,例如資料庫連線池或者網路連線池
  • 某個物件需要被共享

單例模式的建立方式有很多種,其中懶漢式必須保證多執行緒引起的問題,下面幾種方式都可以保證物件唯一:

  • 餓漢式,靜態常量建立
  • 餓漢式,靜態程式碼塊建立
  • 懶漢式,加鎖建立
  • 懶漢式,雙重檢查建立
  • 懶漢式,靜態內部類
  • 列舉,這個原理可以通過檢視解讀位元組碼進行檢視

餓漢式就是classLoader進行類載入的時候進行建立,這種寫法比較簡單,懶漢式只有在實際用的時候才會建立。列舉也是類載入的時候就會建立

public class Singleton {

    // 1. 餓漢式,靜態常量
    private static Singleton INSTANCE = new Singleton();

    // 2. 餓漢式,靜態程式碼塊
    static {
        INSTANCE = new Singleton();
    }

//    public static Singleton getInstance(){
//        return INSTANCE;
//    }


    // 3. 懶漢式,加鎖建立
//    private static synchronized Singleton getInstance(){
//        if(INSTANCE == null){
//            INSTANCE = new Singleton();
//        }
//        return INSTANCE;
//    }

    // 4.雙重檢查,這個其實是上面的優化,在併發場景下,如果物件已經例項化了,
    // 就不需要在加鎖,加鎖是一個比較重的操作,因此單例模式,很少會是一個執行緒頻繁呼叫,所以一般也不會出現偏向鎖
//    private static Singleton getInstance() {
//        if (INSTANCE == null) {
//            synchronized (Singleton.class) {
//                if (INSTANCE == null) {
//                    INSTANCE = new Singleton();
//                }
//            }
//        }
//        return INSTANCE;
//    }

    // 5. 靜態內部類建立,這個SingleTonFactory裡面的屬性,並不會在Singleton類載入的時候建立
    private static class SingleTonFactory {
        public static Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingleTonFactory.INSTANCE;
    }
}

從下述截圖可以看出對於enum的程式碼的位元組碼進行解析,cinit會在類裝載的時候進行呼叫,其中getstatic看後面引用關係可以這個物件就初始化了

2.工廠模式

定義一個建立產品物件的工廠介面,將產品物件的實際建立工作推遲到具體子工廠類當中。工廠模式有 3 種不同的實現方式,分別是簡單工廠模式、工廠方法模式和抽象工廠模式。

簡單工廠模式

優點:

  • 工廠類中包含必要的建立邏輯,可以決定何時建立產品例項
  • 客戶端無需知道建立具體產品的雷鳴,只需要知道必要的引數即可
  • 可以引入配置檔案,在不修改客戶端程式碼的請魯昂下更換和新增新的具體產品類

缺點:

  • 單一,負責所有產品的建立,職責過重,如果業務複雜,程式碼會比較臃腫,違背高內聚原則
  • 系統擴充套件困難,引入新產品需要修改工廠邏輯,如果產品型別較多,會造成邏輯複雜
  • 使用static工廠方法,工廠角色無法形成基於繼承的等級結構

使用場景:

  • 產品種類相對較少

舉例說明,首先看一下UML類圖:

一個簡單的工廠類

public class SimpleFactory {

    public static Product getProduct(int type){
        if(type == 1){
            return new ConcreteProductA();
        }else if (type ==2){
            return new ConcreteProduceB();
        }
        throw new IllegalArgumentException();
    }
}

工廠方法模式

因為對於稍微複雜一點的邏輯,簡單工廠就會面臨大範圍修改程式碼,並且違反了開閉原則,二工廠發發就是對簡單工廠模式的進一步抽象話

優點:

  • 與簡單工廠方法一樣,客戶端只要具體工廠的名稱即可得到對應的產品
  • 靈活性強,對於新增加一個產品,只需要多寫一個對應的工廠類
  • 高層模組只需要知道產品的抽象類,無需關心其他實現類,滿足迪米特法則,依賴倒置原則以及裡式替換

缺點:

  • 類的個數會增多,增加複雜性
  • 增加了系統的抽象以及理解難度
  • 抽象產品只能生產一種產品

應用場景:

  • 客戶端只需要知道建立產品的工廠名
  • 建立物件有多個具體字工廠中的某一個完成,抽象工廠只提供建立產品的介面

工廠方法的UML類圖

程式碼:

public interface Product {
    void show();
}

public class ConcreteProduct1 implements Product {
    @Override
    public void show() {
        System.out.println("i am ConcreteProduct1");
    }
}

public class ConcreteProduct2 implements Product {
    @Override
    public void show() {
        System.out.println("i am ConcreteProduct2");
    }
}

public abstract class AbstractFactory {

    public abstract Product newProduct();
}

public class ConcreteFactory1 extends AbstractFactory{
    @Override
    public Product newProduct() {
        return new ConcreteProduct1();
    }
}

public class ConcreteFactory2 extends AbstractFactory{
    @Override
    public Product newProduct() {
        return new ConcreteProduct2();
    }
}

客戶端程式碼

public class Client {

    public static void main(String[] args) {
        // 這裡也可使用配置檔案讀入的一個類名,通過反射獲取
        AbstractFactory factory = new ConcreteFactory1();
        Product product = factory.newProduct();
        product.show();
    }
}

工廠模式小結

工廠模式就是將例項化物件的程式碼提取出來,放在一個類中統一管理和維護,達到與主專案的解耦,從而提升專案的擴充套件性以及維護性

3. 原型模式

通過原型例項制定建立物件的種類,並通過拷貝這些原型,建立新的物件

優點:

  • 客戶端無需知道物件建立的細節,簡化了物件建立的過程
  • java自帶的原型模式是基於記憶體二進位制流的複製,在效能上比直接new一個物件更加優良

缺點:

  • 需要為每一個類配置一個clone方法
  • clone方法位於物件內部,對已有類進行改造石,需要修改程式碼,違反開閉原則
  • 當實現深克隆時,需要編寫較為複雜的程式碼,並且所有的方法都需要實現深克隆,實現較為複雜

應用場景:

  • 物件之間相同或者類似
  • 建立物件成本大
  • 建立一個物件需要繁瑣的資料準備
  • 系統中大量使用該物件,並且需要給這個物件重新賦值

實現:

  • 淺克隆:建立一個物件,新物件的豎向與原來的物件完全相同,並且引用型別也會轉向該物件的記憶體地址
  • 深克隆:建立一個物件,屬性中的引用物件也會被克隆,建立一個新的物件,不再使用原有的地址

結構圖如下

在進行深克隆時,有兩種方式,一種就是使用clone,然後自己處理判斷邏輯,確實是否需要繼續呼叫克隆,還有一種就是序列化方式,這兩種後者會損耗效能,但是維護簡單,前者邏輯處理複雜,但是效能較好,因此在繼續寧選擇是,可以根據業務場景選擇。技術沒有絕對的好壞,只有最適合的

public class ConcreteProtoType1 implements ProtoType, Serializable, Cloneable {

    private static final long serialVersionUID = 1L;

    String name;

    private ProtoType protoType;

    public ConcreteProtoType1(String name, ProtoType protoType) {
        this.name = name;
        this.protoType = protoType;
    }

    @Override
    public ProtoType clone() {
        Object clone = null;
        try {
            // 呼叫Object的克隆方法
            clone = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return (ProtoType) clone;
    }

    @Override
    public ProtoType deepClone1() {
        // 方案一:所有引用的屬性都實現深拷貝,然後引用屬性統一呼叫深拷貝方法即可
        ConcreteProtoType1 clone = null;
        try {
            // 呼叫Object的克隆方法
            clone = (ConcreteProtoType1)super.clone();
            if(Objects.nonNull(protoType))
                clone.setProtoType(protoType.deepClone1());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return (ProtoType) clone;
    }

    @Override
    public ProtoType deepClone2() {

        // 在這裡使用的時序列化,把物件進行序列化,然後再反序列化,也同樣可以得到一個新的物件
        ByteArrayOutputStream outputStream = null;
        ByteArrayInputStream inputStream = null;
        ObjectOutputStream objectOutputStream = null;
        ObjectInputStream objectInputStream = null;

        try {
            outputStream = new ByteArrayOutputStream();
            objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(this);

            inputStream = new ByteArrayInputStream(outputStream.toByteArray());
            objectInputStream = new ObjectInputStream(inputStream);
            return (ProtoType) objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            try {
                if (Objects.nonNull(outputStream))
                    outputStream.close();
                if (Objects.nonNull(inputStream))
                    inputStream.close();
                if (Objects.nonNull(objectOutputStream))
                    objectOutputStream.close();
                if (Objects.nonNull(objectInputStream))
                    objectInputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public ProtoType getProtoType() {
        return this.protoType;
    }

    public void setProtoType(ProtoType protoType) {
        this.protoType = protoType;
    }

    @Override
    public String toString() {
        return "ConcreteProtoType1{" +
                "name='" + name + '\'' +
                '}';
    }
}

呼叫

public class Client {

    public static void main(String[] args) {
        ProtoType origin = new ConcreteProtoType1("1", new ConcreteProtoType1("2", null));
        ProtoType clone = origin.clone();
        long start = System.nanoTime();
        ProtoType deepClone1 = origin.deepClone1();
        long cloneEnd = System.nanoTime();
        ProtoType deepClone2 = origin.deepClone2();
        long deepCloneEnd = System.nanoTime();
        System.out.println("first clone use time, nano: " + (cloneEnd - start));
        System.out.println("second clone use time, nano: " + (deepCloneEnd - cloneEnd));
        System.out.println("origin.getProtoType().hashCode() = " + origin.getProtoType().hashCode());
        System.out.println("clone.getProtoType().hashCode() = " + clone.getProtoType().hashCode());
        System.out.println("deepClone1.getProtoType().hashCode() = " + deepClone1.getProtoType().hashCode());
        System.out.println("deepClone2.getProtoType().hashCode() = " + deepClone2.getProtoType().hashCode());
        System.out.println("origin reference== clone reference==> " + (origin.getProtoType() == clone.getProtoType()));
        System.out.println("origin reference == deepClone1 reference==>" + (origin.getProtoType() == deepClone1.getProtoType()));
        System.out.println("origin reference == deepClone2 reference==>" + (origin.getProtoType() == deepClone2.getProtoType()));
    }
}
first clone use time, nano: 12100
second clone use time, nano: 9364900
origin.getProtoType().hashCode() = 127618319
clone.getProtoType().hashCode() = 127618319
deepClone1.getProtoType().hashCode() = 1556595366
deepClone2.getProtoType().hashCode() = 194494468
origin reference== clone reference==> true
origin reference == deepClone1 reference==>false
origin reference == deepClone2 reference==>false

設計模式只是提供思想,並非拘泥於程式碼,我在做無門業務是,由於一個樹關係的構建比較複雜,預計未來會有很多個例項都會跟這個樹關係是一致的,於是便將樹關係提取出來,構建完成之後,基於已經建立的樹關係,再進行構建下一個例項。

4.抽象工廠模式

抽象工廠是一種為訪問類提供一個建立一組相關或相互依賴物件的介面,且訪問類無需指定所要的產品的具體類就能得到同組的不同等級的產品的模式結構

工廠方法一個實現工廠只有一個產品,但是抽象工廠一個工廠可以有多個產品,因此如果只有一個產品時,抽象工廠就會退化到工廠方法模式

優點:

  • 包含上述抽象工廠的優點
  • 類內部對產品族中相關聯的多等級產品共同管理,而不必專門引入多個新的類進行管理
  • 抽象工廠可以保證客戶端只是用一個產品的產品組
  • 增加程式的可擴充套件性,當增加一個產品族時,也就是一個具體的工廠,不需要修改原始碼,滿足開閉原則

缺點

  • 當產品族增加一個新的產品的,所有的工廠類都需要修改

應用場景:

  • 需要建立的物件時一系列相互關聯的或相互依賴的產品族時
  • 有多個產品族,但是每次只是用其中一個產品族

根據農場的特點構建UML圖

我們可以通過類的名稱動態載入對應的實現工廠,這樣可以增加一個新的產品族,可以通過配置檔案匯入解耦

public class Client {

    public static void main(String[] args) throws Exception {
        String sgFarm = "factory.abs.SGFarm";
        Farm farm = (Farm) Class.forName(sgFarm).getDeclaredConstructor().newInstance();
        Animal animal = farm.newAnimal();
        animal.show();
        Plant plant = farm.newPlant();
        plant.show();
    }
}

展示一下其中一個抽象工廠

public class SRFarm implements Farm {
    @Override
    public Animal newAnimal() {
        return new Horse();
    }

    @Override
    public Plant newPlant() {
        return new Fruit();
    }
}

5. 建造者模式

將一個複雜物件的構造與他的表示分離,是同樣的構建過程可以建立不同的表示。也就是將一個複雜的物件分解為多個簡單的物件,然後進行一步一步構建。將變與不變相分離。也就是產品的組成部分是不變的,但是每一部分可以靈活選擇。

舉個例子:建造一輛車。我們會有一張圖紙表示,需要一個輪胎,一個發動機,一個車廂,那麼我們可以對這三個產品進行抽象化,然後有一個指揮者通過介面進行構建,輪胎可以選擇寶馬的,發動機選擇勞斯萊斯的,車廂 選擇五菱的。這樣就構建出一個車,其實本質上,這些模組一個都不少。

優點:

  • 封裝性好,構建與表示分離
  • 擴充套件性好,各個具體建造者相互獨立,有利於系統解耦
  • 客戶端不必知道產品內部細節,建造者可以對建立過程逐步細化,控制細節風險

缺點

  • 產品的組成部分必須相同
  • 產品內部如果發生變化,則建造者也需要同步修改,增加後期維護成本

使用場景:

  • 相同的方法,不同的執行順序,產生不同的結構
  • 多個產品裝配到一個物件,產生的結果不同
  • 產品類比較複雜,初始化一個物件很複雜,引數較多,並且引數很多有預設值

建造者的UML,當然產品也可以進行抽象化,然後具體建造者持有產品(介面)

產品程式碼如下:

public interface Product {

    void setPartA(String partA);

    void setPartB(String partB);

    void setPartC(String partC);

    void show();
}

public class ProductA implements Product {

    private String partA;

    private String partB;

    private String partC;

    @Override
    public void setPartA(String partA) {
        this.partA = partA;
    }

    @Override
    public void setPartB(String partB) {
        this.partB = partB;
    }

    @Override
    public void setPartC(String partC) {
        this.partC = partC;
    }

    @Override
    public void show() {
        System.out.println("ProductA{" +
                "partA='" + partA + '\'' +
                ", partB='" + partB + '\'' +
                ", partC='" + partC + '\'' +
                '}');
    }
}

建造者程式碼:

public abstract class Builder {

    protected Product product;

    public Builder(Product product) {
        this.product = product;
    }

    protected abstract void setPartA();

    protected abstract void setPartB();

    protected abstract void setPartC();

    public Product getResult() {
        return product;
    }
}

// 其中一個實現
public class ConcreteBuild2 extends Builder {
    public ConcreteBuild2(Product product) {
        super(product);
    }

    @Override
    protected void setPartA() {
        product.setPartA("ConcreteBuild2 setA");
    }

    @Override
    protected void setPartB() {
        product.setPartA("ConcreteBuild2 setB");
    }

    @Override
    protected void setPartC() {
        product.setPartA("ConcreteBuild3 setC");
    }
}

指揮者

public class Director {

    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public Product construct(){
        // 進行拼裝的邏輯放在了這裡
        builder.setPartA();
        builder.setPartB();
        builder.setPartC();
        return builder.getResult();
    }
}

客戶端

public class Client {
    public static void main(String[] args) {
        Builder build = new ConcreteBuild1(new ProductA());
        Director director = new Director(build);
        Product product = director.construct();
        product.show(); // ProductA{partA='ConcreteBuild1 setC', partB='null', partC='null'}
    }
}

小結:

其實建造者模式與工廠模式很像,唯一不同就是對複雜物件的建立。如果是簡單物件,直接工廠模式建立。如果是需要使用不同的物件來構建一個產品,可以考慮使用建造者模式。

我在業務的編解碼服務中,就使用到建造者模式,會基於配置資訊,分析接收到的訊息型別,來使用不同的建造者。通過結合不同的編解碼器最終構造成一個codec,然後對收到的資訊進行解碼,複雜的邏輯全部放入指揮者,擴充套件起來就非常容易。

當然也可以使用工廠模式,但修改邏輯就需要修改使用方,為了讓使用方無感知,因此採用了建造者模式。這個可以基於業務場景進行選擇。