Java 設計模式 輕讀彙總版
網上設計模式的文章較多,本篇主要是自己總結學習用,力求簡單,易於掌握。文章大量參考網路資源,主要有https://javadoop.com/post/design-pattern 一文和 《設計模式之禪》一書。
文章目錄
一 建立類模式
1 單例模式
定義
確保某一個類只有一個例項,並且自行例項化並向整個系統提供這個例項.
各種單例寫法對比
單例寫法 | 單例保障機制 | 單例物件初始化時機 | 優點 | 缺點 |
---|---|---|---|---|
餓漢模式 | 類載入機制 | 類載入 | 簡單,易理解 | 難以保證懶載入,無法應對反射和反序列化 |
雙重校驗鎖(DCL) | 鎖機制(需volatile防止重排序) | 第一次呼叫getInstance() | 實現懶載入 | 複雜,無法應對反射和反序列化 |
Holder模式(靜態內部類) | 類載入機制 | 第一次呼叫getInstance() | 實現懶載入 | 無法應對反射和反序列化 |
列舉 | 列舉語言特性 | 第一次引用列舉物件 | 簡潔,安全(語言級別防止通過反射和反序列化破壞單例) | enum的另類用法 |
拓展點
- 雙重校驗鎖:雙重的原因和volatile關鍵字
- 餓漢模式:單例 final 關鍵字在併發情況下的作用,static 關鍵字修飾在類載入機制中的時機
- Holder模式:類載入條件
- enum:enum的相關用法
參考
2 工廠模式(含簡單工廠)
定義
定義一個用於建立物件的介面,讓子類決定例項化哪一個類。工廠方法使一個類的例項化延遲到其子類。
簡單工廠(靜態工廠模式)
只需一個工廠的時候使用。
一個工廠類 XxxFactory,裡面有一個靜態方法,根據我們不同的引數,返回不同的派生自同一個父類(或實現同一介面)的例項物件
程式碼
public class FoodFactory {
public static Food makeFood(String name) {
if (name.equals("noodle")) {
Food noodle = new LanZhouNoodle();
noodle.addSpicy("more");
return noodle;
} else if (name.equals("chicken")) {
Food chicken = new HuangMenChicken();
chicken.addCondiment("potato");
return chicken;
} else {
return null;
}
}
}
工廠模式
需要多個工廠的時候使用。
將工廠類進行抽象提取。
核心在於,我們需要在第一步選好我們需要的工廠。比如,我們有 LogFactory 介面,實現類有 FileLogFactory 和 KafkaLogFactory,分別對應將日誌寫入檔案和寫入 Kafka 中,顯然,我們客戶端第一步就需要決定到底要例項化 FileLogFactory 還是 KafkaLogFactory,這將決定之後的所有的操作。
例圖
3 抽象工廠模式
定義
為建立一組相關或相互依賴的物件提供一個介面,而且無須指定它們的具體類。
一般是涉及物件族的時候使用,遮蔽一個物件族的相同約束。缺點是很難拓展。
例子
使用普通工廠模式
// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();
// 得到 AMD 的主機板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();
// 組裝 CPU 和主機板
Computer computer = new Computer(cpu, mainBoard);
單獨看 CPU 工廠和主機板工廠,它們分別是前面我們說的工廠模式。這種方式也容易擴充套件,因為要給電腦加硬碟的話,只需要加一個 HardDiskFactory 和相應的實現即可,不需要修改現有的工廠。
但是,這種方式有一個問題,那就是如果 Intel 家產的 CPU 和 AMD 產的主機板不能相容使用,那麼這程式碼就容易出錯,因為客戶端並不知道它們不相容,也就會錯誤地出現隨意組合。
下面就是我們要說的產品族的概念,它代表了組成某個產品的一系列附件的集合:
使用抽象工廠模式
當涉及到這種產品族的問題的時候,就需要抽象工廠模式來支援了。我們不再定義 CPU 工廠、主機板工廠、硬碟工廠、顯示屏工廠等等,我們直接定義電腦工廠,每個電腦工廠負責生產所有的裝置,這樣能保證肯定不存在相容問題。
這個時候,對於客戶端來說,不再需要單獨挑選 CPU廠商、主機板廠商、硬碟廠商等,直接選擇一家品牌工廠,品牌工廠會負責生產所有的東西,而且能保證肯定是相容可用的。
public static void main(String[] args) {
// 第一步就要選定一個“大廠”
ComputerFactory cf = new AmdFactory();
// 從這個大廠造 CPU
CPU cpu = cf.makeCPU();
// 從這個大廠造主機板
MainBoard board = cf.makeMainBoard();
// 從這個大廠造硬碟
HardDisk hardDisk = cf.makeHardDisk();
// 將同一個廠子出來的 CPU、主機板、硬碟組裝在一起
Computer result = new Computer(cpu, board, hardDisk);
}
當然,抽象工廠的問題也是顯而易見的,比如我們要加個顯示器,就需要修改所有的工廠,給所有的工廠都加上製造顯示器的方法。這有點違反了對修改關閉,對擴充套件開放這個設計原則。
4 建造者模式
定義
將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示
常見使用形式
Food food = Food.builder().a().b().c().build();
寫法
核心:使用一個具有相關屬性的靜態內部類Builder,先把所有的屬性都設定給 Builder,然後 build() 方法的時候,將這些屬性複製給實際產生的物件。並可在 build() 的時候做自定義檢查。
簡易版
使用Lombok
@Builder
class User {
private String name;
private String password;
private String nickName;
private int age;
}
標準版
//--------------建造者類----------------------------------------
class User {
// 下面是“一堆”的屬性
private String name;
private String password;
private String nickName;
private int age;
// 構造方法私有化,不然客戶端就會直接呼叫構造方法了
private User(String name, String password, String nickName, int age) {
this.name = name;
this.password = password;
this.nickName = nickName;
this.age = age;
}
// 靜態方法,用於生成一個 Builder,這個不一定要有,不過寫這個方法是一個很好的習慣,
// 有些程式碼要求別人寫 new User.UserBuilder().a()...build() 看上去就沒那麼好
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
// 下面是和 User 一模一樣的一堆屬性
private String name;
private String password;
private String nickName;
private int age;
private UserBuilder() {
}
// 鏈式呼叫設定各個屬性值,返回 this,即 UserBuilder
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// build() 方法負責將 UserBuilder 中設定好的屬性“複製”到 User 中。
// 當然,可以在 “複製” 之前做點檢驗
public User build() {
if (name == null || password == null) {
throw new RuntimeException("使用者名稱和密碼必填");
}
if (age <= 0 || age >= 150) {
throw new RuntimeException("年齡不合法");
}
// 還可以做賦予”預設值“的功能
if (nickName == null) {
nickName = name;
}
return new User(name, password, nickName, age);
}
}
}
//---------------------客戶端呼叫----------------------------------//
public class APP {
public static void main(String[] args) {
User d = User.builder()
.name("foo")
.password("pAss12345")
.age(25)
.build();
}
}
5 原型模式
定義
用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件
實質即克隆
Java 實現
Object 類中有一個 clone() 方法,它用於生成一個新的物件,當然,如果我們要呼叫這個方法,java 要求我們的類必須先實現 Cloneable 介面,此介面沒有定義任何方法,但是不這麼做的話,在 clone() 的時候,會丟擲 CloneNotSupportedException 異常。
protected native Object clone() throws CloneNotSupportedException;
注意
java 的克隆是淺克隆,碰到物件引用的時候,克隆出來的物件和原物件中的引用將指向同一個物件。通常實現深克隆的方法是將物件進行序列化,然後再進行反序列化。
二 結構類模式
1 代理模式(委託模式)
定義
為其他物件提供一種代理以控制對這個物件的訪問
例圖
拓展點
動態代理與AOP(Spring),JDK動態代理(需介面),CGLIB(需可繼承)
2 介面卡模式
定義
將一個類的介面變換成客戶端所期待的另一種介面,從而使原本因介面不匹配而無法在一起工作的兩個類能夠在一起工作。
預設介面卡模式
對於有多個方法的介面,可以提供一個預設實現(空實現)的預設介面卡實現類,這樣使用者只需繼承這個介面卡類然後重寫個別需要用到的方法即可。
物件介面卡模式
類介面卡模式
通過繼承的方法,介面卡自動獲得了所需要的大部分方法。這個時候,客戶端使用更加簡單,直接 Target t = new SomeAdapter(); 就可以了。
注意點
- 類適配和物件適配的異同
一個採用繼承,一個採用組合;
類適配屬於靜態實現,物件適配屬於組合的動態實現,物件適配需要多例項化一個物件。
總體來說,物件適配用得比較多。 - 介面卡模式和代理模式的異同
在程式碼結構上,它們很相似,都需要一個具體的實現類的例項。但是它們的目的不一樣,代理模式做的是增強原方法的活;介面卡做的是適配的活,為的是提供“把雞包裝成鴨,然後當做鴨來使用”,而雞和鴨它們之間原本沒有繼承關係。
3 裝飾模式
定義
動態地給一個物件新增一些額外的職責。就增加功能來說,裝飾模式相比生成子類更加靈活。
結構圖
所有的具體裝飾者們 ConcreteDecorator 都可以作為 Component 來使用,因為它們都實現了 Component 中的所有介面。它們和 Component 實現類 ConcreteComponent 的區別是,它們只是裝飾者,起裝飾作用,也就是即使它們看上去牛逼轟轟,但是它們都只是在具體的實現中加了層皮來裝飾而已。(通常在構造方法中傳入被包裝的基類Component)
例圖
Java IO 中的裝飾模式
4 門面模式(外觀模式)
定義
要求一個子系統的外部與其內部的通訊必須通過一個統一的物件進行。門面模式提供一個高層次的介面,使得子系統更加易於使用。
提供“統一的物件”給外部訪問,不允許有任何直接訪問子系統的行為發生,力求“金玉其表”
需要注意門面模式不符合開閉原則,難以拓展(因為無法直接訪問內部)。
例圖(slf4j)
通用程式碼
//--------------------------------子系統-----------------------------------
public class ClassA{
public void dosomethingA(){}
}
public class ClassB{
public void dosomethingB(){}
}
public class ClassC{
public void dosomethingC(){}
}
//----------------------------門面模式-------------------------------------
public class Facade{
//被委託的物件
private ClassA a=new ClassA();
private ClassB a=new ClassB();
private ClassC a=new ClassC();
//提供給外部訪問的方法
public void methodA(){
this.a.doSomethingA();
}
public void methodB(){
this.b.doSomethingB();
}
public void methodC(){
this.c.doSomethingC();
}
}
5 橋樑模式(橋接模式)
定義
將抽象和實現解耦,使得兩者可以獨立地變化
即把會變化的實現定義成一個介面Implementor(橋樑)
結構圖
例圖
6 組合模式(合成模式 / 部分-整體模式)
定義
將物件組合成樹形結構以表示“部分-整體”的層次結構,使得使用者對單個物件和組合物件的使用具有一致性。
當你發現需求中是體現部分與整體層次的結構時,以及你希望使用者可以忽略組合物件與單個物件的不同,統一地使用組合結構中的所有物件時,就應該考慮使用組合模式了。
例子
每個員工都有姓名、部門、薪水這些屬性,同時還有下屬員工集合(雖然可能集合為空),而下屬員工和自己的結構是一樣的,也有姓名、部門這些屬性,同時也有他們的下屬員工集合。
public class Employee {
private String name;
private String dept;
private int salary;
private List<Employee> subordinates; // 下屬
public Employee(String name,String dept, int sal) {
this.name = name;
this.dept = dept;
this.salary = sal;
subordinates = new ArrayList<Employee>();
}
public void add(Employee e) {
subordinates.add(e);
}
public void remove(Employee e) {
subordinates.remove(e);
}
public List<Employee> getSubordinates(){
return subordinates;
}
public String toString(){
return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]");
}
}
7 享元模式
定義
使用共享物件可有效地支援大量的細粒度的物件
理解
每個事物都是不同的,但是又有一定的共性,如果只有完全相同的事物才能共享,那麼享元模式可以說就是不可行的;
因此我們應該儘量將事物的共性共享,而又保留它的個性。為了做到這點,享元模式中區分了內部狀態/內蘊狀態(Internal State)和外部狀態/外蘊狀態(External State)。內部狀態就是共性,外部狀態就是個性了。
內部狀態儲存在享元內部,不會隨環境的改變而有所不同,是可以共享的;
外部狀態是不可以共享的,它隨環境的改變而改變的,因此外部狀態是由客戶端來保持(因為環境的變化是由客戶端引起的)。
在每個具體的環境下,客戶端將外部狀態傳遞給享元,從而建立不同的物件出來。
(外部狀態一般用基本型別或String,如果外部狀態也用類來表示則往往得不償失)
因為把外部狀態的管理交由客戶端,故享元模式主要適用於數量多的、性質相近(外部狀態少)的物件。
結構圖
例子
//----------------------------抽象享元單元(Flyweight)----------------------
public interface Flyweight
{
public void operation(String state);
}
//----------------------------具體享元單元(ConcreteFlyweight )----------------------
public class ConcreteFlyweight implements Flyweight
{
private String str;
public ConcreteFlyweight(String str)
{
this.str = str;
}
@Override
public void operation(String state)
{
System.out.println("內蘊狀態:"+str);
System.out.println("外蘊狀態:"+state);
}
}
//----------------------------享元工廠(FlyWeightFactory)----------------------
public class FlyWeightFactory
{
private Map<String,ConcreteFlyweight> flyWeights = new HashMap<String, ConcreteFlyweight>();
public ConcreteFlyweight factory(String str)
{
ConcreteFlyweight flyweight = flyWeights.get(str);
if(null == flyweight)
{
flyweight = new ConcreteFlyweight(str);
flyWeights.put(str, flyweight);
}
return flyweight;
}
public int getFlyWeightSize()
{
return flyWeights.size();
}
}
測試如下,可以看到 “a fly weight” 是作為外部狀態傳遞給 f1的operation 方法的。對於這種僅需臨時使用的物件,並不需要自己維持其外部狀態,就比較適合使用享元模式。
//----------------------------測試程式碼----------------------
FlyWeightFactory factory = new FlyWeightFactory();
Flyweight f1 = factory.factory("a");
Flyweight f2 = factory.factory("b");
Flyweight f3 = factory.factory("a");
f1.operation("a fly weight");
f2.operation("b fly weight");
f3.operation("c fly weight");
System.out.println(f1 == f3);
System.out.println(factory.getFlyWeightSize());
//----------------------------測試結果----------------------
內蘊狀態:a
外蘊狀態:a fly weight
內蘊狀態:b
外蘊狀態:b fly weight
內蘊狀態:a
外蘊狀態:c fly weight
true
2
三 行為類模式
1 策略模式
定義
定義一組演算法,把每個演算法都封裝起來,並且使它們之間可以互換。
例圖
策略模式和橋樑模式的區別
策略模式更加簡單,橋樑模式則是在Strategy使用類多加了一層抽象。
2 觀察者模式
定義
定義物件間一種一對多的依賴關係,使得每當一個物件改變狀態,則所有依賴於它的物件都會得到通知並被自動更新。
被觀察者(Subject)內部維護了一個觀察者(Observer)列表,當被觀察者執行操作的時候觸發 notify 遍歷 觀察者列表逐個執行 update()操作,相對於觀察者觀察到了主題的變化而執行一定的操作。
注意
JDK提供了 Observable 和 Observer 來代表被觀察者和觀察者。
生產中往往使用訊息中介軟體來實現,這時候往往變成 釋出-訂閱模型,有一些文章強調這兩個模式是不同的。釋出-訂閱模型往往需要一箇中間件(如分散式使用訊息佇列、單機可以使用Guava的EventBus事件匯流排),觀察者向中介軟體訂閱主題,釋出者向中介軟體釋出主題,可以進一步解耦。
結構圖
3 責任鏈模式
定義
使多個物件都有機會處理請求,從而避免了請求的傳送者和接受者之間的耦合關係。將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有物件處理它為止。
模式本質:分離職責,動態組合。分離職責是前提,動態組合是精華所在。
拓展
SpringSecurity的攔截鏈,Netty的處理鏈等
結構圖
例子
<