OOD&OOP-桥接模式

桥接模式

在 GoF 的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化。”

关于桥接模式,很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。

JDBC 驱动是桥接模式的经典应用。我们以如何利用 JDBC 驱动来查询数据库为例:

Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
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 = new CopyOnWriteArrayList<DriverInfo>();

  //...
  static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
  }
  //...

  public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
    if (driver != null) {
      registeredDrivers.addIfAbsent(new DriverInfo(driver));
    } else {
      throw new NullPointerException();
    }
  }

  public static Connection getConnection(String url, String user, String password) throws SQLException {
    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 来执行。

以一个 API 接口监控告警为例,根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。通知的紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。不同的紧急程度对应不同的通知渠道。比如,SERVE(严重)级别的消息会通过“自动语音电话”告知相关人员。

最简单、最直接的一种实现方式代码如下所示:

#include<list>
#include<string>
enum class NotificationEmergencyLevel {
	SEVERE, URGENCY, NORMAL, TRIVIAL
};

class Notification {
private:
	std::list<std::string> emailAddresses;
	std::list<std::string> telephones;
	std::list<std::string> wechatIds;
	
public:
	void setAddresses(std::list<std::string> emailAddresses_)
	{
		emailAddresses = emailAddresses_;
	}
	void setTelephones(std::list<std::string> telephones_)
	{
		telephones = telephones_;
	}
	void setWechatIds(std::list<std::string> wechatIds_)
	{
		wechatIds = wechatIds_;
	}
	void notify(NotificationEmergencyLevel level, std::string message)
	{
		if (level == NotificationEmergencyLevel::SEVERE)
		{
			//自动语音电话...
		}
		else if (level == NotificationEmergencyLevel::NORMAL)
		{
			//发邮件...
		}
		else if (level == NotificationEmergencyLevel::URGENCY)
		{
			//发微信...
		}
		else if (level == NotificationEmergencyLevel::TRIVIAL)
		{
			//发邮件...
		}
	}
};

Notification 类的代码实现有一个最明显的问题,那就是有很多 if-else 分支逻辑。而且每个 if-else 分支中的代码逻辑都比较复杂,发送通知的所有逻辑都扎堆在 Notification 类中。我们知道,类的代码越多,就越难读懂,越难修改,维护的成本也就越高。很多设计模式都是试图将庞大的类拆分成更细小的类,然后再通过某种更合理的结构组装在一起。

**针对 Notification 的代码,我们将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender 相关类)。其中,Notification 类相当于抽象,MsgSender 类相当于实现,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。**所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,我们可以动态地去指定(比如,通过读取配置来获取对应关系)。

#include<list>
#include<string>
enum class NotificationEmergencyLevel {
	SEVERE, URGENCY, NORMAL, TRIVIAL
};

class MsgSender {
public:
	virtual void send(std::string message) = 0;
	virtual ~MsgSender() {}
};

class emailMsgSender :public MsgSender {
private:
	std::list<std::string> emailAddresses;
public:
	void setEmail(std::list<std::string> emailAddresses_)
	{
		emailAddresses = emailAddresses_;
	}
	void send(std::string message) override{
		//...
	}
};

class telephoneMsgSender :public MsgSender {
	std::list<std::string> telephones;
public:
	void setTelephones(std::list<std::string> telephones_)
	{
		telephones = telephones_;
	}
	void send(std::string message)override {
		//...
	}
};
class wechatMsgSender : public MsgSender {
private:
	std::list<std::string> wechatIds;
public:
	void setWechatIds(std::list<std::string> wechatIds_)
	{
		wechatIds = wechatIds_;
	}
	void send(std::string message)override {
		//...
	}
};

class Notification {
protected:
	MsgSender* msgSender;
	
public:
	Notification(MsgSender* msgSend_)
	{
		msgSender = msgSend_;
	}
	virtual void notify(std::string message) = 0;

};

class EmergencyNotification : public Notification {
public:
	EmergencyNotification(MsgSender* msgSend_) :Notification(msgSend_) {}
	void notify(std::string message) {
		msgSender->send(message);
	}
};

class UrgencyNotification : public Notification {
public:
	UrgencyNotification(MsgSender* msgSend_) :Notification(msgSend_) {}
	void notify(std::string message) {
		msgSender->send(message);
	}
};

class NormalNotification : public Notification {
public:
	NormalNotification(MsgSender* msgSend_) :Notification(msgSend_) {}
	void notify(std::string message) {
		msgSender->send(message);
	}
};

class TrivialNotification : public Notification {
public:
	TrivialNotification(MsgSender* msgSend_) :Notification(msgSend_) {}
	void notify(std::string message) {
		msgSender->send(message);
	}
};

紧急程度和警报的方式可以是两个不同的纬度。可以有不同的组合方式。这与slf4j这一日志门面的设计有异曲同工之妙。slf4j其中有三个核心概念,logger,appender和encoder。分别指这个日志记录器负责哪个类的日志,日志打印到哪里以及日志打印的格式。三个纬度上可以有不同的实现,使用者可以在每一纬度上定义多个实现,配置文件中将各个纬度的某一个实现组合在一起就ok了。
桥接就是面向接口编程的集大成者。面向接口编程只是说在系统的某一个功能上将接口和实现解藕,而桥接是详细的分析系统功能,将各个独立的纬度都抽象出来,使用时按需组合。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值