1.声明
设计模式中的设计思想、图片和部分代码参考自《Head First设计模式》,作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates。
在这里我只是对这本书进行学习阅读,并向大家分享一些心得体会。
2.设计家庭影院
在适配器模式中,我们知道如何将一个类的接口转换成另一个符合符合客户期望的接口。Java要做到这一点,必须将一个不兼容目标接口的对象包装起来,变成兼容的对象。
此外还有另外一种模式,它也可以改变接口,但是它改变接口的原因是为了简化接口。这个模式被巧妙地命名为外观模式(Facadce-Pattern),之所以这么称呼,是因为它将一个或数个类的复杂的一切都隐藏在背后,只暴露出一个干净美好的外观。
2.1需求
很多人都爱去电影院看电影,但是如果能够在家就体会到电影院的效果,那就更方便了。现在我们就准备设计一个"家庭电影院"这样的系统。
经过一番精心设计,现在组装了一套豪华的系统,包括Dvd播放机、Cd播放机、立体声、剧院屏幕等。并且我们又花费了好几天的时间进行系统布线、连接所有装置、调试等操作。系统类图如下:
2.2最初的家庭影院系统
类图:
现在我们开始运行系统,开始准备观看喜爱的电影。
播放电影(使用最初的影院系统):
- 打开爆米花机
- 开始爆米花
- 灯光调暗
- 放下屏幕
- 打开投影机
- 投影机的输入接到DVD
- 投影机设置为宽屏模式
- 打开功放
- 将功放的输入设置为DVD
- 功放设置为环绕立体声
- 调节音量到14
- 打开DVD播放器
- 开始播放DVD
将上述步骤转换成代码调用:
可以看到,电影的播放非常的繁琐,而且在观影结束后,还需要将操作再倒序操作一遍。这简直令人崩溃,那么外观模式是如何应对这类问题的。
2.3有外观模式改进的家庭影院
通过观察最初的设计方案,我们发现一个弊端,拿projector(投影仪)举例子,投影仪的打开、防止DVD、设置为宽屏模式这些都需要我们亲力亲为,显得很麻烦,那么我们可不可以将它封装一下,仅仅暴露出一个方法,这个方法将所有关于投影仪的操作都帮我们完成了呢?
这就是外观模式的设计初衷。现在我们可以设计一个新类HomeTheaterFacade,它为我们提供一下几种方法:看电影、关电影、听CD、关CD、听广播、关广播。这样我们一键就可以执行观影的的操作。
类图:
那么只要我们按下遥控器上的一个按钮,之前所有的观影操作都一气呵成。
补充说明:
- 有一点我们需要注意,就是HomeTheaterFacade为我们提供了几个简单的操作接口,但是这并不意味着HomeTheaterFacade就屏蔽了底层的细节,这些底层的操作我们仍然是可以访问的,提供简单接口的同时,仍然将完整的功能暴露出来。
- 一个子系统可以有多个外观。
- 外观不只是简化接口,而且还可以让客户从组件的子系统中解耦,距离来说,如果我们有钱后购买了的新的爆米花机,如果当初的客户代码是针对的外观改编而不是子系统编程,那么修改系统是不会对客户造成影响的。
- 有的人认为适配器模式和外观模式的差别在于:适配器包装一个类,而外观可以代表多个类。其实这种说法是错误的,因为适配器模式可以将一个或多个类接口变成客户所期望的一个接口。同样的,一个外观也可以只针对一个拥有复杂接口的类提供简化的接口。所以二者的差异,并不在于它们"包装"了几个类,而是在于它们的意图。适配器的意图是:将接口转换成另一个接口。外观模式的意图是:提供子系统的一个简化接口。
2.4外观模式代码实现
影院组件-投影仪(其它组件与投影仪类似,在此忽略):
//投影仪
public class Projector {
// 描述
String description;
// DVD播放器
DvdPlayer dvdPlayer;
public Projector(String description, DvdPlayer dvdPlayer) {
this.description = description;
this.dvdPlayer = dvdPlayer;
}
// 开启
public void on() {
System.out.println(description + " on");
}
// 关闭
public void off() {
System.out.println(description + " off");
}
// 宽屏模式
public void wideScreenMode() {
System.out.println(description + " 处于宽屏模式 (16x9 aspect ratio)");
}
// tv模式
public void tvMode() {
System.out.println(description + " 处于tv模式 (4x3 aspect ratio)");
}
public String toString() {
return description;
}
}
外观模式影院:
//外观模式影院
public class HomeTheaterFacade {
//使用组合的方式,用到的影院组件全在这里
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;
}
//观看电影
public void watchMovie(String movie) {
System.out.println("准备观看电影。。。。。。。");
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);
}
//结束观看
public void endMovie() {
System.out.println("准备关闭电影。。。。。。");
popper.off();
lights.on();
screen.up();
projector.off();
amp.off();
dvd.stop();
dvd.eject();
dvd.off();
}
}
测试类:
//测试类
public class HomeTheaterTestDrive {
public static void main(String[] args) {
/*
* 初始化各个组件
* 我们在这个测试程序中,直接建立了这些组件。
* 正常情况下,某个外观会指派组件给客户使用,
* 而不需要客户自行创建外观。
*/
Amplifier amp = new Amplifier("Top-O-Line Amplifier");
Tuner tuner = new Tuner("Top-O-Line AM/FM Tuner", amp);
DvdPlayer dvd = new DvdPlayer("Top-O-Line DVD Player", amp);
CdPlayer cd = new CdPlayer("Top-O-Line CD Player", amp);
Projector projector = new Projector("Top-O-Line Projector", dvd);
TheaterLights lights = new TheaterLights("Theater Ceiling Lights");
Screen screen = new Screen("Theater Screen");
PopcornPopper popper = new PopcornPopper("Popcorn Popper");
//创建HomeTheaterFacade
HomeTheaterFacade homeTheater =
new HomeTheaterFacade(amp, tuner, dvd, cd,
projector, screen, lights, popper);
//播放电影
homeTheater.watchMovie("国产零零漆");
//关闭电影
homeTheater.endMovie();
}
}
输出结果:
准备观看电影。。。。。。。
Popcorn Popper 开启
Popcorn Popper 崩爆米花!
Theater Ceiling Lights 调节亮度到 10%
Theater Screen 下降
Top-O-Line Projector 开启
Top-O-Line Projector 处于宽屏模式 (16x9 aspect ratio)
Top-O-Line Amplifier 开启
Top-O-Line Amplifier 设置DVD Top-O-Line DVD Player
Top-O-Line Amplifier 环绕声 (5 speakers, 1 subwoofer)
Top-O-Line Amplifier 设置音量为 5
Top-O-Line DVD Player 开启
Top-O-Line DVD Player 放映 "国产零零漆"
准备关闭电影。。。。。。
Popcorn Popper 关闭
Theater Ceiling Lights 开启
Theater Screen 升起
Top-O-Line Projector 关闭
Top-O-Line Amplifier 关闭
Top-O-Line DVD Player 关闭 "国产零零漆"
Top-O-Line DVD Player 喷射
Top-O-Line DVD Player 关闭
3.定义外观模式
外观模式
提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
外观模式的意图是要提供一个简单的接口,好让子系统更容易使用。
类图:
外观模式看起来比较好理解,但是它的作用却很大,外观模式也可以帮我们遵循一个新的面向对象原则-"最少知识"原则。
4."最少知识"原则
4.1定义
"最少知识"原则
要减少对象之间的交互,只和几个最亲密的人谈话。
意思是,当我们在设计一个系统时,不管任何对象,都要注意它所交互的类有哪些,并注意它和这些类之间是如何交互的。
这个原则希望我们在设计中,不要让太多的类耦合在一起,免得修改系统中一部分,会影响到其他部分。如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,它需要花许多成本维护,也会因为太复杂而不容易被其他人了解。
4.2遵守"最少知识"原则的方针
就任何对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:
- 该对象本身。
- 被当作方法的参数而传进来的对象。
- 此方法所创建或实例化的任何对象。
- 对象的任何组件("组件"指的是被实例变量所引用的任何对象,换句话说,此对象和它的"组件"之间是"有一个"(has-a)的关系。
仔细阅读上面的方针,我们发现有一种调用是不被推荐的,那就是不要调用方法中的返回对象的方法,即如果某个对象是某个其它方法的返回结果,那么不要调用该对象的方法。
听上去有点难理解,这里以从气象站获取温度为例:
不推荐的写法:
推荐的写法:
原因:
为什么上面的写法不推荐,这样写有什么弊端呢?
如果我们这样做,相当于向另一个对象的子部分发请求,会增加我们直接接触的对象的数目。而"最少知识原则"将代码改为要求这个对象为我们做出请求(即我们不直接和对象的组件接触,而是拜托该对象,让它去向它的组件索取数据),这样一来,我们就不需要认识该对象的组件了,从而让我们的社交圈子维持在最小的状态(出现变动,受影响也最小)。
"最少知识"案例:
这是一个汽车类,start()中的方法调用遵循了"最少知识"原则:
4.3补充说明
1).最少知识的缺点:
虽然这个原则减少了对象之间的依赖,会减少软件的维护成本。但是采用这个原则也会倒置更多"包装"类被制造出来,以处理和其它组件的沟通,这可能会导致复杂度和开发时间的增加,并降低运行时的性能。
2).墨忒耳法制(Law of Demeter)
其实墨忒耳法制和最少知识原则是同一个原则,但是我们习惯叫做最少知识原则。
5.外观模式和最少知识原则
6.几种模式的对比
最后对比几种设计模式来结束外观模式的介绍。
- 适配器模式:将一个对象包装起来以改变其接口。
- 装饰者模式:将一个对象包装起来以增加新的行为和责任。
- 外观模式:将一群对象"包装"起来以简化其接口。