掌握桥接模式的3大应用场景

有这样一个场景:在开发一个复杂的系统时,你发现一些类的功能越来越复杂,类的层次结构越来越混乱,每次添加一个新的功能或者修改一个旧的功能都变得非常困难。你开始怀疑,是不是自己的设计出了问题,是不是要重新设计这个系统/功能呢?但是,重新设计意味着要投入大量的时间和精力,而且还可能带来新的问题。你不禁感叹:太 ~ 难 ~ 啦 ~~

如果你遇到了类似这样的问题,那么,恭喜你,有一种方法可以有效地帮你解决这类问题,这就是我们今天要分享的内容——桥接模式。

认识桥接模式

桥接模式是一个非常强大的设计模式,属于一种结构型设计模式,你可以简单的把它想象成一座桥,这座桥将“两岸”连接起来。

桥接模式可以将一个大的、复杂的类或者类层次结构分解成两个独立的部分:抽象部分和现实部分。这样做的好处是,这两个部分可以独立地进行修改和扩展,而不会相互影响。这就好比在“两岸”分别划块地搞房地产,你可以在这边盖一座摩天大厦,在另一边弄个茅草屋。它们之间有影响吗?并没有,它们可以独立地进行改造和扩展,而不会影响到他们对方。

所以,当我们的系统或某个功能模块越来越复杂、层次结构越来越混乱时,我们就需要这种方法来管理这样的复杂性。

桥接模式的组成

在初步了解了桥接模式后,你还要知道桥接模式有哪些组成部分,不然只是知道它的概念是没法在实际项目中应用的。下面,我们就来一起看看桥接模式都由哪些部分组成。

抽象部分

抽象部分是桥接模式中的一个抽象类,它定义了一些基本的和属性。这些方法和属性使我们系统中的一些基本功能,比如在一个图形编辑器中,我们可能会有画线、画圆、填充颜色等基本功能。

但是,这些功能的具体实现方式可能会有很多种,比如画线可以是实线、虚线、点线,填充颜色可以是红色、蓝色、绿色等。这些具体的实现方式就是我们接下来要介绍的实现部分。

实现部分

实现部分是一个接口或者抽象类,它定义了一些具体的实现方法。这些方法是我们系统中的一些具体功能,比如画实线、画虚线、画点线,填充红色、填充蓝色、填充绿色等。

在桥接模式中,抽象部分和实现部分是完全独立的,它们可以独立地进行修改和扩展,而不会相互影响。这就好比我们前面提到的在两岸分别划块地搞房地产,你可以在这边盖一座摩天大厦,在另一边弄个茅草屋,它们之间没有任何影响。

桥接

那么,抽象部分和实现部分之间是如何连接的呢?这就是我们的桥接。在桥接模式中,抽象部分会持有一个实现部分的引用,这样就可以在抽象部分中调用实现部分的方法,实现具体的功能。

这就好比我们在两岸之间建了一座桥,这座桥就是我们的桥接。有了这座桥,我们就可以在这边的摩天大厦和另一边的茅草屋之间自由地来回走动,实现各种功能。

那么,这样做有什么好处呢?这样做的好处就是我们可以独立地修改和扩展抽象部分和实现部分。比如,我们可以在不改变抽象部分的情况下,添加新的实现部分,比如添加一个画虚线的实现。同样,我们也可以在不改变实现部分的情况下,添加新的抽象部分,比如添加一个画矩形的抽象。

这就是桥接模式的组成部分。通过理解这些组成部分,我们就可以更好地理解桥接模式,更有效地在实际项目中应用它。

桥接模式的实现

首先我们需要定义抽象部分和实现部分。在这个例子中,我们将创建一个 Shape 类作为抽象部分,和一个 Color 类作为实现部分。

class Shape {
  constructor(color) {
    this.color = color;
  }
}

class Color {
  constructor(type) {
    this.type = type;
  }
  applyColor() {
    return this.type;
  }
}

上面的代码中,Shape 类有一个 color 属性,这个属性是一个 Color 对象。这就是我们的“桥接”,它连接了抽象部分和实现部分。

现在我们已经定义了抽象部分和实现部分,那么如何使用它们呢?让我们来看一个例子。

class Circle extends Shape {
  constructor(color) {
    super(color);
  }
  draw() {
    return `Drawing a circle with ${this.color.applyColor()} color.`;
  }
}

const redColor = new Color('red');
const circle = new Circle(redColor);
console.log(circle.draw());

在这个例子中,我们创建了一个 Circle 类,它继承了 Shape 类。Circle 类有一个 draw 方法,这个方法使用 color 对象的 applyColor 方法来获取颜色。然后,我们创建了一个 Color 对象 redColor,并将它传递给 Circle 对象。最后,我们调用 Circle 对象的 draw 方法,它将输出 Drawing a circle with red color.。

  • 我们通过桥接模式实现的这段示例代码具有以下优点:
    • 首先,桥接模式可以帮助我们分离抽象和实现部分,这样它们就可以独立地进行修改和扩展。这可以让我们的代码更加灵活,也更容易适应需求的变化。
    • 其次,桥接模式能提高我们代码的可读性和可维护性。当一个类或者类层次结构变得非常复杂时,使用桥接模式可以将这个复杂的结构分解成更小、更易于理解和管理的部分。
    • 最后,桥接模式能帮助我们更好地理解和管理代码中的依赖关系。在桥接模式中,抽象部分和实现部分是完全独立的,它们之间的依赖关系是通过桥接来管理的。也就是说,这可以让我们更清楚地看到代码中的依赖关系,更好地理解和管理这些依赖关系。

下面是示例的完整代码:

class Shape {
  constructor(color) {
    this.color = color;
  }
}

class Color {
  constructor(type) {
    this.type = type;
  }
  applyColor() {
    return this.type;
  }
}

class Circle extends Shape {
  constructor(color) {
    super(color);
  }
  draw() {
    return `Drawing a circle with ${this.color.applyColor()} color.`;
  }
}

const redColor = new Color('red');
const circle = new Circle(redColor);
console.log(circle.draw());

桥接模式的应用场景

这里我将桥接模式总结了三种场景,你也可以思考一下还有没有其他场景下可以用到桥接模式。

系统需要独立地处理抽象部分和实现部分

当你的系统需要独立地处理抽象部分和实现部分时,可以考虑应用桥接模式。以上面的代码为例,比如,需要在现有的图形编辑器中添加一个新的形状 Rectangle:

class Shape {
  constructor(color) {
    this.color = color;
  }
}

class Rectangle extends Shape {
  constructor(color) {
    super(color);
  }

  draw() {
    return `Drawing a rectangle with ${this.color.applyColor()} color.`;
  }
}

const blueColor = new Color('blue');
const rectangle = new Rectangle(blueColor);
console.log(rectangle.draw());

观察这段代码我们不难发现,这里我们独立地添加了一个新的形状 Rectangle,但是我们并没有修改现有的代码。

系统中存在大量的类,它们之间有复杂的关系

在这种场景下——系统中存在大量的类,且它们之间有复杂的关系时,桥接模式也非常有用。比如,你正在开发一个电商系统,你需要处理各种商品(如书籍、电子产品、服装等)和各种支付方式(如信用卡支付、支付宝支付、微信支付等)。这时,你可以使用桥接模式,将商品和支付方式分别作为抽象部分和实现部分,这样你就可以独立地添加新的商品和支付方式,而不需要修改现有的代码。

class Product {
  constructor(paymentMethod) {
    this.paymentMethod = paymentMethod;
  }
}

class Book extends Product {
  constructor(paymentMethod) {
    super(paymentMethod);
  }

  purchase() {
    return `Purchasing a book with ${this.paymentMethod.pay()} method.`;
  }
}

class CreditCardPayment {
  pay() {
    return 'credit card';
  }
}

const creditCardPayment = new CreditCardPayment();
const book = new Book(creditCardPayment);
console.log(book.purchase());

上面的示例中,我们添加了一个新的商品 Book 和一个新的支付方式 CreditCardPayment,而同样不需要修改 Product 类或其他支付方式的代码。

系统需要在运行时切换实现方法

在类似这种场景下,桥接模式也是一个不错的方案。比如,你正在开发一个游戏,玩家可以在游戏中切换不同的武器(如剑、枪、弓箭等)。这时,你可以使用桥接模式,将玩家和武器分别作为抽象部分和实现部分,这样玩家就可以在游戏运行时切换武器。

class Player {
  constructor(weapon) {
    this.weapon = weapon;
  }

  attack() {
    return `Attacking with ${this.weapon.use()} weapon.`;
  }

  changeWeapon(weapon) {
    this.weapon = weapon;
  }
}

class Sword {
  use() {
    return 'sword';
  }
}

class Gun {
  use() {
    return 'gun';
  }
}

const sword = new Sword();
const gun = new Gun();
const player = new Player(sword);
console.log(player.attack()); // Attacking with sword weapon.
player.changeWeapon(gun);
console.log(player.attack()); // Attacking with gun weapon.

这段代码创建了一个 Player 类,它有一个 weapon 属性和一个 attack 方法。玩家可以使用 attack 方法进行攻击,也可以使用 changeWeapon 方法在运行时切换武器。

除了上面提到的三种场景外,你还能想到一些可以应用桥接模式的场景吗?通过这几个例子,你应该能发现桥接模式的一个很明显的特点。没错,就是“解耦”。桥接模式可以让我们将复杂的功能分解成更小、更易于管理的部分,从而使我们的代码更加灵活、可读和可维护。

与其他设计模式的比较

之前我们在适配器模式中提到过桥接模式,适配器模式主要用于使已有的接口能适应新的需求,它通常在系统已经成熟或由于某些原因不能改变原有系统的情况下使用。而桥接模式主要用于将抽象和实现部分分离,使它们可以独立地变化。

为了能更好的理解适配器模式和桥接模式之间的区别与联系,强烈建议你看一下我们之前分享的内容:

除适配器模式外,装饰器模式与桥接模式也有些相似。不过装饰器模式主要用于在不改变原有对象的基础上,给对象添加新的功能。

关于装饰器模式,将来我会和你分享这部分内容。

贡献者: mankueng