Bridge (Мост)
Bridge применяется, когда класс необходимо расширить несколькими независимыми (ортогональными) путями.
Да, звучит сложно. Поясним на примере.
Пример
Представим, что у нас есть иерархия классов, которые моделируют работу компьютеров. У нас есть абстрактный класс Computer и абстрактный класс-наследник PC (настольный компьютер).
Также у нас есть две операционные системы: Windows и Linux. Операционные системы представлены в классах PCWithLinux и PCWithWindows.
package naive;
public abstract class Computer {
protected final String name;
protected Computer(String name) {
this.name = name;
}
public abstract void boot();
}javapackage naive;
public abstract class PC extends Computer {
protected PC(String name) {
super(name);
}
@Override
public void boot() {
System.out.println("Booting " + name + "...");
System.out.println("Pressing the power button on the computer case...");
}
}javapackage naive;
public final class PCWithLinux extends PC {
public PCWithLinux(String name) {
super(name);
}
@Override
public void boot() {
super.boot();
bootTheOperatingSystem();
}
private void bootTheOperatingSystem() {
System.out.println("Booting the Linux operating system...");
}
}javapackage naive;
public final class PCWithWindows extends PC {
public PCWithWindows(String name) {
super(name);
}
@Override
public void boot() {
super.boot();
bootTheOperatingSystem();
}
private void bootTheOperatingSystem() {
System.out.println("Booting the Windows operating system...");
}
}javaДля наглядности покажем иерархию классов на схеме:
Пока что проблема не так очевидна. Но что если в нашу иерархию добавится новый тип компьютеров – ноутбук? На ноутбук тоже можно установить обе операционные системы, поэтому иерархия будет выглядеть так:
Мы видим, что для нового класса Laptop нам пришлось создать практически идентичные классы под каждую операционную систему. Каждый раз, когда будет появляться новый вид компьютера, нам придётся создавать по два класса. А если появится новая операционная система, то мы будем вынуждены добавлять по одному классу в каждый вид компьютера. Это выглядит совсем неподдерживаемо.
Проблема в том, что в нашей иерархии на самом деле два концепта – тип компьютера и операционная система. Вместо того чтобы дать этим концептам развиваться независимо, мы смешали их в одну иерархию классов.
Решение – зарефакторить классы в две отдельные иерархии и провести между ними «мост».
package computer;
public interface OperatingSystem {
void boot();
}javapackage computer;
public abstract class Computer {
protected final String name;
private final OperatingSystem operatingSystem;
protected Computer(String name, OperatingSystem operatingSystem) {
this.name = name;
this.operatingSystem = operatingSystem;
}
public void boot() {
System.out.println("Booting " + name + "...");
bootHardware();
operatingSystem.boot();
}
protected abstract void bootHardware();
}javapackage computer;
public final class PC extends Computer {
public PC(String name, OperatingSystem operatingSystem) {
super(name, operatingSystem);
}
@Override
protected void bootHardware() {
System.out.println("Pressing the power button on the computer case...");
}
}javapackage computer;
public final class Laptop extends Computer {
public Laptop(String name, OperatingSystem operatingSystem) {
super(name, operatingSystem);
}
@Override
protected void bootHardware() {
System.out.println("Opening the laptop lid...");
}
}javapackage computer;
public final class Linux implements OperatingSystem {
@Override
public void boot() {
System.out.println("Booting the Linux operating system...");
}
}javapackage computer;
public final class Windows implements OperatingSystem {
@Override
public void boot() {
System.out.println("Booting the Windows operating system...");
}
}javaТеперь мы можем добавить новый тип компьютера в иерархию компьютеров, и это не затронет иерархию операционных систем. И наоборот – добавление новой операционной системы не затрагивает иерархию компьютеров. Мы достигли независимого развития двух разных концептов.
// Комбинируем любой компьютер с любой ОС
Computer pc = new PC("Desktop", new Linux());
Computer laptop = new Laptop("ThinkPad", new Windows());
pc.boot();
laptop.boot();javaComposition over Inheritance
Есть классический принцип – «предпочитайте композицию наследованию». Принцип говорит о том, что наследование во многих случаях неоправданно, и вместо него мы можем выразить связь между двумя классами по-другому – просто передать объект другого класса через конструктор.
Паттерн «Мост» – это ещё один аргумент в пользу этого принципа, поскольку паттерн демонстрирует сложности, которые может доставить неправильное применение наследования.
Реальные примеры
JDBC
JDBC – стандартный API Java для работы с базами данных. Его архитектура разделяет ту же идею, что и паттерн Bridge – отделение API от его реализации, чтобы они могли развиваться независимо.
Абстракция – это интерфейсы java.sql.Connection, Statement, ResultSet. Они предоставляют единый интерфейс для работы с любой базой данных. Реализация – это драйверы конкретных СУБД: com.mysql.cj.jdbc.Driver, org.postgresql.Driver и другие.
package real;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class JdbcExample {
public static void demonstrate() throws Exception {
// Абстракция: код работает с интерфейсами java.sql.*
// Реализация: конкретный драйвер извлекается из URL подключения
try (Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/mydb", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name FROM users")) {
while (rs.next()) {
System.out.println(rs.getInt("id") + ": " + rs.getString("name"));
}
}
}
}javaКлиентский код работает только с интерфейсами java.sql.* и не зависит от конкретной СУБД. Можно переключиться с PostgreSQL на MySQL, изменив только строку подключения – весь остальной код останется прежним.
Две иерархии развиваются независимо. Разработчики PostgreSQL могут выпустить новую версию драйвера, а разработчики JDK – добавить новые методы в Connection. Эти изменения не влияют друг на друга.
database/sql
Стандартная библиотека Go использует тот же подход для работы с базами данных. Пакет database/sql – это абстракция, а пакет database/sql/driver определяет интерфейс реализации.
package main
import (
"database/sql"
"fmt"
// Драйвер PostgreSQL регистрируется через init()
_ "github.com/lib/pq"
)
func main() {
// Абстракция: sql.DB
// Реализация: драйвер PostgreSQL, зарегистрированный выше
db, err := sql.Open("postgres", "host=localhost dbname=mydb sslmode=disable")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
panic(err)
}
fmt.Printf("%d: %s\n", id, name)
}
if err := rows.Err(); err != nil {
panic(err)
}
}goДрайвер регистрирует себя при импорте через функцию init(), вызывая sql.Register(). Это позволяет переключить СУБД, заменив только импорт и строку подключения.