跳至主要內容

结构型设计模式

Mr.LRH大约 14 分钟

结构型设计模式

结构型模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。

适配器模式 (Adapter)

能使接口不兼容的对象能够相互合作。

适合应用场景:

  • 希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。适配器模式允许创建一个中间层类,其可作为代码与遗留类、第三方类或提供怪异接口的类之间的转换器。
  • 需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。可以将缺少功能的对象封装在适配器中,从而动态地获取所需功能。

优点:

  • 单一职责原则。可以将接口或数据转换代码从程序主要业务逻辑中分离。
  • 开闭原则。只要客户端代码通过客户端接口与适配器进行交互,就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

缺点:

  • 代码整体复杂度增加,因为需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。

Adapter_UML

代码示例
js
class Target {
  request() {
    return "Target: The default target's behavior."
  }
}

class Adaptee {
  specificRequest() {
    return '.eetpadA eht fo roivaheb laicepS'
  }
}

class Adapter extends Target {
  constructor(adaptee) {
    super()
    this.adaptee = adaptee
  }
  request() {
    const result = this.adaptee.specificRequest().split('').reverse().join('')
    return `Adapter: (TRANSLATED) ${result}`
  }
}

function clientCode(target) {
  console.log(target.request())
}

console.log('Client: I can work just fine with the Target objects:')
const target = new Target()
clientCode(target)

const adaptee = new Adaptee()
console.log("Client: The Adaptee class has a weird interface. See, I don't understand it:")
console.log(`Adaptee: ${adaptee.specificRequest()}`)

console.log('Client: But I can work with it via the Adapter:')
const adapter = new Adapter(adaptee)
clientCode(adapter)

桥接模式 (Bridge)

将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能在开发时分别使用。

适合应用场景:

  • 拆分或重组一个具有多重功能的庞杂类(例如,能与多个数据库服务器进行交互的类)时,可使用桥接模式。
  • 在几个独立维度上扩展一个类时,可使用桥接模式。桥接建议将每个维度抽取为独立的类层次。初始类将相关工作委派给属于对应类层次的对象,无需自己完成所有工作。
  • 需要在运行时切换不同实现方法,可使用桥接模式。

优点:

  • 可以创建与平台无关的类和程序。
  • 客户端代码仅与高层抽象部分进行互动,不会接触到平台的详细信息。
  • 开闭原则。可以新增抽象部分和实现部分,且它们之间不会相互影响。
  • 单一职责原则。抽象部分专注于处理高层逻辑,实现部分处理平台细节。

缺点:

  • 对高内聚的类使用该模式可能会让代码更加复杂。

Bridge_UML

代码示例
js
class Abstraction {
  constructor(implementation) {
    this.implementation = implementation
  }

  operation() {
    const result = this.implementation.operationImplementation()
    return `Abstraction: Base operation with:\n${result}`
  }
}

class ExtendedAbstraction extends Abstraction {
  operation() {
    const result = this.implementation.operationImplementation()
    return `ExtendedAbstraction: Extended operation with:\n${result}`
  }
}

class ConcreteImplementationA {
  operationImplementation() {
    return "ConcreteImplementationA: Here's the result on the platform A."
  }
}

class ConcreteImplementationB {
  operationImplementation() {
    return "ConcreteImplementationB: Here's the result on the platform B."
  }
}

function clientCode(abstraction) {
  // ..
  console.log(abstraction.operation())
  // ..
}

let implementation = new ConcreteImplementationA()
let abstraction = new Abstraction(implementation)
clientCode(abstraction)

implementation = new ConcreteImplementationB()
abstraction = new ExtendedAbstraction(implementation)
clientCode(abstraction)

组合模式 (Composite)

可以将对象组合成树状结构,并且能像使用独立对象一样使用它们。

适合应用场景:

  • 需要实现树状对象结构,可以使用组合模式。该模式提供了两种共享公共接口的基本元素类型:简单叶节点和复杂容器。容器中可以包含叶节点和其他容器。可以构建树状嵌套递归对象结构。
  • 希望客户端代码以相同方式处理简单和复杂元素时,可以使用组合模式。该模式定义的所有元素共用同一个接口。在这一接口下,客户端不必在意其所使用的对象的具体类。

优点:

  • 可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则。无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分。

缺点:

  • 对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,需要过度一般化组件接口,使其变得令人难以理解。

Composite_UML

代码示例
js
class Component {
  setParent(parent) {
      this.parent = parent;
  }
  getParent() {
      return this.parent;
  }
  add(component) { }
  remove(component) { }
  isComposite() {
      return false;
  }
}

class Leaf extends Component {
  operation() {
      return 'Leaf';
  }
}

class Composite extends Component {
  constructor() {
      super(...arguments);
      this.children = [];
  }
  add(component) {
      this.children.push(component);
      component.setParent(this);
  }
  remove(component) {
      const componentIndex = this.children.indexOf(component);
      this.children.splice(componentIndex, 1);
      component.setParent(null);
  }
  isComposite() {
      return true;
  }
  operation() {
      const results = [];
      for (const child of this.children) {
          results.push(child.operation());
      }
      return `Branch(${results.join('+')})`;
  }
}

function clientCode(component) {
  // ...
  console.log(`RESULT: ${component.operation()}`);
  // ...
}

const simple = new Leaf();
console.log('Client: I\'ve got a simple component:');
clientCode(simple);

const tree = new Composite();
const branch1 = new Composite();
branch1.add(new Leaf());
branch1.add(new Leaf());
const branch2 = new Composite();
branch2.add(new Leaf());
tree.add(branch1);
tree.add(branch2);
console.log('Client: Now I\'ve got a composite tree:');
clientCode(tree);

function clientCode2(component1, component2) {
  // ...
  if (component1.isComposite()) {
      component1.add(component2);
  }
  console.log(`RESULT: ${component1.operation()}`);
  // ...
}

console.log('Client: I don\'t need to check the components classes even when managing the tree:');
clientCode2(tree, simple);

装饰模式 (Decorator)

通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

适合应用场景:

  • 在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。装饰能将业务逻辑组织为层次结构,可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。
  • 用继承来扩展对象行为的方案难以实现或者根本不可行, 可以使用装饰模式。

优点:

  • 无需创建新子类即可扩展对象的行为。
  • 可以在运行时添加或删除对象的功能。
  • 可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。可以将实现了许多不同行为的一个大类拆分为多个较小的类。

缺点:

  • 在封装器栈中删除特定封装器比较困难。
  • 实现行为不受装饰栈顺序影响的装饰比较困难。
  • 各层的初始化配置代码看上去可能会很糟糕。

Decorator_UML

代码示例
js
class ConcreteComponent {
  operation() {
    return 'ConcreteComponent'
  }
}

class Decorator {
  constructor(component) {
    this.component = component
  }
  operation() {
    return this.component.operation()
  }
}

class ConcreteDecoratorA extends Decorator {
  operation() {
    return `ConcreteDecoratorA(${super.operation()})`
  }
}

class ConcreteDecoratorB extends Decorator {
  operation() {
    return `ConcreteDecoratorB(${super.operation()})`
  }
}

function clientCode(component) {
  // ...
  console.log(`RESULT: ${component.operation()}`)
  // ...
}

const simple = new ConcreteComponent()
console.log("Client: I've got a simple component:")
clientCode(simple)

const decorator1 = new ConcreteDecoratorA(simple)
const decorator2 = new ConcreteDecoratorB(decorator1)
console.log("Client: Now I've got a decorated component:")
clientCode(decorator2)

外观模式 (Facade)

能为程序库、框架或其他复杂类提供一个简单的接口。

适合应用场景:

  • 需要一个指向复杂子系统的直接接口,且该接口的功能有限,可以使用外观模式。

  • 需要将子系统组织为多层结构,可以使用外观模式。创建外观来定义子系统中各层次的入口。可以要求子系统仅使用外观来进行交互,以减少子系统之间的耦合。

  • 优点:可以让代码独立于复杂子系统。

  • 缺点:外观可能成为与程序中所有类都耦合的上帝对象。

Facade_UML

代码示例
js
class Facade {
  constructor(subsystem1, subsystem2) {
    this.subsystem1 = subsystem1 || new Subsystem1()
    this.subsystem2 = subsystem2 || new Subsystem2()
  }
  operation() {
    let result = 'Facade initializes subsystems:\n'
    result += this.subsystem1.operation1()
    result += this.subsystem2.operation1()
    result += 'Facade orders subsystems to perform the action:\n'
    result += this.subsystem1.operationN()
    result += this.subsystem2.operationZ()
    return result
  }
}

class Subsystem1 {
  operation1() {
    return 'Subsystem1: Ready!\n'
  }
  // ...
  operationN() {
    return 'Subsystem1: Go!\n'
  }
}

class Subsystem2 {
  operation1() {
    return 'Subsystem2: Get ready!\n'
  }
  // ...
  operationZ() {
    return 'Subsystem2: Fire!'
  }
}

function clientCode(facade) {
  // ...
  console.log(facade.operation())
  // ...
}

const subsystem1 = new Subsystem1()
const subsystem2 = new Subsystem2()
const facade = new Facade(subsystem1, subsystem2)
clientCode(facade)

享元模式 (Flyweight)

摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,能在有限的内存容量中载入更多对象。

适合应用场景:

  • 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。应用该模式所获的收益大小取决于使用它的方式和情景。它在下列情况中最有效:
    • 程序需要生成数量巨大的相似对象
    • 将耗尽目标设备的所有内存
    • 对象中包含可抽取且能在多个对象间共享的重复状态

优点:如果程序中有很多相似对象,将可以节省大量内存。

缺点:

  • 需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。
  • 代码会变得更加复杂。

Flyweight_UML

代码示例
js
class Flyweight {
  constructor(sharedState) {
    this.sharedState = sharedState
  }
  operation(uniqueState) {
    const s = JSON.stringify(this.sharedState)
    const u = JSON.stringify(uniqueState)
    console.log(`Flyweight: Displaying shared (${s}) and unique (${u}) state.`)
  }
}

class FlyweightFactory {
  constructor(initialFlyweights) {
    this.flyweights = {}
    for (const state of initialFlyweights) {
      this.flyweights[this.getKey(state)] = new Flyweight(state)
    }
  }
  getKey(state) {
    return state.join('_')
  }
  getFlyweight(sharedState) {
    const key = this.getKey(sharedState)
    if (!(key in this.flyweights)) {
      console.log("FlyweightFactory: Can't find a flyweight, creating new one.")
      this.flyweights[key] = new Flyweight(sharedState)
    } else {
      console.log('FlyweightFactory: Reusing existing flyweight.')
    }
    return this.flyweights[key]
  }
  listFlyweights() {
    const count = Object.keys(this.flyweights).length
    console.log(`\nFlyweightFactory: I have ${count} flyweights:`)
    for (const key in this.flyweights) {
      console.log(key)
    }
  }
}

const factory = new FlyweightFactory([
  ['Chevrolet', 'Camaro2018', 'pink'],
  ['Mercedes Benz', 'C300', 'black'],
  ['Mercedes Benz', 'C500', 'red'],
  ['BMW', 'M5', 'red'],
  ['BMW', 'X6', 'white'],
  // ...
])

factory.listFlyweights()

// ...

function addCarToPoliceDatabase(ff, plates, owner, brand, model, color) {
  console.log('\nClient: Adding a car to database.')
  const flyweight = ff.getFlyweight([brand, model, color])
  flyweight.operation([plates, owner])
}

addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'M5', 'red')
addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'X1', 'red')
factory.listFlyweights()

代理模式 (Proxy)

能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。

适合应用场景:

  • 延迟初始化(虚拟代理)。如果有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。无需在程序启动时就创建该对象,可将对象的初始化延迟到真正有需要的时候。
  • 访问控制(保护代理)。如果只希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分,而客户端则是各种已启动的程序(包括恶意程序),此时可使用代理模式。代理可仅在客户端凭据满足要求时将请求传递给服务对象。
  • 本地执行远程服务(远程代理)。适用于服务对象位于远程服务器上的情形。代理通过网络传递客户端请求,负责处理所有与网络相关的复杂细节。
  • 记录日志请求(日志记录代理)。适用于需要保存对于服务对象的请求历史记录时。代理可以在向服务传递请求前进行记录。缓存请求结果(缓存代理)。适用于需要缓存客户请求结果并对缓存生命周期进行管理时,特别是当返回结果的体积非常大时。代理可对重复请求所需的相同结果进行缓存,还可使用请求参数作为索引缓存的键值。
  • 智能引用。可在没有客户端使用某个重量级对象时立即销毁该对象。代理会将所有获取了指向服务对象或其结果的客户端记录在案。代理会时不时地遍历各个客户, 检查它们是否仍在运行。如果相应的客户端列表为空,代理就会销毁该服务对象,释放底层系统资源。代理还可以记录客户端是否修改了服务对象。其他客户端还可以复用未修改的对象。

优点:

  • 可以在客户端毫无察觉的情况下控制服务对象。
  • 客户端对服务对象的生命周期没有特殊要求,可以对生命周期进行管理。
  • 即使服务对象还未准备好或不存在,代理也可以正常工作。
  • 开闭原则。可以在不对服务或客户端做出修改的情况下创建新代理。

缺点:

  • 代码可能会变得复杂,因为需要新建许多类。
  • 服务响应可能会延迟。

Proxy_UML

代码示例
js
class RealSubject {
  request() {
    console.log('RealSubject: Handling request.')
  }
}
class Proxy {
  constructor(realSubject) {
    this.realSubject = realSubject
  }
  request() {
    if (this.checkAccess()) {
      this.realSubject.request()
      this.logAccess()
    }
  }
  checkAccess() {
    console.log('Proxy: Checking access prior to firing a real request.')
    return true
  }
  logAccess() {
    console.log('Proxy: Logging the time of request.')
  }
}
function clientCode(subject) {
  // ...
  subject.request()
  // ...
}
console.log('Client: Executing the client code with a real subject:')
const realSubject = new RealSubject()
clientCode(realSubject)

console.log('Client: Executing the same client code with a proxy:')
const proxy = new Proxy(realSubject)
clientCode(proxy)
上次编辑于:
贡献者: lrh21g