装饰器模式包装了对象 (wrap object),赋予它们新的职责。
现在,以不同的目的来包装一些对象:使它们的接口看起来不像它自己,而是像其他东西。
这样,可以将期望一个接口的设计调整为实现一个不同接口的类。
另外,探讨另一个包装对象的模式,以简化它们的接口。
面向对象适配器
OO适配器与现实世界中的适配器扮演着相同的角色:它们接受一个接口并将其转换为一个客户期望的接口。
适配器实现了你的类期望的接口,并且可以与供应商接口沟通,以服务你的请求。
适配器类似中间人,它接收客户的请求,并将其转换为供应商类能理解的请求。
火鸡适配器
回顾一下第1章中的Duck接口和类的简化版本:
class Duck {
public:
virtual void quack() const = 0;
virtual void fly() const = 0;
};
class MallardDuck : public Duck {
public:
void quack() const {
std::cout << "Quack" << std::endl;
}
void fly() const {
std::cout << "I'm flying" << std::endl;
}
};
现在有了新的家禽:
class Turkey {
public:
virtual void gobble() const = 0;
virtual void fly() const = 0;
};
class WildTurkey : public Turkey{
public:
void gobble() const {
std::cout << "Gobble gobble" << std::endl;
}
void fly() const {
std::cout << "I'm flying a short distance" << std::endl;
}
};
现在,假设缺少Duck对象,想在它们的位置使用一些Turkey对象。显然,不能直接使用火鸡,因为它们具有不同的接口。
写一个适配器:
// 首先,需要实现要转换成的类型的接口。这是你的客户希望看到的接口。
class TurkeyAdapter : public Duck {
private:
const Turkey* turkey;
public:
// 接下来,需要获得要适配的对象的引用;在这里,通过构造函数来做到这一点。
explicit TurkeyAdapter(const Turkey* turkey) {
this->turkey = turkey;
}
// 现在,需要在接口中实现所有方法。
void quack() const {
turkey->gobble();
}
// 火鸡是以短距离飞行,他们无法像鸭子一样进行长距离飞行。要在Duck的fly()方法和Turkey的fly()之间进行映射,需要调用Turkey的fly()方法五次以弥补这一点。
void fly() const {
for(int i=0; i < 5; i++) {
turkey->fly();
}
}
};
测试适配器:
void testDuck(const Duck* duck) {
duck->quack();
duck->fly();
}
int main() {
std::unique_ptr<MallardDuck> duck(new MallardDuck());
std::unique_ptr<WildTurkey> turkey(new WildTurkey());
std::unique_ptr<Duck> turkeyAdapter(new TurkeyAdapter(turkey.get())); // 在TurkeyAdapter中包装一个火鸡,使它看起来像一个Duck
std::cout << "The Turkey says..." << std::endl;
turkey->gobble();
turkey->fly();
std::cout << std::endl << "The Duck says..." << std::endl;
testDuck(duck.get());
std::cout << std::endl << "The TurkeyAdapter says..." << std::endl;
testDuck(turkeyAdapter.get());
return 0;
}
运行结果:
The Turkey says...
Gobble gobble
I'm flying a short distance
The Duck says...
Quack
I'm flying
The TurkeyAdapter says...
Gobble gobble
I'm flying a short distance
I'm flying a short distance
I'm flying a short distance
I'm flying a short distance
I'm flying a short distance
适配器模式解析
客户使用适配器的过程如下:
- 客户端通过目标接口调用适配器的方法来向适配器发出请求。
- 适配器 (adapter) 使用被适配者接口 (adaptee interface) 将请求转换为被适配器的一个或多个调用。
- 客户收到调用结果,但不知道有适配器在进行转换。
注意:Client 和 Adaptee 是解耦的,它们不知道彼此。
实现适配器的“适配”工作实际上与目标接口的接口的大小成正比。
如果要实现一个大型目标接口,那么需要做很多工作。可以重写客户端调用这个接口,这将导致大量调查工作和代码更改。或者,提供一个类,封装一个类中的所有更改。
适配器模式的作用是将一个接口转换为另一个接口。尽管适配器模式的大多数示例都展示了一个适配器包装一个被适配者,但也可能会遇到一些情况,需要一个适配器拥有多个被适配者来实现目标接口。这涉及外观模式。
如果系统有旧的和新的部分,旧的部分期望旧的供应商接口,但是已经编写了新的部分以使用新的供应商接口,怎么办?
可以做的一件事就是创建一个支持两个接口的双向适配器 (Two Way Adapter)。要创建双向适配器,只需实现涉及的两个接口,这样适配器就可以充当旧接口或新接口。
实现一个将Duck转换为Turkey的适配器 DuckAdapter。
class DuckAdapter : public Turkey {
private:
const Duck* duck;
mutable int random;
public:
explicit DuckAdapter(const Duck* duck) {
this->duck = duck;
srand((unsigned)time(nullptr)); // 为随机数生成器提供一个种子
}
void gobble() const {
duck->quack();
}
void fly() const {
// 因为鸭子比火鸡飞得更远,所以决定让鸭子平均五次飞行一次
random = rand() % 5 + 1;
if (random == 5) {
duck->fly();
}
}
};
适配器模式的定义
适配器模式 (The Adapter Pattern) 的正式定义:
适配器模式将一个类的接口转换为客户期望的另一个接口。适配器使原本接口不兼容的类可以协同工作。
适配器模式的类图:
适配器模式充满了良好的OO设计原则:使用对象组合,用更改后的接口包装被适配对象。这种方法的另一个优点是,可以将适配器与被适配者的任何子类一起使用。
了解模式如何将客户绑定到接口而不是实现上。我们可以使用多个适配器,每个适配器转换一个不同的后端类集合。或者,我们可以添加新的实现,只要它们遵守Target接口即可。
对象适配器和类适配器
实际上有两种适配器:对象适配器、类适配器。上面的类图描述的是对象适配器。
类适配器需要多重继承才能实现,所以在Java中是不可能的。下面是多重继承的类图。
唯一的区别是,类适配器继承 Target 和 Adaptee,对象适配器使用组合将请求传递给 Adaptee。
对象适配器和类适配器使用两种不同的方式来适配被适配者 (组成VS继承)。
对象适配器:
- 使用组合。编写一些代码将工作委托给被适配者。
- 更具有灵活性。
- 不仅可以适配一个被适配者的类,还可以适配它的任何子类。添加到适配器代码中的任何行为,可以与被适配者及其子类一起工作。
类适配器:
- 使用继承。
- 适配一个特定的被适配者类。
- 优势:不必重新实现整个被适配者。如果需要的话,可以覆盖被适配者的行为。
真实世界的适配器
早期的Java中的Collection类型 (比如Vector、Stack) 实现了一个名为 element() 的方法,该方法返回一个Enumeration。Enumeration接口可以遍历集合内的每个元素。Java集合类更新后,使用Iterator接口。
要求:将Java早期的Enumeration接口转换为Iterator接口。
public class EnumerationIterator implements Iterator {
Enumeration enum;
public EnumerationIterator(Enumeration enum) {
this.enum = enum;
}
public boolean hasNext() {
return enum.hasMoreElements();
}
public Object next() {
return enum.nextElement();
}
// 无法在适配器上实现一个具有实际功能的remove()方法,可以做的是抛出一个运行时异常
public void remove() {
throw new UnsupportedOperationException();
}
}
上面的例子展现出了适配器不完美的一面;客户将不得不提防潜在的异常,但是只要客户小心,并且适配器的文档有详细说明,这不失为一个合理的解决方案。
装饰者模式VS适配器模式VS外观模式
外观模式 (Facade Pattern) 也是一个改变接口的模式,但它改变接口的原因是简化接口。之所以这么命名,是因为这个模式将一个或多个类的所有复杂部分隐藏在一个干净美好的外观之后。
模式 | 意图 |
---|---|
装饰者模式 | 不改变接口,但加入职责 |
适配器模式 | 将一个接口转换为另一个接口 |
外观模式 | 让接口更简单 |
家庭影院
建立一个自己的家庭影院:经过一番研究,组装了一个系统,其中包括DVD播放器,投影仪,自动屏幕,环绕立体声,甚至还有爆米花机。花费数天时间来布线,安装投影仪,进行所有连接并进行细调。下面是放在一起的所有组件:
观看电影 (以困难的方式)
挑选一部DVD影片,使用上述系统观看电影,在观看之前需要执行一些任务:
- 打开爆米花机
- 开始爆米花
- 调暗灯光
- 放下屏幕
- 打开投影仪
- 将投影仪输入设置为DVD
- 将投影仪设置为宽屏模式
- 打开功放
- 将功放设置为DVD输入
- 将功放设置为环绕立体声
- 将功放音量设置为中 (5)
- 打开DVD播放器
- 开始播放DVD播放器
将这些任务写成类和方法的调用:
popper.on();
popper.pop();
lights.dim(10); // 将灯光调暗至10%
screen.down();
projector.on();
projector.setInput(dvd);
projector.wideScreenMode();
amp.on();
amp.setDvd(dvd);
amp.setSurroundSound();
amp.setVolume(5);
dvd.on();
dvd.play(movie);
使用外观模式,通过实现一个提供一个更合理接口的 Facade 类,可以接受一个复杂的子系统并使它更容易使用。
看一下Facade的运作方式:
- 为家庭影院系统创建 Facade。为此,创建一个新的类 HomeTheaterFacade,它公开了一些简单的方法,例如 watchMovie()。
- Facade 类将家庭影院组件视为一个子系统,并调用该子系统以实现其 watchMovie() 方法。
- 客户代码现在调用家庭影院 Facade 上的方法,而不是子系统上的方法。因此,现在要观看电影,只需调用一个方法 watchMovie(),它就可以与灯光、DVD播放器等组件进行通信。
- Facade 不会“封装”子系统类。它们只是为其功能提供了简化的接口。如果客户需要使用跟多特定接口,仍然可以直接使用子系统类。
外观不只是简化了接口,也将客户从组件的子系统中解耦。
实现家庭影院
构造家庭影院外观,实现简化的接口
使用组合,以便外观可以访问子系统的所有组件。
class HomeTheaterFacade {
private:
// 这是组合,这些是会用到的子系统所有组件
Amplifier* amp;
Tuner* tuner;
DvdPlayer* dvd;
CdPlayer* cd;
Projector* projector;
TheaterLights* lights;
Screen* screen;
PopcornPopper* popper;
public:
// 外观将子系统的每个组件的引用传递给其构造函数中,将每个对象分配给相应的实例变量
HomeTheaterFacade(Amplifier* amp,
Tuner* tuner,
DvdPlayer* dvd,
CdPlayer* cd,
Projector* projector,
Screen* screen,
TheaterLights* lights,
PopcornPopper* popper) {
this->amp = amp;
this->tuner = tuner;
this->dvd = dvd;
this->cd = cd;
this->projector = projector;
this->screen = screen;
this->lights = lights;
this->popper = popper;
}
// 注意,watchMovie()和endMovie()中每项任务的职责都委托给子系统中的相应组件
void watchMovie(std::string movie) {
std::cout << "Get ready to watch a movie..." << std::endl;
popper->on();
popper->pop();
lights->dim(10);
screen->down();
projector->on();
projector->wideScreenMode();
amp->on();
amp->setDvd(dvd);
amp->setSurroundSound();
amp->setVolume(5);
dvd->on();
dvd->play(movie);
}
void endMovie() {
std::cout << "Shutting movie theater down..." << std::endl;
popper->off();
lights->on();
screen->up();
projector->off();
amp->off();
dvd->stop();
dvd->eject();
dvd->off();
}
};
观看电影 (以轻松的方式)
int main() {
// 在这里实例化组件。通常情况下,客户会得到一个外观,而不必自己构造。
std::unique_ptr<Amplifier> amp(new Amplifier());
std::unique_ptr<Tuner> tuner(new Tuner());
std::unique_ptr<DvdPlayer> dvd(new DvdPlayer());
std::unique_ptr<CdPlayer> cd(new CdPlayer());
std::unique_ptr<Projector> projector(new Projector());
std::unique_ptr<Screen> screen(new Screen());
std::unique_ptr<TheaterLights> lights(new TheaterLights());
std::unique_ptr<PopcornPopper> popper(new PopcornPopper());
// 使用子系统的所有组件实例化外观
std::unique_ptr<HomeTheaterFacade> homeTheater(
new HomeTheaterFacade(amp.get(), tuner.get(), dvd.get(), cd.get(), projector.get(), screen.get(), lights.get(), popper.get()));
// 使用简化的接口,打开电影,关闭电影
homeTheater->watchMovie("Raiders of the Lost Ark");
homeTheater->endMovie();
return 0;
}
外观模式的定义
外观模式 (The Facade Pattern) 的正式定义:
外观模式为子系统中的一组接口提供了统一的接口。外观定义了一个更高级别的接口,使子系统更易于使用。
外观模式的类图:
最少知识原则
最少知识原则 (The Principle of Least Knowledge) 指导我们将对象之间的交互减少为只有几个亲密的“朋友”。该原则通常表述为:
最少知识的原则:只和离你最近的朋友进行交互。
这意味着在设计系统时,对于任何对象,请注意与之交互的类的数量以及与这些类进行交互的方式。
这一原理希望我们在设计时,不要将太多的类耦合在一起,以免使系统某一部分中的更改影响到其他部分。
如何不要赢得朋友和影响太多对象
如何避免这样做呢?该原则提供了一些指导:就任何对象而言,在该对象中的任何方法中,只应该调用属于以下范围的方法:
- 对象本身
- 作为参数传递给方法的对象
- 该方法创建或实例化的任何对象
- 该对象的任何组件
将“组件”视为实例变量引用的任何对象。换句话说,将此视为HAS-A关系。
比如,不采用知识最少原则:
public float getTemp() {
Thermometer thermometer = station.getThermometer(); // 从气象站取得温度计对象
return thermometer.getTemperature();
}
应用知识最少原则,在Station类中加入一个方法,该方法向温度计发出了请求。这样可以减少所依赖的类的数量。
public float getTemp() {
return station.getTemperature();
}
将方法调用保持在界限内
这是一个Car类,展示可以调用方法的所有方式,并且仍然遵循“最少知识原则”:
public class Car {
Engine engine; // 类的一个组件,可以调用它的方法
// other instance variables
public Car() {
// initialize engine, etc.
}
public void start(Key key) {
Doors doors = new Doors(); // 创建一个新对象,它的方法合法
boolean authorized = key.turns(); // 被当作参数传递进来的对象,它的方法可以调用
if (authorized) {
engine.start(); // 可以调用对象组件的方法
updateDashboardDisplay(); // 可以调用对象内的本地方法
doors.lock(); // 可以调用你所创建或实例化的对象的方法
}
}
public void updateDashboardDisplay() {
// update display
}
}
迪米特法则 (The Law of Demeter) 就是知识最少原则。
倾向于使用“最少知识原则”,原因有两个:这个名字更直观;法则 (Law) 一词意味着我们总是必须运用这个原则。
实际上,没有原则是法则,所有原则应在有帮助时才使用。所有设计都涉及权衡 (抽象VS速度,空间VS时间等),尽管原则提供了指导,但在应用它们之前应考虑所有因素。
使用最小知识原则有什么不利之处吗?
尽管该原则减少了对象之间的依赖关系,并且研究表明这减少了软件维护成本,但在应用这个原则的情况下,还会导致编写更多的“包装”类来处理其他组件的方法调用。这可能导致复杂性和开发时间增加,并降低运行时性能。
外观模式与知识最少原则
在家庭影院系统中,客户只有一个朋友 HomeTheaterFacade。在OO编程中,只有一个朋友是件好事。
HomeTheaterFacade 为客户管理所有子系统组件。它使得客户变得简单又具有灵活性。
如果子系统太复杂,有太多朋友混杂在一起,可以引入其他外观来形成子系统层。
要点
- 当需要使用现有的类而其接口不是你所需要的时,使用适配器。
- 当需要简化并统一一个大型接口或一组复杂的接口时,使用外观。
- 适配器将接口更改为客户期望的接口。
- 外观使客户从复杂的子系统解耦。
- 根据目标接口的大小和复杂性,实现适配器可能需要很少的工作,也可能需要大量的工作。
- 实现外观需要将子系统组合到外观中,并使用委托来执行外观的工作。
- 适配器模式有两种形式:对象适配器和类适配器。类适配器需要多重继承。
- 可以为一个子系统实现多个外观。
- 适配器包装一个对象以更改其接口,装饰器包装一个对象以添加新的行为和职责,外观“包装”一组对象以简化接口。