設計模式——創建型設計模式

創建型設計模式

爭對對象/類創建時的優化

工廠方法模式(了解)

通過定義頂層抽象工廠類,通過繼承的方式,針對於每一個產品都提供一個工廠類用於創建。

情況:只適用於簡單對象,當我們需要生產許多個產品族的時候,這種模式就有點乏力了

創建對象不再使用傳統的new,而是創建一個工廠類,作為all實體類創建對象的一個封裝類。(避免了更改類名、構造方法時,需要修改大量的程式碼)

簡單工廠模式:(不靈活,不建議)

不符合開閉原則。如果輸入沒有提前寫好的水果則就需要再添加每個類里的程式碼

//水果抽象類
public abstract class Fruit {   
    private final String name;
    
    public Fruit(String name){
        this.name = name;
    }

    @Override
    public String toString() {
        return name+"@"+hashCode();   //列印一下當前水果名稱,還有對象的hashCode
    }
}

//水果實體類
public class Apple extends Fruit{   //蘋果,繼承自水果

    public Apple() {
        super("蘋果");
    }
}
public class Orange extends Fruit{  //橘子,也是繼承自水果
    public Orange() {
        super("橘子");
    }
}

//水果工廠
public class FruitFactory {
    /**
     * 這裡就直接來一個靜態方法根據指定類型進行創建
     * @param type 水果類型
     * @return 對應的水果對象
     */
    public static Fruit getFruit(String type) {
        switch (type) {
            case "蘋果":
                return new Apple();
           	case "橘子":
                return new Orange();
            default:
                return null;
        }
    }
}

//主方法
public class Main {
    public static void main(String[] args) {
        Fruit fruit = FruitFactory.getFruit("橘子");   //直接問工廠要,而不是我們自己去創建
        System.out.println(fruit);
    }
}

工廠方法模式:通過范型靈活實現

如果新增了水果類型,直接創建一個新的工廠類就行,不需要修改之前已經編寫好的內容。

缺點:一種水果就有一種新的工廠類,太多工廠類了

//水果抽象類
public abstract class Fruit {   
    private final String name;
    
    public Fruit(String name){
        this.name = name;
    }

    @Override
    public String toString() {
        return name+"@"+hashCode();   //列印一下當前水果名稱,還有對象的hashCode
    }
}

//水果工廠
public abstract class FruitFactory<T extends Fruit> {   //將水果工廠抽象為抽象類,添加泛型T由子類指定水果類型
    public abstract T getFruit();  //不同的水果工廠,通過此方法生產不同的水果
}

//Apple工廠
public class AppleFactory extends FruitFactory<Apple> {  //蘋果工廠,直接返回Apple,一步到位
    @Override
    public Apple getFruit() {
        return new Apple();
    }
}

//主方法
public class Main {
    public static void main(String[] args) {
        test(new AppleFactory()::getFruit);   //比如我們現在要吃一個蘋果,那麼就直接通過蘋果工廠來獲取蘋果
    }

    //此方法模擬吃掉一個水果
    private static void test(Supplier<Fruit> supplier){
        System.out.println(supplier.get()+" 被吃掉了,真好吃。");
    }
}

抽象工廠模式

情況:適用於有一系列產品的公司。

缺點:容易違背開閉原則。一旦增加了一種產品,此時就必須去修改抽象工廠的介面,這樣就涉及到抽象工廠類的以及所有子類的改變

舉例:

image

實際上這些產品都是成族出現的,比如小米的產品線上有小米12,小米平板等,華為的產品線上也有華為手機、華為平板,但是如果按照我們之前工廠方法模式來進行設計,那就需要單獨設計9個工廠來生產上面這些產品,顯然這樣就比較浪費時間的。

我們就可以使用抽象工廠模式,我們可以將多個產品,都放在一個工廠中進行生成,按不同的產品族進行劃分,比如小米,那麼我就可以安排一個小米工廠,而這個工廠裡面就可以生產整條產品線上的內容,包括小米手機、小米平板、小米路由等。

//工廠抽象類
public abstract class AbstractFactory {
    public abstract Phone getPhone();
    public abstract Table getTable();
    public abstract Router getRouter();
}

//工廠實現類
public class AbstractFactoryImpl extends AbstractFactory{
    @Override
    public Phone getPhone() {
        return new ProductPhone();
    }

    @Override
    public Table getTable() {
        return new ProductTable();
    }

    @Override
    public Router getRouter() {
        return new ProductRouter();
    }
}

//產品抽象類
public abstract class AbRouter{
    public abstract Router getRouter();
}
...
    
//產品實體類
public class Router extends AbRouter{
    
    @Override
    public Router getRouter(){
        return new Router();
    }
}

建造者模式

當構造對象時參數較多,可以通過建造者模式使用鏈式方法創建對象,保證參數填寫正確。

可以去看看StringBuilder的源碼,有很多的框架都為我們提供了形如XXXBuilder的類型,我們一般也是使用這些類來創建我們需要的對象。

建造者模式創建對象其實和StringBuilder一樣:實際上我們是通過建造者來不斷配置參數或是內容,當我們配置完所有內容後,最後再進行對象的構建。

public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();   //創建一個StringBuilder來逐步構建一個字元串
    builder.append(666);   //拼接一個數字
    builder.append("老鐵");   //拼接一個字元串
   	builder.insert(2, '?');  //在第三個位置插入一個字元
    System.out.println(builder.toString());   //差不多成形了,最後轉換為字元串
}

舉例:

//實體類的編寫
public class Student {
	int id;
    int age;
    int grade;
    String name;
    String college;
    String profession;
    List<String> awards;

    //一律使用建造者來創建,不對外直接開放
    private Student(int id, int age, int grade, String name, String college, String profession, List<String> awards) {
        this.id = id;
        this.age = age;
        this.grade = grade;
        this.name = name;
        this.college = college;
        this.profession = profession;
        this.awards = awards;
    }

    public static StudentBuilder builder(){   //通過builder方法直接獲取建造者
        return new StudentBuilder();
    }

    public static class StudentBuilder{   //這裡就直接創建一個內部類
        //Builder也需要將所有的參數都進行暫時保存,所以Student怎麼定義的這裡就怎麼定義
        int id;
        int age;
        int grade;
        String name;
        String college;
        String profession;
        List<String> awards;

        public StudentBuilder id(int id){    //直接調用建造者對應的方法,為對應的屬性賦值
            this.id = id;
            return this;   //為了支援鏈式調用,這裡直接返回建造者本身,下同
        }

        public StudentBuilder age(int age){
            this.age = age;
            return this;
        }
      
      	...

        public StudentBuilder awards(String... awards){
            this.awards = Arrays.asList(awards);
            return this;
        }
        
        public Student build(){    //最後我們只需要調用建造者提供的build方法即可根據我們的配置返回一個對象
            return new Student(id, age, grade, name, college, profession, awards);
        }
    }
}
//主方法
public static void main(String[] args) {
    Student student = Student.builder()   //獲取建造者
            .id(1)    //逐步配置各個參數
            .age(18)
            .grade(3)
            .name("小明")
            .awards("ICPC-ACM 區域賽 金牌", "LPL 2022春季賽 冠軍")
            .build();   //最後直接建造我們想要的對象
}

單例模式

單例模式:在電腦進程中,同一個類始終只會有一個對象來進行操作。

多例模式:在電腦進程中,對一個實體類創建一次對象就是對當個對象操作,若是創建多個對象則是分別對對應的對象操作。

單例模式的三種寫法:

  1. 餓漢式單例(不建議)

    在最開始就創建了對象(太饑渴了,一開始就需要對象)

    public class Singleton {
        private final static Singleton INSTANCE = new Singleton();   //用於引用全局唯一的單例對象,在一開始就創建好
        
        private Singleton() {}   //禁用了構造方法Singleton()來創建對象。不允許隨便new,需要對象直接找getInstance
        
        public static Singleton getInstance(){   //獲取全局唯一的單例對象
            return INSTANCE;
        }
    }
    
  2. 加鎖的懶漢式單例(不建議,沒有第三種方法好)

    懶漢:在要用的時候才創建對象。但又得防多執行緒就上了鎖

    public class Singleton {
        private static volatile Singleton INSTANCE;   //在一開始先不進行對象創建。volatile關鍵字是多執行緒的時候,這個變數更改了,別的執行緒可以立馬檢測到
    
        private Singleton() {}   //禁用了構造方法Singleton()來創建對象。不允許隨便new,需要對象直接找getInstance
    
        public static Singleton getInstance(){
        if(INSTANCE == null) {	//這層判斷是便於第一次外訪問時不用在走鎖
            synchronized (Singleton.class) {	//加鎖是為了防止多執行緒創建了多個對象
                if(INSTANCE == null) INSTANCE = new Singleton();  //由於加了鎖,所以當一個進程進來創建了對象,其他執行緒需要再判斷一次有沒有人已經創建了這個類對象,有就不創建了。內層還要進行一次檢查,雙重檢查鎖定
            }
        }
        return INSTANCE;
    }
    }
    
  3. 靜態內部類的半懶、半餓式單例(建議)

    靜態內部類該開始不會載入,需要的時候才會載入,由於這個類一載入就會創建對象。

    所以實現了懶漢的資源不濫用,餓漢的防止多執行緒

    public class Singleton {
        private Singleton() {}//禁用了構造方法Singleton()來創建對象。不允許隨便new,需要對象直接找getInstance
    
        private static class Holder {   //由靜態內部類持有單例對象,但是根據類載入特性,我們僅使用Singleton類時,不會對靜態內部類進行初始化。一旦類初始化之後值將不會改變,有點餓漢式的味道。
            private final static Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance(){   //只有真正使用內部類時,才會進行類初始化
            return Holder.INSTANCE;   //直接獲取內部類中的
        }
    }
    

原型模式

定義:用原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象。(說白了就是複製)

  • 淺拷貝:①對於類中基本數據類型,會直接複製值給拷貝對象;②對於引用類型(對象類型),只會複製對象的地址,而實際上指向的還是原來的那個對象,拷貝個基莫。
public static void main(String[] args) {
    int a = 10;
    int b = a;  //基本類型淺拷貝
    System.out.println(a == b);	//true

    Object o = new Object();
    Object k = o;    //引用類型淺拷貝,拷貝的僅僅是對上面對象的引用
    System.out.println(o == k);	//true
}
  • 深拷貝:無論是基本類型還是引用類型,深拷貝會將引用類型的所有內容,全部拷貝為一個新的對象,包括對象內部的所有成員變數,也會進行拷貝。

使用Cloneable介面提供的拷貝機制,來實現原型模式:操作完會發現Object的clone默認還是淺複製

protected class Student implements Cloneable{   //注意需要實現Cloneable介面
    ...
    //Cloneable中的方法,下面程式碼複製Object的clone源碼    
    @Override
    public Object clone() throws CloneNotSupportedException {   //提升clone方法的訪問許可權
        return super.clone();
    }
}

//主方法
public static void main(String[] args) throws CloneNotSupportedException {
    Student student0 = new Student();
    Student student1 = (Student) student0.clone();
    System.out.println(student0);
    System.out.println(student1);
    //兩個結果不同,就是地址不同
    
    Student student0 = new Student("小明");
    Student student1 = (Student) student0.clone();
    System.out.println(student0.getName() == student1.getName());
    //true 
}

深拷貝:在student實現介面Cloneable後重寫clone方法

@Override
public Object clone() throws CloneNotSupportedException {   //這裡我們改進一下,針對成員變數也進行拷貝
    Student student = (Student) super.clone();
    student.name = new String(name);
    return student;   //成員拷貝完成後,再返回
}