23種設計模式之五種創建型模式從概念介紹再到案例分析,不懂就從例子去感受

  • 2020 年 10 月 30 日
  • 筆記

一、創建型模式(Factory Method)

1.工廠模式
1.1普通工廠模式

就是建立一個工廠類,對實現了同一介面的一些類進行實例的創建。首先看下關係圖:

舉例如下:(我們舉一個發送郵件和簡訊的例子)
首先創建二者的共同介面

public interface Sender {  
    public void Send();  
} 

其次,創建實現類:

public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
} 
public class SmsSender implements Sender {  
  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}

最後,建工廠類:

public class SendFactory {  
  
    public Sender produce(String type) {  
        if ("mail".equals(type)) {  
           return new MailSender();  
        } else if ("sms".equals(type)) {  
            return new SmsSender();  
        } else {  
            System.out.println("請輸入正確的類型!");  
           return null;  
        }  
    }  
}

我們來測試下:

public class FactoryTest {  
  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produce("sms");  
        sender.Send();  
    }  
}
1.2 多工廠方法模式

是對普通工廠方法模式的改進,在普通工廠方法模式中,如果傳遞的字元串出錯,則不能正確創建對象,而多個工廠方法模式是提供多個工廠方法,分別創建對象。關係圖:

將上面的程式碼做下修改,改動下SendFactory類就行,如下:

public class SendFactory {  
      
    public Sender produceMail(){  
        return new MailSender();  
    }  
      
    public Sender produceSms(){  
        return new SmsSender();  
    }  
}  

測試類如下:

public class FactoryTest {  
  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produceMail();  
        sender.Send();  
    }  
}  

輸出:this is mailsender!

1.3 靜態工廠方法模式

將上面的多個工廠方法模式里的方法置為靜態的,不需要創建實例,直接調用即可。

public class SendFactory {  
      
    public static Sender produceMail(){  
        return new MailSender();  
    }  
      
    public static Sender produceSms(){  
        return new SmsSender();  
    }  
}
public class FactoryTest {  
  
    public static void main(String[] args) {      
        Sender sender = SendFactory.produceMail();  
        sender.Send();  
    }  
}

輸出:this is mailsender!

  總體來說,工廠模式適合:凡是出現了大量的產品需要創建,並且具有共同的介面時,可以通過工廠方法模式進行創建。在以上的三種模式中,第一種如果傳入的字元串有誤,不能正確創建對象,第三種相對於第二種,不需要實例化工廠類,所以,大多數情況下,我們會選用第三種——靜態工廠方法模式。
2. 抽象工廠模式

​ 工廠方法模式有一個問題就是,類的創建依賴工廠類,也就是說,如果想要拓展程式,必須對工廠類進行修改,這違背了閉包原則,所以,從設計角度考慮,有一定的問題,如何解決?就用到抽象工廠模式,創建多個工廠類,這樣一旦需要增加新的功能,直接增加新的工廠類就可以了,不需要修改之前的程式碼。因為抽象工廠不太好理解,我們先看看圖,然後就和程式碼,就比較容易理解。

請看例子:

public interface Sender {  
    public void Send();  
}

兩個實現類:

public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
} 
public class SmsSender implements Sender {  
  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
} 

兩個工廠類:

public class SendMailFactory implements Provider {  
      
    @Override  
    public Sender produce(){  
        return new MailSender();  
    }  
}
public class SendSmsFactory implements Provider{  
  
    @Override  
    public Sender produce() {  
        return new SmsSender();  
    }  
} 

在提供一個介面:

public interface Provider {  
    public Sender produce();  
} 
public class Test {  
  
    public static void main(String[] args) {  
        Provider provider = new SendMailFactory();  
        Sender sender = provider.produce();  
        sender.Send();  
    }  
} 

其實這個模式的好處就是,如果你現在想增加一個功能:發及時資訊,則只需做一個實現類,實現Sender介面,同時做一個工廠類,實現Provider介面,就OK了,無需去改動現成的程式碼。這樣做,拓展性較好!

3. 單例模式

通過私有構造方法,不允許外面去創建對象,實現只有一個實例。

3.1 餓漢式單例

缺點:一開始就把所有的對象都載入,容易浪費空間。

public class Hungry {
	//核心是構造,不允許外面去創建對象
	private Hungry() {
	}
	//創建靜態變數
	private final static Hungry HUNGRY = new Hungry();
	//返回對象
	public static Hungry getInstance() {
		return HUNGRY;
	}
}
3.2 懶漢式單例

(1)只有在對象為空時才去new對象——單執行緒下沒有問題

public class LazyMan {

	private LazyMan() {
	}
	private static LazyMan lazyMan;
	
	//只有在對象為空時才去new對象------單執行緒下沒有問題
	public static LazyMan getInstance() {
		if(lazyMan == null) {
			lazyMan = new LazyMan();
		}
		return lazyMan;
	}
}

(2)通過多執行緒去測試,發現單例被破壞。

private LazyMan() {
	System.out.println(Thread.currentThread().getName()+"ok");
}
//多執行緒的情況下就會出現多個對象
public static void main(String[] args) {
	for(int i=0;i<10;i++) {
		new Thread(()->{
			LazyMan.getInstance();
		}).start();
	}
}

去實現雙重檢測鎖模式的懶漢式單例–DCL懶漢式,並且在lazyMan = new LazyMan();位置,由於還不是原子操作,容易造成指令重排,去加上volatile屬性,他是可見的不是原子的,來保證安全。

private volatile static LazyMan lazyMan;

public static LazyMan getInstance() {
		if(lazyMan==null) {
			synchronized (LazyMan.class) {   //鎖住類就會只有一個對象
				if(lazyMan == null) {
					lazyMan = new LazyMan();   //不是一個原子操作
					/**
					 * 1. 分配記憶體空間
					 * 2. 執行構造方法,初始化對象
					 * 3. 把對象指向這個空間
					 *  假如又A,B兩個執行緒,A執行了132是沒有問題的,但是當B在執行時就							直接return了,但是此時還沒有完成構造,是不安全的。
					 *  volatile只能保證可見性,不證原子性。
					 * */
				}
			}
		}
		return lazyMan;
	} 

(3)通過反射機制去破環構造方法的私有性,從而破壞單例模式—一個反射對象。

public static void main(String[] args) throws Exception{
		LazyMan instance = LazyMan.getInstance();
		Constructor<LazyMan> declaredConstructor = 			    LazyMan.class.getDeclaredConstructor(null);
		declaredConstructor.setAccessible(true);   //用反射使其私有構造無效
		LazyMan instance2 = declaredConstructor.newInstance();
		
		System.out.println(instance);
		System.out.println(instance2);
	}

去實現三重檢測,也就是在構造方法上再次進行加鎖判斷,避免某一種反射的破壞,其餘不變。

private LazyMan() {
	synchronized (LazyMan.class) {
		if(lazyMan!=null) {
			throw new RuntimeException("不要試圖使用反射破壞單例");
		}
	}
}

(4)上面的先正常創建對象,再用反射,如果我們直接建立多個反射對象就可以破壞單例。

public static void main(String[] args) throws Exception{
		Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
		declaredConstructor.setAccessible(true);   //用反射使其私有構造無效
		LazyMan instance = declaredConstructor.newInstance();
		LazyMan instance2 = declaredConstructor.newInstance();
		
		System.out.println(instance);
		System.out.println(instance2);
}

通過增加一個標誌位,可以防止多個反射對象的破壞。

private static boolean flag = false;
	
private LazyMan() {
	synchronized (LazyMan.class) {
		if(flag==false) {
			flag = true;
		}else {
			throw new RuntimeException("不要試圖使用反射破壞單例");
		}
	}
}

(5)如果我們能夠破解這個標誌位,仍然是可以去破壞單例。

public static void main(String[] args) throws Exception{
		Field flag = LazyMan.class.getDeclaredField("flag");//獲得標誌位
		flag.setAccessible(true);
		
		Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
		declaredConstructor.setAccessible(true);   //用反射使其私有構造無效
		LazyMan instance = declaredConstructor.newInstance();
		flag.set(instance, false);	//破壞標誌位
		LazyMan instance2 = declaredConstructor.newInstance();
		
		System.out.println(instance);
		System.out.println(instance2);
	}

(6)通過使用枚舉去保證單例和安全,因為枚舉是不可以被反射的。

public enum EnumSingle {
	INSTANCE;
	//枚舉本身也是一個類,只是繼承了枚舉
	public EnumSingle getInstance() {
		return INSTANCE;
	}
}
public static void main(String[] args) throws Exception {
		EnumSingle instance1 = EnumSingle.INSTANCE;
		EnumSingle instance2 = EnumSingle.INSTANCE;
		
		System.out.println(instance1);
		System.out.println(instance2);
}

我們可以通過我們的反編譯class位元組文件,發現有一個帶參構造,如果這個時候再去用反射破壞,發現就會報menu不能被反射的錯了。

public static void main(String[] args) throws Exception {		
		EnumSingle instance1 = EnumSingle.INSTANCE;
		Constructor<EnumSingle> de = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
		de.setAccessible(true);
		EnumSingle instance2 = de.newInstance();
	}

3.3 靜態內部類實現單例

public class Holder {

	private Holder() {
		
	}
	public static Holder getInstance() {
		return InnerClass.HOLDER;
	}
	public static class InnerClass{
		private static final Holder HOLDER = new Holder();
	}
}
4.建造者模式

將一個複雜對象的構建與他的表示層分離,實現了解耦,使得同樣的構造過程可以構造不同的表示。具體的建造者類之間是相互獨立的,這有利於系統的擴展,符合「開閉原則」。

缺點:共性、內部變化複雜,導致多個建造者類,系統龐大。

(1)舉例工人蓋房子,有四個步驟地基、鋼筋、水泥、粉刷。工人有指揮官,負責指揮工作的順序,如果需要建不同的樓,只需要換不同的工人,因為工人的技能是不同的。

//抽象的建造者:方法
public abstract class Builder {

	abstract void buildA();  //地基
	abstract void buildB();  //鋼筋
	abstract void buildC();  //鋪電線
	abstract void buildD();  //粉刷
	
	//完工:得到產品
	abstract Product getProduct();
}
//產品:房子
public class Product {

	private String builderA;
	private String builderB;
	private String builderC;
	private String builderD;
	
	public String getBuilderA() {
		return builderA;
	}
	public void setBuilderA(String builderA) {
		this.builderA = builderA;
	}
	public String getBuilderB() {
		return builderB;
	}
	public void setBuilderB(String builderB) {
		this.builderB = builderB;
	}
	public String getBuilderC() {
		return builderC;
	}
	public void setBuilderC(String builderC) {
		this.builderC = builderC;
	}
	public String getBuilderD() {
		return builderD;
	}
	public void setBuilderD(String builderD) {
		this.builderD = builderD;
	}
	@Override
	public String toString() {
		return "Product [builderA=" + builderA + ", builderB=" + builderB + ", builderC=" + builderC + ", builderD="
				+ builderD + "]";
	}
}
public class Worker extends Builder{
	
	private Product product;
	public Worker() {
		product = new Product();
	}
	//工人把工作,實現建造的沒有步驟
	public void buildA() {
		product.setBuilderA("地基");
		System.out.println("地基");
	}
	public void buildB() {
		product.setBuilderB("鋼筋");
		System.out.println("鋼筋");
	}
	public void buildC() {
		product.setBuilderC("鋪電線");
		System.out.println("鋪電線");
	}
	public void buildD() {
		product.setBuilderD("粉刷");
		System.out.println("粉刷");
	}

	public Product getProduct() {
		return product;
	}
}
//指揮:核心,負責指揮構建一個工程,工程如何構建由他決定
public class Director {

	//指揮工人按照順序建房子
	public Product build(Builder builder) {
		builder.buildA();
		builder.buildB();
		builder.buildC();
		builder.buildD();
		return builder.getProduct();
	}
}
public class Test {

	public static void main(String[] args) {
		//需要一個指揮者 
		Director director = new Director();
		//指揮工人去完成具體的產品
		Product product = director.build(new Worker());
		System.out.println(product.toString());
	}
}

(2)上述是有指揮官的構造,如果沒有指揮官,就不需要順序,也就是實現無序的搭配。

舉例:麥當勞的套餐,服務員(建造者)隨意搭配幾種食物形成套餐銷售給客戶。

//抽象的建造者:方法
public abstract class Builder {

	abstract Builder buildA(String msg);  //漢堡
	abstract Builder buildB(String msg);  //可樂
	abstract Builder buildC(String msg);  //薯條
	abstract Builder buildD(String msg);  //甜品
	
	//完工:得到產品套餐
	abstract Product getProduct();
}
//產品:套餐
public class Product {
	//默認值,也就是有默認的套餐
	private String builderA = "漢堡";
	private String builderB = "可樂";
	private String builderC = "薯條";
	private String builderD = "甜點";
	
	public String getBuilderA() {
		return builderA;
	}
	public void setBuilderA(String builderA) {
		this.builderA = builderA;
	}
	public String getBuilderB() {
		return builderB;
	}
	public void setBuilderB(String builderB) {
		this.builderB = builderB;
	}
	public String getBuilderC() {
		return builderC;
	}
	public void setBuilderC(String builderC) {
		this.builderC = builderC;
	}
	public String getBuilderD() {
		return builderD;
	}
	public void setBuilderD(String builderD) {
		this.builderD = builderD;
	}
	@Override
	public String toString() {
		return "Product [builderA=" + builderA + ", builderB=" + builderB + ", builderC=" + builderC + ", builderD="
				+ builderD + "]";
	}
}
public class Worker extends Builder{
	
	private Product product;
	
	public Worker() {
		product = new Product();
	}
	public Builder buildA(String msg) {
		product.setBuilderA(msg);
		return this;
	}
	public Builder buildB(String msg) {
		product.setBuilderB(msg);
		return this;
	}
	public Builder buildC(String msg) {
		product.setBuilderC(msg);
		return this;
	}
	public Builder buildD(String msg) {
		product.setBuilderD(msg);
		return this;
	}
	
	public Product getProduct() {
		return product;
	}
}
public class Test {

	public static void main(String[] args) {
		//服務員
		Worker worker = new Worker();
		//不改變參數,默認套餐
		//Product product = worker.getProduct();
		
		//鏈式編程:在原來的基礎上可以自由組合了,如果不組合也有默認的套餐。
		Product product = worker.buildA("披薩").buildB("美年達").getProduct();
		System.out.println(product.toString());
	}
}
5.原型模式

原型模式是以一個為原型,在使用的時候不用再去new ,而是通過clone來提升效率。

第一步,實現一個介面 Cloneable;第二步,重寫一個方法 clone();

(1)淺克隆:通過克隆出來的對象和原對象是一樣的,但是屬性的引用也是一樣的,指向同一塊記憶體空間。

public class Video implements Cloneable{

	private String name;
	private Date createTime;	
	//淺克隆
	protected Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
	
	public Video() {}
	public Video(String name,Date createTime) {
		this.name = name;
		this.createTime = createTime;
	}

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Date getCreateTime() {
		return createTime;
	}
	public void setCreateTime(Date createTime) {
		this.createTime = createTime;
	}
	
	public String toString() {
		return "Video [name=" + name + ", createTime=" + createTime + "]";
	}
}

測試如下:

public class Bilibili {

	public static void main(String[] args) throws CloneNotSupportedException {
		//原型對象
		Date date = new Date();
		Video v1 = new Video("狂神說",date);
		System.out.println("v1:"+v1);
		System.out.println("v1.hashcode():"+v1.hashCode());
		//clone一個對象
		Video v2 = (Video)v1.clone();   //克隆出來的對象和原來的對象是一摸一樣的
		System.out.println("v2:"+v2);
		System.out.println("v2.hashcode():"+v2.hashCode());
		
		v2.setName("clone:狂神說");
		System.out.println(v2);
		
		System.out.println("=====但是這樣是淺克隆,date會隨著原型值的改變而改變=====");
		date.setTime(123135);
		System.out.println("v1:"+v1);
		System.out.println("v2:"+v2);
	}
}

(2)深克隆:克隆對象額同時也會把屬性一同克隆,使克隆的屬性擁有自己獨立的記憶體空間。還有序列化,反序列化。

protected Object clone() throws CloneNotSupportedException {
		Object obj =  super.clone();
		Video v = (Video)obj;
		//克隆屬性
		v.createTime = (Date)this.createTime.clone();
		return obj;
	}

//www.cnblogs.com/yanwenbing/p/Pattern_OOP.html