headfirst设计模式(8)—适配器模式与外观模式
前言
这一章主要讲2个模式,一个是,适配器模式(负责将一个类的接口适配成用户所期待的),另外一个是外观模式(为子系统提供一个共同的对外接口),看完的第一反应是,为什么要把它们两放在同一章,难道它们有什么不可告人的秘密?
难道是因为他们俩都很简单吗?不会不会,毕竟是大名鼎鼎的headfirst,怎么可能这么草率,这我是万万不相信的!
细想了一下,我和工作的点点滴滴,我发现,一般到项目的后期,好像都比较容易用上这两个东西...
当然,项目的后期并不是说一个项目自己从头发开到尾的项目,而是在它生命周期的后半段,比如适配器,用来适配老的接口,比如外观模式,用来隐藏各个子系统,各个模块的协作细节。
不过外观模式却不一定都是在后期才发力的:
1,前期如果系统比较复杂,在系统规划的时候,就会有意识的对系统分层,为上层模块提供一些高级的api。
2,在系统的中期呢,开发过程中,发现子系统越来越复杂,也可以提供类似的操作。
3,在系统后期,模块越来越多,功能越来越复杂,还有历史原因,外观模式就更加有用了,毕竟,有一个简单易用的api,比手动调用各个系统的逻辑那简直是不要太舒服!
为什么后期不重构?而是要做这些修修补补的工作呢?
举个例子,房子上有棵树,你觉得这棵树很碍事,就把树给干掉了,因为你以为,是房子上面长的,结果呢?特么是树把房子吊着的!类似的坑实在是太多了,所以,重构需谨慎,且构且珍惜。
当然不是说重构不好,而是要综合考量各方面的因素,而且,重构也用得上这些啊,毕竟,重构不是重写...(诶,重写好像也要用)
适配器模式
先看看鸭子接口(对应target)
/** * 鸭子接口 */ public interface duck { /** * 鸭叫 */ void quack(); /** * 飞行 */ void fly(); }
然后看一下火鸡的接口和实现类(对应adaptee)
/** * 火鸡接口 */ public interface turkey { /** * 火鸡叫 */ void gobble(); /** * 飞行 */ void fly(); } /** * 野火鸡 */ public class wildturkey implements turkey { public void gobble() { system.out.println("咯咯"); } public void fly() { system.out.println("我在飞,虽然我飞的很近"); } }
首先可以看出,它们的之间有一些共同之处,都有叫声,都可以飞行,这个也是适配的前提,有共同点!
如果没有共同点,是不是去隔壁的其他设计模式看看?
ok,接下来开始适配操作
火鸡适配器(adapter)
/** * 火鸡适配器 */ public class turkeyadapter implements duck { turkey turkey;//持有一个火鸡对象 public turkeyadapter(turkey turkey) { this.turkey = turkey; } /** * 鸭叫 */ public void quack() { turkey.gobble(); } /** * 飞行 */ public void fly() { //适配的时候,这里模拟飞行5次 for(int i= 0; i < 5; i++) { turkey.fly(); } } }
适配器的逻辑也很简单
首先,实现duck接口,要让client能够调用,那么首先得长得和别人一样啊
其次,持有一个真正的处理对象,然后再根据duck接口来进行适配,比如这里,quack接口,就直接调用turkey#gobble(),而fly()可能是因为某种神秘力量,需要火鸡飞行的距离和鸭子一样远,所以需要手动去适配,在这里添加了适配的代码
最后,适配器的作用就是把一个类转换成另外一个类,转换的时候可能需要一些逻辑上的处理,让它能符合用户的期待
public class duckclient { public static void main(string[] args) { //初始化一只火鸡 wildturkey turkey = new wildturkey(); //伪装成一只鸭子 duck duck = new turkeyadapter(turkey); system.out.println("鸣叫:"); duck.quack(); system.out.println("------------------"); system.out.println("飞行:"); duck.fly(); } }
结果:
外观模式
核心思想:为子系统们提供一套通用的对外接口(高级api)
为什么会有这样的需求呢?
各个子系统在设计过程中,或者在实际使用的过程中会发现,有一些通用的步骤,对于更加高的调用层来说,它们其实不需要知道底层是通过哪些步骤来实现的,更多的是,以一个统一的接口来调用。
比如,在想在家里搞一个家庭影院,需要以下步骤:
1,灯光不能太亮,亮度需要调低到10
2,需要打开投影机,并且要调整到宽屏模式
3,音响需要调整成环绕立体音,音量设置成5
4,打开dvd开始播放
代码如下:
灯光:
/** * 影院灯光 */ public class theaterlights { string description; public theaterlights(string description) { this.description = description; } public void on() { system.out.println(description + " 打开"); } public void off() { system.out.println(description + " 关闭"); } public void dim(int level) { system.out.println(description + " 亮度调节到:" + level + "%"); } public string tostring() { return description; } }
投影仪:
/** * 投影仪屏幕 */ public class screen { string description; public screen(string description) { this.description = description; } public void up() { system.out.println(description + " 上升"); } public void down() { system.out.println(description + " 下降"); } public string tostring() { return description; } } /** * 投影仪 */ public class projector { string description; dvdplayer dvdplayer; public projector(string description, dvdplayer dvdplayer) { this.description = description; this.dvdplayer = dvdplayer; } public void on() { system.out.println(description + " 打开"); } public void off() { system.out.println(description + " 关闭"); } public void widescreenmode() { system.out.println(description + " 调整到宽屏模式"); } public void tvmode() { system.out.println(description + " 调整到tv模式"); } public string tostring() { return description; } }
音响:
/** * 音响 */ public class amplifier { string description; public amplifier(string description) { this.description = description; } public void on() { system.out.println(description + " 打开"); } public void off() { system.out.println(description + " 关闭"); } //立体声 public void setstereosound() { system.out.println(description + " 立体声模式"); } //环绕声 public void setsurroundsound() { system.out.println(description + " 环绕声模式"); } public void setvolume(int level) { system.out.println(description + " 调整音量到: " + level); } public string tostring() { return description; } }
dvd播放器:
/** * dvd播放器 */ public class dvdplayer { string description; int currenttrack; amplifier amplifier; string movie; public dvdplayer(string description, amplifier amplifier) { this.description = description; this.amplifier = amplifier; } public void on() { system.out.println(description + " 播放"); } public void off() { system.out.println(description + " 关闭"); } public void play(string movie) { this.movie = movie; currenttrack = 0; system.out.println(description + " 播放 \"" + movie + "\""); } public string tostring() { return description; } }
不重要的代码就折叠了,免得难得看,不使用外观模式,需要调用一堆代码:
/** * 不使用外观模式 */ public class client { public static void main(string[] args) { amplifier amp = new amplifier("top-o-line 扬声器"); dvdplayer dvd = new dvdplayer("top-o-line dvd播放器", amp); projector projector = new projector("top-o-line 投影仪", dvd); theaterlights lights = new theaterlights("客厅灯"); screen screen = new screen("投影仪银幕"); system.out.println("准备看电影..."); lights.dim(10); screen.down(); projector.on(); projector.widescreenmode(); amp.on(); amp.setsurroundsound(); amp.setvolume(5); dvd.on(); dvd.play("夺宝奇兵"); } }
使用外观模式,一行解决:
/** * 使用外观模式后的测试类 */ public class facadeclient { private static hometheaterfacade home_theater; static{ amplifier amp = new amplifier("top-o-line 扬声器"); dvdplayer dvd = new dvdplayer("top-o-line dvd播放器", amp); projector projector = new projector("top-o-line 投影仪", dvd); theaterlights lights = new theaterlights("客厅灯"); screen screen = new screen("投影仪银幕"); home_theater = new hometheaterfacade(amp, dvd, projector, screen, lights); } public static void main(string[] args) { //看电影 home_theater.watchmovie("夺宝奇兵"); } }
我擦?咋还是这么多行?
static块里面的代码是初始化代码,一般使用spring,都是依赖注入的东西,其实调用就一行:
home_theater.watchmovie("夺宝奇兵");
但是能够一键解决的,更多的是一些通用的操作,比如说,例子中,灯光不能太亮,你想把它调到5,不想用默认的10,,那么可能就只能自己写一遍外观模式封装的逻辑了。
那么这里就有个问题了,能不能重载方法,让它支持可以自定义灯光亮度这个参数呢?对于这个我只能说,要看业务需求了,如果100个人里面只有1个人用,那么对于系统产生的复杂度可能比 产生的价值高,反过来,可能就需要去实现。
但是,如果这种需求越来越多,系统变得越来越复杂,那外观模式还是一个简单可爱的小姐姐吗?如果不实现,就无法达到隐藏子系统复杂度的痛点,如果实现,就会产生新的api调用的复杂度,我终于知道为啥我特么还在学习设计模式了...
说了这么多,说说它的优缺点吧
优点:
1,对客户屏蔽了子系统组件使用起来门槛更低。
2,实现了子系统与客户之间的松耦合关系。
3,虽然提供了访问子系统的统一入口,但是并不影响用户直接使用子系统类。
缺点:
1,通过外观类访问子系统时,减少了可变性和灵活性。
2,在新的子系统加入,或者子系统接口变更时,可能需要修改外观类或客户端的源代码,违背了“开闭原则”。