白話梳理創(chuàng)建型設(shè)計(jì)模式:單例、原型,構(gòu)建
寫在前面
分享一些設(shè)計(jì)模式的筆記。陸續(xù)整理,按照設(shè)計(jì)模式類型,創(chuàng)建型,結(jié)構(gòu)型,行為型發(fā)布
博文會用通俗的話梳理一些自己的理解,結(jié)合開發(fā)中的實(shí)際場景,
關(guān)于設(shè)計(jì)模式,個人覺得,在理解上要站著它要解決的問題域的角度,而不是它的實(shí)現(xiàn)方式,因?yàn)閷W(xué)完全部的設(shè)計(jì)模式,你會感覺,好像大多設(shè)計(jì)模式實(shí)現(xiàn)上基本一樣。往往有這一種被欺騙的感覺....哈
食用方式:適合設(shè)計(jì)模式溫習(xí),需要對設(shè)計(jì)模式有大概的了解
理解不足小伙伴幫忙指正,虛心接受 ^_^
「 傍晚時分,你坐在屋檐下,看著天慢慢地黑下去,心里寂寞而凄涼,感到自己的生命被剝奪了。當(dāng)時我是個年輕人,但我害怕這樣生活下去,衰老下去。在我看來,這是比死亡更可怕的事。--------王小波」
在23種設(shè)計(jì)模式中,創(chuàng)建型的設(shè)計(jì)模式有了5種,分別為:單例、原型、建造、工廠方法和抽象工廠。
今天要溫習(xí)的是前三個
單例
關(guān)于單例的實(shí)現(xiàn)方式,先不講,聊聊為什么需要單例?單例的優(yōu)點(diǎn)是什么,有哪些地方使用了單例?
單例用通俗的話講,即在某個作用域內(nèi),不管如何操作,某個類的實(shí)例只能是同一個,創(chuàng)建的這種類實(shí)例稱為單例模式。
為什么需要單例?
一種情況是有些實(shí)例沒理由重新創(chuàng)建或者丟棄它,它需要一直存在著,同時,在內(nèi)存里面只有一個實(shí)例,可以減少內(nèi)存開銷,頻繁的創(chuàng)建和銷毀實(shí)例需要考慮GC等其他的的問題,比如常見的一些工廠類實(shí)例,只是希望通過他來生成一些實(shí)例,沒必多個實(shí)例存在,沒必要創(chuàng)建銷毀,而且多次重建,編碼角度考慮,是很壞的編碼,比如一些ORM框架中生成SqlSession的SqlSessionFactory實(shí)例,一般使用單例模式或者靜態(tài)單例模式,粗了減少加載配置的同時,考慮數(shù)據(jù)庫連接數(shù)性能問題。
二是某些實(shí)例,希望它在整個生命周期內(nèi)是不可變得,全局唯一的。不管在什么情況下,它的構(gòu)成能夠被嚴(yán)格的控制,可以始終保證他是共享且安全的。同時使用單例可以避免那些存儲唯一實(shí)例的全局變量污染命名空間。比如Java中某個類對應(yīng)的class實(shí)例,都是單例模式,一個Class實(shí)例用于描述一個類加載到內(nèi)存中的數(shù)據(jù),只描述一個類,即一個類只有一個Class實(shí)例。同時它沒有構(gòu)造函數(shù),不能主動實(shí)例化,而是在類在加載時由java虛擬機(jī)通過類加載器中的defineClass自動構(gòu)造的。
如何實(shí)現(xiàn)單例
對于單例,本質(zhì)的問題是如何保證只能被實(shí)例化一次,所以不管如何實(shí)現(xiàn)都需要構(gòu)造函數(shù)私有化.或者沒有構(gòu)造函數(shù)由JVM自動構(gòu)造
最簡單的實(shí)現(xiàn)是餓漢式單例 ,singleton作為類變量并且直接得到了初始化,即類中所有的變量都會被初始化 singleton作為類變量在初始化的過程中會被收集進(jìn)<clinit>()方法中,雖然這樣能夠百分之百的保證同步,但是因?yàn)椴皇菓屑虞d,singleton被加載后可能很長一段時間不被使用,即實(shí)例所開辟的空間會存在很長時間,內(nèi)存角度考慮,不是好的實(shí)現(xiàn)。
private Singleton(){ }
private static final Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
所以為了實(shí)現(xiàn)懶加載,有了懶漢式單例模式,雖然懶漢式可以保證懶加載,但是線程不安全, 當(dāng)有兩個線程訪問時,不能保證單例的唯一性
private Singleton(){ }
private static Singleton singleton =null;
public static Singleton getInstance(){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
為了保證線程安全,有了懶漢式+同步方法,即能保證懶加載,又可以保證singleton實(shí)例的唯一性,但是synchronizeed關(guān)鍵字的排他性導(dǎo)致getInstance()方法只能在同一時間被一個線程訪問。性能低下。
private Singleton(){ }
private static Singleton singleton =null;
public static synchronized Singleton getInstance(){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
這個時候,有人對懶漢式+同步方法做了改進(jìn),雙重校驗(yàn)鎖單例(Double-Check)+Volatile ,當(dāng)有兩個線程發(fā)現(xiàn)singleton為null時,只有一個線程可以進(jìn)入到同步代碼塊里。即滿足了懶加載,又保證了線程的唯一性,不加volition的缺點(diǎn),有時候可能會報NPE,(JVM運(yùn)行指令重排序),有可能實(shí)例實(shí)例的變量未完成實(shí)例化其他線程去獲取到singleton變量。未完成初始化的實(shí)例調(diào)用其方法會拋出空指針異常。
private Singleton(){ }
private static volatile Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null){
synchronized (Singleton.class){
if (singleton ==null){
singleton = new Singleton();
}
}
}
return singleton2;
}
有人覺得這樣寫好麻煩,有些有沒有簡單的寫法,當(dāng)然有,靜態(tài)內(nèi)部類單例,靜態(tài)內(nèi)部類的單例模式在Singleton類初始化時并不會創(chuàng)建Singleton實(shí)例,在靜態(tài)內(nèi)部類中定義了singleton實(shí)例。當(dāng)給靜態(tài)內(nèi)部類被主動創(chuàng)建時則會創(chuàng)建Singleton靜態(tài)變量,是最好的單例模式之一,;類似于靜態(tài)工廠,將類的創(chuàng)建延遲到靜態(tài)內(nèi)部類,外部類的初始化不會實(shí)例化靜態(tài)內(nèi)部類的靜態(tài)變量。
private Singleton(){ }
private static class Singtetons{
private static Singleton SINGLETON = new Singleton();
/* static {
final Singleton SINGLETON = new Singleton();
}*/
}
public static Singleton getInstance(){
return Singtetons.SINGLETON;
}
當(dāng)然還有其他的一些寫法,比如基于枚舉的單例等,感興趣小伙伴可以去了解下
原型
為什么需要原型?
原型模式,用通俗的話講,即在原有實(shí)例的基礎(chǔ)上創(chuàng)建多個新的實(shí)例,減少多實(shí)例和復(fù)雜實(shí)例創(chuàng)建的內(nèi)存消耗。
原型模式和享元模式有些類似,都是嘗試重用現(xiàn)有的同類實(shí)例,但是他們本質(zhì)是不相同的,原型模式對現(xiàn)有實(shí)例的再加工,比如JS里的原型設(shè)計(jì),原型鏈,或者克隆當(dāng)前實(shí)例;而享元模式是對現(xiàn)有實(shí)例的重復(fù)使用,比如Java里的整形池、字符串池(String Pool),另一個角度,原型是創(chuàng)建型設(shè)計(jì)模式,而享元是結(jié)構(gòu)型設(shè)計(jì)模式。
所以可以這樣理解記憶,如果希望重用現(xiàn)有實(shí)例,再加工或者直接克隆,屬于實(shí)例創(chuàng)建是原型模式,如果直接使用,屬于結(jié)構(gòu)型享元模式。
原型模式一般用于克隆生成實(shí)例,會結(jié)合工廠模式使用,換一種角度考慮,其實(shí)和一種叫寫時復(fù)制(copy-on-write)的技術(shù)特別類似,在容器、虛擬化技術(shù)中都有應(yīng)用。
在容器技術(shù)中,應(yīng)用級別考慮,內(nèi)核共用本身就是一種原型設(shè)計(jì),同時一個運(yùn)行的容器分為鏡像層和容器層,這里的鏡像層可以理解為原型層,在對容器層的數(shù)據(jù)進(jìn)行修改時,如果是update會把文件復(fù)制到容器層update,如果是新增會在容器層新增,如果刪除,會屏蔽鏡像層的文件。如果讀取,會在由容器層到鏡像層自上而下的查找。
在虛擬化技術(shù)中,如果系統(tǒng)級別考慮,硬件資源共享本身也是一種原型設(shè)計(jì),同時OpenStack 利用其 Glance組件,把虛機(jī)的分為原始后端盤和增量前端盤,這里的原始后端盤即可以理解為原型盤,一個標(biāo)準(zhǔn)系統(tǒng)鏡像,在創(chuàng)建的虛機(jī)里。當(dāng)修改系統(tǒng)文件的時候,會復(fù)制原始的文件到增量盤修改。創(chuàng)建的文件只在增量盤創(chuàng)建
原型設(shè)計(jì)有許多和抽象工廠和建造者一樣的效果:它隱藏了具體的實(shí)例類,因此減少了實(shí)例的數(shù)目??梢栽谶\(yùn)行時刻增加和刪除實(shí)例,通過改變結(jié)構(gòu)、改變值來指向新的實(shí)例。減少了子類的構(gòu)造,也減少復(fù)雜實(shí)例的重復(fù)構(gòu)建。
如何實(shí)現(xiàn)原型?
整體上講,原型設(shè)計(jì)模式的應(yīng)用有兩種:
第一種是通過改變結(jié)構(gòu)、改變值來指向新的實(shí)例,把實(shí)例中公共的部分作為原型,比如Js實(shí)例中的一些事件函數(shù),這種情況下,可以減少對同類實(shí)例中相同部分的創(chuàng)建,減少內(nèi)存開銷,比如JS的原型設(shè)計(jì)
另一種情況是復(fù)雜實(shí)例的克隆,這種情況一般用于實(shí)例構(gòu)建需要依賴外部數(shù)據(jù),涉及第三方數(shù)據(jù),比如數(shù)據(jù)庫,IO讀取等情況,構(gòu)建相對復(fù)雜耗時,所以把整個實(shí)例作為原型,重寫克隆方法實(shí)現(xiàn)對實(shí)例的克隆
這里那JS原生的原型模式Demo來看下。
// 在原型基礎(chǔ)上的新實(shí)例
function Person() {
this.sex = 'man';
}
// 原型實(shí)例
Person.prototype = {
name: 'Nicholas',
age: 29,
job: "Software Engineer",
sayName : function () {
console.log(this.name)
}
}
var person1 = new Person();
person1.name = 'liruilong';
console.log(person1)
var person2 = new Person();
console.log(person2)
console.log(person1.sayName == person2.sayName);//true
Person.prototype指向原型,而Person本身即為原型擴(kuò)展后的實(shí)例。通對name的賦值也可以看到,修改屬性會直接覆蓋原型的值。
[object Object] {
age: 29,
job: "Software Engineer",
name: "liruilong",
sayName: function () {
window.runnerWindow.proxyConsole.log(this.name)
},
sex: "man"
}
[object Object] {
age: 29,
job: "Software Engineer",
name: "Nicholas",
sayName: function () {
window.runnerWindow.proxyConsole.log(this.name)
},
sex: "man"
}
true
關(guān)于重寫克隆方法的原型設(shè)計(jì)模式利用,可以通過深度遍歷,或者序列化的方式實(shí)現(xiàn),感興趣小伙伴可以下去了解下
建造者
為什么需要建造者
建造者設(shè)計(jì)模式也被稱為為生成器模式,個人覺得,這是編碼中使用最多的一個設(shè)計(jì)模式了,用通俗的話講,即使用多個簡單的實(shí)例一步一步構(gòu)建成一個復(fù)雜的實(shí)例,為什么需要建造者,通過建造者,可以將一個復(fù)雜的構(gòu)建與其表示相分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。
通過一個最簡單的建造者設(shè)計(jì)模式應(yīng)用體會下,比如一個Bean的生成,涉及的屬性較多,構(gòu)造函數(shù)很不方便,可以使用建造者設(shè)計(jì)模式,通setter 方法對屬性賦值,返回一個this的方式(區(qū)別于傳統(tǒng)的setter方法),每個setter 方法的調(diào)用即是一次構(gòu)建,返回的是不同的實(shí)例(屬性不同)。
public Bulider setD(int d) {
this.d = d;
return this;
}
復(fù)雜一點(diǎn)的,比如 Java 中 SpringBoot 系列安全組件 SpringSecurity配置的生成,即通過建造者的設(shè)計(jì)模式構(gòu)建了一個配置類,可以有選擇通過構(gòu)建的方式配置鑒權(quán),授權(quán),比如表單驗(yàn)證等,以及各種事件的處理器。關(guān)于配置的Demo感興趣小伙伴可以了解下。
開發(fā)中,需要生成一些復(fù)雜多變的東西,比如doc,excel等,設(shè)計(jì)策略較多,利用策略模式往往會有重疊的部分,就可以使用建造者模式來實(shí)現(xiàn)。比如這是一個生產(chǎn)中生成復(fù)雜Excel的Demo:https://liruilong.blog.csdn.net/article/details/113191009
如何實(shí)現(xiàn)建造者
這個一個通過建造者設(shè)計(jì)模式生成實(shí)例的Demo。這樣寫的好處:
一是相比傳統(tǒng)的先使用構(gòu)造函數(shù)生成實(shí)例,然后通過setter 方法一個個賦值來講,他是一個整體,即整個構(gòu)建過程是一個鏈?zhǔn)秸{(diào)用,傳統(tǒng)的構(gòu)造方式,構(gòu)造過程被分配到了個多個setter方法調(diào)用中,在構(gòu)造過程中 Bean 可能處于一種不一致的狀態(tài),真的出現(xiàn)這種情況往往很難調(diào)試
另一種情況,這樣的寫法,可以把一個Bean定義為不可變得,而使用傳統(tǒng)的形式則很難實(shí)現(xiàn)。
package com.liruilong.common.demo;
/**
* @Auther Liruilong
* @Date 2020/8/4 12:34
* @Description:
*/
public class Demo {
private final int a;
private final int d;
private Demo(Bulider bulider) {
a = bulider.a;
d = bulider.d;
}
public static class Bulider {
private int a;
private int d;
public Bulider(int a) {
this.a = a;
}
public Demo build() {
return new Demo(this);
}
public Bulider setA(int a) {
this.a = a;
return this;
}
public Bulider setD(int d) {
this.d = d;
return this;
}
}
public static void main(String[] args) {
Demo build = new Demo.Bulider(3).setD(4).build();
}
}
嗯,創(chuàng)建型前三個設(shè)計(jì)模式就和小伙伴們分享到這里,之前有時間會陸續(xù)分享剩下的2個,生活加油
博文參考
《Java并發(fā)編程詳解》
《JavaScript高級程序設(shè)計(jì)》(第3版)
《Effective Java》 (中文版第3版)
《Head First設(shè)計(jì)模式》(中文版)
《設(shè)計(jì)模式 可復(fù)用面向?qū)ο筌浖幕A(chǔ)》(中文版)
作者:山河已無恙
歡迎關(guān)注微信公眾號 :山河已無恙