书中将 Composite 组合模式、Decorator 装饰器模式归纳为用来让看上去不同的对象的操作变的统一的设计模式。
这两个设计模式的相同之处是达到目的的所有成员都继承与一个基类,保证了面向用户的接口的一致性。不同之处是组合模式是一部分成员选择复写基类方法或选择添加新方法,而装饰器模式中装饰类会被委托基类的实例,并直接调用其方法进行功能的扩展,当然也可以自己添加新的方法。
这两种设计模式都是在特定场景下的归纳出的应用方式。
- 组合模式:所有成员类都继承与一个基类,不同的实现可复写或添加不同方法,并可以组合这些实现的实例形成树形结构。
- 装饰器模式:所有成员类都继承与一个基类,基于抽象基类的装饰器类可以被委托原有的类的实例,通过包装调用委托对象进行功能的扩展、修改。
Composite 组合模式
定义
一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。
说白了就是抽象类的不同实现类在实现树状结构中的应用,以往的模式很少有不同实现类共同实现一个目标。
角色
- 树叶结构(文件):树状层级结构中表示内容的角色。
- 树枝结构(文件夹):树状层次结构中表示容器的角色,可放入树枝和树叶。
- 抽象构件(一致性模型):使树叶结构和树枝结构具有一致性的角色,声明公共接口并实现一些默认行为。
JavaScript 实现
直接使用书中的例子
// 抽象构件
class Component {
getName () {
throw new Error('需实现 Component 类的 getName 方法')
}
getSize () {
throw new Error('需实现 Component 类的 getSize 方法')
}
printPath () {
return ''
}
add () {
console.error('当前对象不支持添加')
}
}
// 树枝构建
class Dir extends Component {
constructor (name) {
super()
this.name = name
this.files = []
}
getName () {
return this.name
}
getSize () {
return 'sth'
}
printPath () {
return '当前文件夹地址'
}
add (file) {
this.files.push(file)
}
}
// 树叶结构
class File extends Component {
constructor (name) {
super()
this.name = name
this.files = []
}
getName () {
return this.name
}
getSize () {
return 'sth'
}
printPath () {
return '当前文件地址'
}
}
// 执行
const dir = new Dir('文档')
const file = new File('图片')
dir.add(file)
dir.getName()
file.getName()
拓展
从上述例子中我们可以看到,抽象构件类定义了 add 方法并默认报错,而只有树枝结构类做了正确的实现,树叶结构类并没有实现。这其实是一致性中差异行为定义方式的问题。
有三种方式:
- 定义在抽象构件类中,并提供默认实现(报错或不报错均可)。
- 定义在抽象构件类中,但是不提供实现。
- 不定义在抽象构件类中,但是不提供实现,由确实需要此功能的树枝类自己实现,此方式在树叶类的实例调用差异 api 时会有意外报错。
Decorator 装饰器模式
定义
指通过委托在不改变现有对象结构的情况下,动态地给该对象增加和修改一些功能的模式。
在书中,委托和被委托方最终都是基于一个基类的。
角色
- 抽象构件:定义了构件和装饰的通用接口。
- 具体构件:实现抽象构件,且其实例会被委托给装饰加以修改。
- 抽象装饰:继承抽象构件并扩展抽象构件,有接收构件委托的能力。
- 具体装饰:实现抽象装饰。
JavaScript 实现
仿照书中的例子
// 抽象构件
class Goods {
getPrice () {
throw new Error('需实现 Goods 类的 getPrice 方法')
}
}
// 具体构件
class CoffeeBeansA extends Goods {
get Price () {
return '20'
}
}
class CoffeeBeansB extends Goods {
get Price () {
return '40'
}
}
// 抽象装饰
class FeedCoffee extends Goods {
constructor (coffee) {
this.coffee = coffee
}
getPrice () {
throw new Error('需实现 Goods 类的 getPrice 方法')
}
}
// 具体装饰
class MotoCoffee extends FeedCoffee {
getPrice () {
return this.coffee.getPrice() + 20
}
}
class MilkCoffee extends FeedCoffee {
getPrice () {
return this.coffee.getPrice() + 10
}
}
// 执行
const beanA = new CoffeeBeansA()
const beanB = new CoffeeBeansA()
const motoCoffeeA = new MotoCoffee(beanA)
const motoCoffeeB = new MotoCoffee(beanB)
const milkCoffeeA = new MilkCoffee(beanA)
const milkCoffeeB = new MilkCoffee(beanB)
motoCoffeeA.getPrice()
motoCoffeeB.getPrice()
motoCoffeeA.getPrice()
motoCoffeeB.getPrice()
拓展
- 如果不使用委托的方式,获取四种(组合)产品的至少需要对四种组合都新创建一个类,而使用了委托的方式只需要对每个组成部分新创建类(甚至更少,针对实例的代码,我们甚至可以将咖啡豆和佐料都委托给加料产品类,加料产品的
getPrice
调用咖啡豆和佐料的getPrice
输出最终价格 ),减少了类的数量。 - 在装饰器模式中装饰和被装饰对象都是继承了同一个类,它们具有一致性。