Principle analysis
Bridge mode, also known as bridge mode, is Bridge Design Pattern in English. This pattern can be said to be one of the most difficult to understand of the 23 design patterns. This model has two different understanding modes:
- First, in GoF's design patterns, it is understood as "decoupling abstraction and implementation so that they can change independently"
- Second, a class has two (or more) dimensions that change independently. We can expand these two (or more) dimensions independently through combination. The combination relationship replaces the inheritance relationship to avoid the exponential explosion of inheritance level
JDBC driver is a classic application of bridge mode. How it is used:
Class.forName("com.mysql.jdbc.Driver");//Loading and registering JDBC drivers String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_pas" 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); }
If we want to replace MySQL database with Oracle database, we only need to replace com. In the first line of code mysql. jdbc. Replace driver with Oracle jdbc. driver. Oracledriver is OK.
How does this elegant database switching work?
com. mysql. jdbc. The source code of driver is as follows:
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() } }
Combined with com mysql. jdbc. Driver code implementation, we can find that when executing class When using the forname ("com.mysql.jdbc.Driver") statement, you actually do two things. The first thing is to ask the JVM to find and load the specified driver class. The second thing is to execute the static code of this class, that is, register the MySQL Driver into the DriverManager class.
What does DriverManager do? The specific code is as follows. After registering the specific driver implementation class (for example, com.mysql.jdbc.Driver) with the driver manager, all subsequent calls to the JDBC interface will be delegated to the specific driver implementation class for execution. The driver implementation classes all implement the same interface (java.sql.Driver), which is also the reason why the driver can be switched flexibly.
public class DriverManager { private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new xxxx(); //... static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } //... public static synchronized void registerDriver(java.sql.Driver driver) throws if (driver != null) { registeredDrivers.addIfAbsent(new DriverInfo(driver)); } else { throw new NullPointerException(); } } public static Connection getConnection(String url, String user, String passwo 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())); } //... }
Bridging patterns are defined as "decoupling abstractions and implementations so that they can change independently". Understanding the concepts of "abstraction" and "implementation" in the definition is the key to understanding the bridge pattern. In the JDBC example, what is "abstraction"? What is "realization"?
In fact, JDBC itself is equivalent to "abstraction". Note that "abstraction" here does not refer to "abstract class" or "interface", but a set of abstract "class libraries" that are independent of the specific database. A specific driver (for example, com.mysql.jdbc.Driver) is equivalent to "implementation". Note that the "implementation" mentioned here is not the "implementation class of the interface", but a set of "class library" related to the specific database. JDBC and driver are developed independently and assembled together through the composition relationship between objects. All the logical operations of JDBC are ultimately delegated to the driver.
Application examples
The following is an example of API interface monitoring alarms: different types of alarms are triggered according to different alarm rules. The alarm supports a variety of notification channels, including email, SMS, wechat and automatic voice phone. There are many kinds of URGENCY of notification, including SEVERE, emergency, NORMAL and trivia. Different emergency levels correspond to different notification channels. For example, the message of SEVERE level will be informed to relevant personnel through "automatic voice phone".
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)) { //... Automatic voice telephone } else if (level.equals(NotificationEmergencyLevel.URGENCY)) { //... Send wechat } else if (level.equals(NotificationEmergencyLevel.NORMAL)) { //... send emails } else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) { //... send emails } } } //In the example of API monitoring alarms, we use the Notification class as follows: 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, "..."); } } }
(the three member variables in the Notification class are set through the set method, but there is an obvious problem in this code implementation, that is, the data in emailAddresses, telephones and wechatIds may be modified outside the Notification class. How can we avoid this situation by reconstructing the code? If there are few parameters, you can initialize in the constructor (if you don't want to be modified, don't expose the set interface. In this way, initialize these emails, put the work of telephone and wechat into the constructor, and use the constructor to initialize these variables. After initialization, it can't be modified outside under normal circumstances.), If there are many parameters, you can use the builder mode to initialize.)
The most obvious problem with the code implementation of Notification class is that there are many if else branch logic. In fact, if the code in each branch is not complex and there is no possibility of infinite expansion in the later stage (adding more if else branch judgment), such a design problem is not big, and there is no need to abandon the if else branch logic.
However, the code of Notification obviously does not meet this condition. Because the code logic in each if else branch is complex, all the logic for sending notifications is clustered in the Notification class. We know that the more code of a class, the harder it is to read and modify, and the higher the maintenance cost. Many design patterns try to split huge classes into smaller classes, and then assemble them through a more reasonable structure.
For the Notification code, we separate the sending logic of different channels to form an independent message sending class (MsgSender related class). Among them, Notification class is equivalent to abstraction and MsgSender class is equivalent to implementation. The two can be developed independently and combined arbitrarily through composition relationship (i.e. bridge). The so-called arbitrary combination means that the correspondence between messages with different urgency and transmission channels is not written in the code, but can be specified dynamically (for example, obtain the correspondence by reading the configuration)
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 { // Similar to the code structure of TelephoneMsgSender, so omit } public class WechatMsgSender implements MsgSender { // Similar to the code structure of TelephoneMsgSender, so omit } 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 { // Similar to the severnotification code structure, so omit } public class NormalNotification extends Notification { // Similar to the severnotification code structure, so omit } public class TrivialNotification extends Notification { // Similar to the severnotification code structure, so omit }
summary
Generally speaking, the principle of bridge mode is difficult to understand, but the code implementation is relatively simple.
There are two different ways to understand this model. In GoF's book design patterns, bridging patterns are defined as "decoupling abstraction and implementation so that they can change independently." In other materials and books, there is another simpler way to understand: "a class has two (or more) independently changing dimensions. We combine them so that the two (or more) dimensions can be expanded independently."
- For the first way of understanding GoF, understanding the concepts of "abstraction" and "implementation" in the definition is the key to understanding it. The "abstraction" in the definition does not refer to "abstract class" or "interface", but a set of "class library" abstracted. It only contains skeleton code, and the real business logic needs to be delegated to the "implementation" in the definition. The "implementation" in the definition is not the "implementation class of the interface", but an independent "class library". "Abstract" and "implementation" are developed independently and assembled together through the composition relationship between objects.
- For the second way of understanding, it is very similar to the design principle of "combination is better than inheritance" we talked about before. It replaces the inheritance relationship through the combination relationship to avoid the exponential explosion of the inheritance level.