設計模式之美 - 49 | 橋接模式:如何實現支持不同類型和渠道的消息推送系統?

這系列相關博客,參考 設計模式之美

設計模式之美 - 49 | 橋接模式:如何實現支持不同類型和渠道的消息推送系統?

上一節課我們學習了第一種結構型模式:代理模式。它在不改變原始類(或者叫被代理類)代碼的情況下,通過引入代理類來給原始類附加功能。代理模式在平時的開發經常被用到,常用在業務系統中開發一些非功能性需求,比如:監控、統計、鑑權、限流、事務、冪等、日誌。

今天,我們再學習另外一種結構型模式:橋接模式。橋接模式的代碼實現非常簡單,但是理解起來稍微有點難度,並且應用場景也比較侷限,所以,相當於代理模式來說,橋接模式在實際的項目中並沒有那麼常用,你只需要簡單瞭解,見到能認識就可以,並不是我們學習的重點。

話不多說,讓我們正式開始今天的學習吧!

橋接模式的原理解析

橋接模式,也叫作橋樑模式,英文是 Bridge Design Pattern。這個模式可以說是 23種設計模式中最難理解的模式之一了。我查閱了比較多的書籍和資料之後發現,對於這個模式有兩種不同的理解方式。

當然,這其中“最純正”的理解方式,當屬 GoF 的《設計模式》一書中對橋接模式的定義。畢竟,這 23 種經典的設計模式,最初就是由這本書總結出來的。在 GoF 的《設計模式》一書中,橋接模式是這麼定義的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻譯成中文就是:“將抽象和實現解耦,讓它們可以獨立變化。”

關於橋接模式,很多書籍、資料中,還有另外一種理解方式:“一個類存在兩個(或多個)獨立變化的維度,我們通過組合的方式,讓這兩個(或多個)維度可以獨立進行擴展。”通過組合關係來替代繼承關係,避免繼承層次的指數級爆炸。這種理解方式非常類似於,我們之前講過的“組合優於繼承”設計原則,所以,這裏我就不多解釋了。我們重點看下 GoF 的理解方式。

GoF 給出的定義非常的簡短,單憑這一句話,估計沒幾個人能看懂是什麼意思。所以,我們通過 JDBC 驅動的例子來解釋一下。JDBC 驅動是橋接模式的經典應用。我們先來看一下,如何利用 JDBC 驅動來查詢數據庫。具體的代碼如下所示:

Class.forName("com.mysql.jdbc.Driver");//加載及註冊JDBC驅動程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
	rs.getString(1);
	rs.getInt(2);
}

如果我們想要把 MySQL 數據庫換成 Oracle 數據庫,只需要把第一行代碼中的com.mysql.jdbc.Driver 換成 oracle.jdbc.driver.OracleDriver 就可以了。當然,也有更靈活的實現方式,我們可以把需要加載的 Driver 類寫到配置文件中,當程序啓動的時候,自動從配置文件中加載,這樣在切換數據庫的時候,我們都不需要修改代碼,只需要修改配置文件就可以了。

不管是改代碼還是改配置,在項目中,從一個數據庫切換到另一種數據庫,都只需要改動很少的代碼,或者完全不需要改動代碼,那如此優雅的數據庫切換是如何實現的呢?

源碼之下無祕密。要弄清楚這個問題,我們先從 com.mysql.jdbc.Driver 這個類的代碼看起。我摘抄了部分相關代碼,放到了這裏,你可以看一下。

package com.mysql.jdbc;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver
	static {
		try {
			java.sql.DriverManager.registerDriver(new Driver());
		} catch (SQLException E) {
			throw new RuntimeException("Can't register driver!");
		}
	}

	/**
	* Construct a new driver and register it with DriverManager
	* @throws SQLException if a database error occurs.
	*/
	public Driver() throws SQLException {
		// Required for Class.forName().newInstance()
	}
}

結合 com.mysql.jdbc.Driver 的代碼實現,我們可以發現,當執行Class.forName(“com.mysql.jdbc.Driver”) 這條語句的時候,實際上是做了兩件事情。第一件事情是要求 JVM 查找並加載指定的 Driver 類,第二件事情是執行該類的靜態代碼,也就是將 MySQL Driver 註冊到 DriverManager 類中。

現在,我們再來看一下,DriverManager 類是幹什麼用的。具體的代碼如下所示。當我們把具體的 Driver 實現類(比如,com.mysql.jdbc.Driver)註冊到 DriverManager 之後,後續所有對 JDBC 接口的調用,都會委派到對具體的 Driver 實現類來執行。而Driver 實現類都實現了相同的接口(java.sql.Driver ),這也是可以靈活切換 Driver 的原因。

public class DriverManager {
	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers =
	
	//...
	static {
		loadInitialDrivers();
		println("JDBC DriverManager initialized");
	}
	//...
	
	public static synchronized void registerDriver(java.sql.Driver driver) thr
		if (driver != null) {
			registeredDrivers.addIfAbsent(new DriverInfo(driver));
		} else {
			throw new NullPointerException();
		}
	}
	
	public static Connection getConnection(String url, String user, String pas
		java.util.Properties info = new java.util.Properties();
		if (user != null) {
			info.put("user", user);
		}
		if (password != null) {
			info.put("password", password);
		}
		return (getConnection(url, info, Reflection.getCallerClass()));
	}
	//...
}

橋接模式的定義是“將抽象和實現解耦,讓它們可以獨立變化”。那弄懂定義中“抽象”和“實現”兩個概念,就是理解橋接模式的關鍵。那在 JDBC 這個例子中,什麼是“抽象”?什麼是“實現”呢?

實際上,JDBC 本身就相當於“抽象”。注意,這裏所說的“抽象”,指的並非“抽象類”或“接口”,而是跟具體的數據庫無關的、被抽象出來的一套“類庫”。具體的Driver(比如,com.mysql.jdbc.Driver)就相當於“實現”。注意,這裏所說的“實現”,也並非指“接口的實現類”,而是跟具體數據庫相關的一套“類庫”。JDBC 和Driver 獨立開發,通過對象之間的組合關係,組裝在一起。JDBC 的所有邏輯操作,最終都委託給 Driver 來執行。

我畫了一張圖幫助你理解,你可以結合着我剛纔的講解一塊看。
在這裏插入圖片描述

橋接模式的應用舉例

在第 16 節中,我們講過一個 API 接口監控告警的例子:根據不同的告警規則,觸發不同類型的告警。告警支持多種通知渠道,包括:郵件、短信、微信、自動語音電話。通知的緊急程度有多種類型,包括:SEVERE(嚴重)、URGENCY(緊急)、NORMAL(普通)、TRIVIAL(無關緊要)。不同的緊急程度對應不同的通知渠道。比如,SERVE(嚴重)級別的消息會通過“自動語音電話”告知相關人員。

在當時的代碼實現中,關於發送告警信息那部分代碼,我們只給出了粗略的設計,現在我們來一塊實現一下。我們先來看最簡單、最直接的一種實現方式。代碼如下所示:

public enum NotificationEmergencyLevel {
	SEVERE, URGENCY, NORMAL, TRIVIAL
}

public class Notification {
	private List<String> emailAddresses;
	private List<String> telephones;
	private List<String> wechatIds;
	
	public Notification() {}
	
	public void setEmailAddress(List<String> emailAddress) {
		this.emailAddresses = emailAddress;
	}
	
	public void setTelephones(List<String> telephones) {
		this.telephones = telephones;
	}
	
	public void setWechatIds(List<String> wechatIds) {
		this.wechatIds = wechatIds;
	}
	
	public void notify(NotificationEmergencyLevel level, String message) {
		if (level.equals(NotificationEmergencyLevel.SEVERE)) {
			//...自動語音電話
		} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
			//...發微信
		} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
			//...發郵件
		} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
			//...發郵件
		}
	}
}

//在API監控告警的例子中,我們如下方式來使用Notification類:
public class ErrorAlertHandler extends AlertHandler {
	public ErrorAlertHandler(AlertRule rule, Notification notification){
		super(rule, notification);
	}
	
	@Override
	public void check(ApiStatInfo apiStatInfo) {
		if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi
			notification.notify(NotificationEmergencyLevel.SEVERE, "...");
		}
	}
}

Notification 類的代碼實現有一個最明顯的問題,那就是有很多 if-else 分支邏輯。實際上,如果每個分支中的代碼都不復雜,後期也沒有無限膨脹的可能(增加更多 if-else 分支判斷),那這樣的設計問題並不大,沒必要非得一定要摒棄 if-else 分支邏輯。

不過,Notification 的代碼顯然不符合這個條件。因爲每個 if-else 分支中的代碼邏輯都比較複雜,發送通知的所有邏輯都扎堆在 Notification 類中。我們知道,類的代碼越多,就越難讀懂,越難修改,維護的成本也就越高。很多設計模式都是試圖將龐大的類拆分成更細小的類,然後再通過某種更合理的結構組裝在一起。

針對 Notification 的代碼,我們將不同渠道的發送邏輯剝離出來,形成獨立的消息發送類(MsgSender 相關類)。其中,Notification 類相當於抽象,MsgSender 類相當於實現,兩者可以獨立開發,通過組合關係(也就是橋樑)任意組合在一起。所謂任意組合的意思就是,不同緊急程度的消息和發送渠道之間的對應關係,不是在代碼中固定寫死的,我們可以動態地去指定(比如,通過讀取配置來獲取對應關係)。

按照這個設計思路,我們對代碼進行重構。重構之後的代碼如下所示:

public interface MsgSender {
	void send(String message);
}

public class TelephoneMsgSender implements MsgSender {
	private List<String> telephones;
	
	public TelephoneMsgSender(List<String> telephones) {
		this.telephones = telephones;
	}
	
	@Override
	public void send(String message) {
		//...
	}
}

public class EmailMsgSender implements MsgSender {
	// 與TelephoneMsgSender代碼結構類似,所以省略...
}

public class WechatMsgSender implements MsgSender {
	// 與TelephoneMsgSender代碼結構類似,所以省略...
}

public abstract class Notification {
	protected MsgSender msgSender;
	
	public Notification(MsgSender msgSender) {
		this.msgSender = msgSender;
	}
	
	public abstract void notify(String message);
}

public class SevereNotification extends Notification {
	public SevereNotification(MsgSender msgSender) {
		super(msgSender);
	}
	
	@Override
	public void notify(String message) {
		msgSender.send(message);
	}
}
public class UrgencyNotification extends Notification {
	// 與SevereNotification代碼結構類似,所以省略...
}

public class NormalNotification extends Notification {
	// 與SevereNotification代碼結構類似,所以省略...
}

public class TrivialNotification extends Notification {
	// 與SevereNotification代碼結構類似,所以省略...
}

重點回顧

好了,今天的內容到此就講完了。我們一塊來總結回顧一下,你需要重點掌握的內容。

總體上來講,橋接模式的原理比較難理解,但代碼實現相對簡單。

對於這個模式有兩種不同的理解方式。在 GoF 的《設計模式》一書中,橋接模式被定義爲:“將抽象和實現解耦,讓它們可以獨立變化。”在其他資料和書籍中,還有另外一種更加簡單的理解方式:“一個類存在兩個(或多個)獨立變化的維度,我們通過組合的方式,讓這兩個(或多個)維度可以獨立進行擴展。”

對於第一種 GoF 的理解方式,弄懂定義中“抽象”和“實現”兩個概念,是理解它的關鍵。定義中的“抽象”,指的並非“抽象類”或“接口”,而是被抽象出來的一套“類庫”,它只包含骨架代碼,真正的業務邏輯需要委派給定義中的“實現”來完成。而定義中的“實現”,也並非“接口的實現類”,而是的一套獨立的“類庫”。“抽象”和“實現”獨立開發,通過對象之間的組合關係,組裝在一起。

對於第二種理解方式,它非常類似我們之前講過的“組合優於繼承”設計原則,通過組合關係來替代繼承關係,避免繼承層次的指數級爆炸。

課堂討論

在橋接模式的第二種理解方式的第一段代碼實現中,Notification 類中的三個成員變量通過 set 方法來設置,但是這樣的代碼實現存在一個明顯的問題,那就是emailAddresses、telephones、wechatIds 中的數據有可能在 Notification 類外部被修改,那如何重構代碼才能避免這種情況的發生呢?

public class Notification {
	private List<String> emailAddresses;
	private List<String> telephones;
	private List<String> wechatIds;
	
	public Notification() {}
	
	public void setEmailAddress(List<String> emailAddress) {
		this.emailAddresses = emailAddress;
	}
	
	public void setTelephones(List<String> telephones) {
		this.telephones = telephones;
	}
	
	public void setWechatIds(List<String> wechatIds) {
		this.wechatIds = wechatIds;
	}
	//...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章