设计模式07—适配器模式与外观模式

桃扇骨 2023-10-11 16:23 118阅读 0赞

上一篇:《命令模式》

7.适配器模式与装饰者模式

7.1 为什么需要适配器模式

我下面举一个简单的例子帮助大家理解为何使用适配器模式

如果我们欧洲的国家购买了美国生产的电脑,那么他们还需要额外的使用一个交流电的适配器,原因如下图所示:
在这里插入图片描述

通过上图我们可以看出适配器的作用就是它位于美式插头和欧式插头中间,其工作就是将欧式插座转换成美式插座,使美式的插头可以插进去,简单来说就是适配器改变了插座的接口,已符合美式笔记本的需求。
某些交流电适配器相当简单,它们只是改变插座的形状来匹配你的插头,直接把电流传送过去。但是有些适配器内部则是相当复杂,可能会改变电流符合装置的需求。

而我们接下来所学到的面向对象的适配器原则就是:将一个接口转换成另一个接口,以符合用户的期望

7.2 面向对象适配器

假设已有一个软件系统,你希望它能和一个新的厂商类库搭配使用,但是这个新厂商所设计出来的接口,不同于旧厂商的接口:
在这里插入图片描述

你不想改变现有的代码,解决这个问题(而且你也不能改变厂商的代码)。所以该怎么做
在这里插入图片描述

适配器就像是一个中间人,他将客户所发出的请求转换成厂商类能理解的请求
在这里插入图片描述

下面我们通过一个具体的实例去看看适配器是具体怎么工作的
我们现在有个一个鸭子对象

  1. public interface Duck {
  2. /**
  3. * 所有的鸭子都具有两种行为
  4. **/
  5. //1.呱呱叫
  6. public void quack();
  7. //2.飞行行为
  8. public void fly();
  9. }

绿头鸭是鸭子的子类

  1. /**
  2. * 绿头鸭
  3. */
  4. public class MallardDuck implements Duck {
  5. @Override
  6. public void quack() {
  7. System.out.println("呱呱叫");
  8. }
  9. @Override
  10. public void fly() {
  11. System.out.println("我会飞");
  12. }
  13. }

接下来我们引入火鸡类

  1. public interface Turkey {
  2. //只会咯咯叫
  3. void gobble();
  4. //会飞
  5. void fly();
  6. }
  7. public class WildTurkey implements Turkey{
  8. @Override
  9. public void gobble() {
  10. System.out.println("咯咯叫");
  11. }
  12. @Override
  13. public void fly() {
  14. System.out.println("我会飞一小段距离");
  15. }
  16. }

现在,假设你缺鸭子对象,想用一些火鸡对象来冒充。显而易见,因为火鸡的接口不同,所以我们不能公然拿来用。
为了使火鸡接口能够适配我们的鸭子模型,所以我们需要一个火鸡接口的适配器

  1. /**
  2. * 首先,我们需要实现想转换成的类型接口,也就是你的客户所期望看到的接口。
  3. */
  4. public class TurkeyAdapter implements Duck {
  5. Turkey turkey;
  6. //接着,需要取得适配的对象引用,这里我们利用构造器取得这个引用。
  7. public TurkeyAdapter(Turkey turkey) {
  8. this.turkey = turkey;
  9. }
  10. @Override
  11. public void quack() {
  12. turkey.gobble();
  13. }
  14. /**
  15. * 显然两个接口都具备了fly()方法,火鸡的飞行距离很短,
  16. * 不像鸭子可以长途飞行。主要让鸭子的飞行和火鸡的飞行能够对应.必须连续五次调用火鸡的fly()来完成。
  17. */
  18. @Override
  19. public void fly() {
  20. for (int i = 0; i < 5; i++) {
  21. turkey.fly();
  22. }
  23. }
  24. }

测试:

  1. public class DuckTestDriver {
  2. public static void main(String[] args) {
  3. /**先创建一只鸭子和一只火鸡**/
  4. MallardDuck duck = new MallardDuck();
  5. WildTurkey turkey = new WildTurkey();
  6. /**将火鸡包装进一个火鸡适配器中,使他看起来像一只鸭子**/
  7. Duck turkeyAdapter = new TurkeyAdapter(turkey);
  8. /**测试这只火鸡,让他咯咯叫**/
  9. System.out.print("火鸡叫声:");
  10. turkey.gobble();
  11. System.out.print("鸭子叫声:");
  12. duck.quack();
  13. System.out.print("使用适配器的鸭子叫声:");
  14. turkeyAdapter.quack();
  15. System.out.println("-----------使用适配器的鸭子飞行行为:-------------");
  16. turkeyAdapter.fly();
  17. }
  18. }

测试结果如下:
在这里插入图片描述

7.3 适配器解析

现在我们已经知道什么是适配器了,让我们后退一步,再次看看各部分之间的关系。
在这里插入图片描述

客户使用适配器的过程如下:

  • 1.客户通过目标接口调用适配器的方法对适配器发出请求。
  • 2.适配器使用被适配者接口把请求转换成被适配者的一个或多个调用接口。
  • 3.客户接收到调用的结果,但并未察觉这一切是适配器在起转换作用。

7.4 适配器模式的定义

在这里插入图片描述

这个模式可以通过创建适配器进行接口转换,让不兼容的接口变成兼容。这可以让客户从实现的接口解耦。如果在一段时间之后,我们想要改变接口,适配器可以将改变的部分封装起来,客户就不必为了应对不同的接口而每次跟着修改。
在这里插入图片描述

7.5 对象和类的适配器

实际上有两种适配器:对象适配器,类适配器;
在前面的叙述中,我们已经描述了对象适配器。
而类适配器:
在这里插入图片描述

唯一的差别就在于类适配器继承了Target和Adaptee。而对象适配器利用组合的方式将请求传送给被适配者。

7.6 枚举器和迭代器的适配

枚举器:
在这里插入图片描述

迭代器:
在这里插入图片描述

接下来我们尝试将枚举器适配到迭代器上
在这里插入图片描述

设计适配器

这个类应该是这样的:我们需要一个适配器,实现了目标接口,而此目标接口是由被适配者所组合的。hasNext()和next()方法很容易实现,直接把它们从目标对应到被适配者就可以了。但是对于remove()方法,我们又该怎么办?目前,类图是这样的:
在这里插入图片描述

处理remove()方法

我们知道枚举不支持删除,因为枚举是一个“只读”接口。适配器无法实现一个有实际功能的remove()方法,最多只能抛出一个运行时异常。幸运地,迭代器接口的设计者事先料到了这样的需要,所以将remove()方法定义成会抛出UnsupportedOpeartionException。
在这个例子中,我们看到了适配器并不完美,客户必须小心潜在的异常,但只要客户够小心,而且适配器的文档能做出说明,这也算是一个合理的解决方案。
接下来我们开始编写EnumeratorIterator

  1. /**
  2. * 因为我们将枚举追配成迭代器,适配器需要实现迭代器接口
  3. * 适配器必须看起来就像是一个迭代器。
  4. */
  5. public class EnumeratorIterator implements Iterator {
  6. Enumeration enumeration;
  7. /**
  8. * 我们利用组合的方式,将枚举组合进入适配器中,所以用一个实例变量记录枚举。
  9. *
  10. * @param enumeration
  11. */
  12. public EnumeratorIterator(Enumeration enumeration) {
  13. this.enumeration = enumeration;
  14. }
  15. /**
  16. * 选代器的hasNext()方法其实是委托给枚举的hasMoreElements()方法。
  17. *
  18. * @return
  19. */
  20. @Override
  21. public boolean hasNext() {
  22. return enumeration.hasMoreElements();
  23. }
  24. /**
  25. * 而选代器的next()方法其实是委托给枚举的nextElement()方法
  26. *
  27. * @return
  28. */
  29. @Override
  30. public Object next() {
  31. return enumeration.nextElement();
  32. }
  33. /**
  34. * 很不幸,我们不能支持选代器的remove()方法,所以必须放弃。
  35. * 在这里、我们的做法是抛出一个异常
  36. */
  37. @Override
  38. public void remove() {
  39. throw new UnsupportedOperationException();
  40. }
  41. }

接下来我们做个测试:

  1. public class EnumerationIteratorTestDrive {
  2. public static void main(String[] args) {
  3. Integer[] list = new Integer[]{
  4. 1, 2, 3, 4, 5};
  5. Vector<Integer> v = new Vector<Integer>(Arrays.asList(list));
  6. Iterator<?> iterator = new EnumeratorIterator(v.elements());
  7. while (iterator.hasNext()) {
  8. System.out.print(iterator.next());
  9. System.out.print(" ");
  10. }
  11. }
  12. }

学到此处,你已经知道适配器模式是如何将一个类的接口转换成另一个符合客户期望的接口的。你也知道在Java中要做到这一点,必须将一个不兼容接口的对象包装起来,变成兼容的对象。
我们现在要看一个改变接口的新模式,但是它改变接口的原因是为了简化接口。这个模式被巧妙地命名为外观模式(Facade-Pattern),之所以这么称呼,是因为它将一个或数个类的复杂的一切都隐藏在背后,只显露出一个干净美好的外观。
在这里插入图片描述

接下来我们通过一个简单的例子让展示外观模式的强大之处
有一套系统,内含DVD播放器、投影机、自动屏幕、环绕立体声,甚至还有爆米花机。
接下来就是开始观看电影,但是在观看之前,你需要做以下事情:
在这里插入图片描述

但是你将会面临以下麻烦:

  • 看完电影后,你还要把一切都关掉,怎么办?难道要反向地把这一切动作再进行一次?
  • 如果要听CD或者广播,难道也会这么麻烦?
  • 如果你决定要升级你的系统,可能还必须重新学习一套稍微不同的操作过程。

怎么办?使用你的家庭影院竟变得如此复杂!让我们看看外观模式如何解决这团混乱,好让你能轻易地享受电影……

7.7 灯光 相机 外观

你需要的正是一个外观:有了外观模式,通过实现一个提供更合理的接口的外观类,你可以将一个复杂的子系统变得容易使用。如果你需要复杂子系统的强大威力,别担心,还是可以使用原来的复杂接口的;但如果你需要的是一个方便使用的接口,那就使用外观。
在这里插入图片描述

现在,你的客户代码可以调用此家庭影院外观所提供的方法,而不必再调用这个子系统的方法。所以,想要看电影,我们只要调用一个方法(也就是watchMovie())就可以了。灯光、CVD播放器、投影机,屏幕幕、爆米花,一口气全部搞定。
外观只是提供你更直接的操作,未将原来的子系统阻隔起来。如果你需要子系统类的更高层功能,还是可以使用原来的子系统。
外观不只是简化了接口,也将客户从组件的子系统中解耦。
外观和适配器可以包装许多类,但是外观的意图是简化接口,而适配器的意图是将接口转换成不同接口。
接下来我们开始构造家庭影院的外观:

  1. public class HomeTheaterFacade {
  2. Amplifier amp;//扩音器
  3. Tuner tuner;//广播
  4. StreamingPlayer player;//流媒体播放器
  5. CdPlayer cd;//cd
  6. Projector projector;//投影仪
  7. TheaterLights lights;//剧院灯光
  8. Screen screen;//屏幕
  9. PopcornPopper popper;//爆米花
  10. public HomeTheaterFacade(Amplifier amp,
  11. Tuner tuner,
  12. StreamingPlayer player,
  13. Projector projector,
  14. Screen screen,
  15. TheaterLights lights,
  16. PopcornPopper popper) {
  17. this.amp = amp;
  18. this.tuner = tuner;
  19. this.player = player;
  20. this.projector = projector;
  21. this.screen = screen;
  22. this.lights = lights;
  23. this.popper = popper;
  24. }
  25. /**
  26. * 看电影
  27. *
  28. * @param movie
  29. */
  30. public void watchMovie(String movie) {
  31. System.out.println("Get ready to watch a movie...");
  32. popper.on();
  33. popper.pop();
  34. lights.dim(10);
  35. screen.down();
  36. projector.on();
  37. projector.wideScreenMode();
  38. amp.on();
  39. amp.setStreamingPlayer(player);
  40. amp.setSurroundSound();
  41. amp.setVolume(5);
  42. player.on();
  43. player.play(movie);
  44. }
  45. /**
  46. * 结束电影
  47. */
  48. public void endMovie() {
  49. System.out.println("Shutting movie theater down...");
  50. popper.off();
  51. lights.on();
  52. screen.up();
  53. projector.off();
  54. amp.off();
  55. player.stop();
  56. player.off();
  57. }
  58. /**
  59. * 听广播
  60. *
  61. * @param frequency
  62. */
  63. public void listenToRadio(double frequency) {
  64. System.out.println("Tuning in the airwaves...");
  65. tuner.on();
  66. tuner.setFrequency(frequency);
  67. amp.on();
  68. amp.setVolume(5);
  69. amp.setTuner(tuner);
  70. }
  71. /**
  72. * 结束广播
  73. */
  74. public void endRadio() {
  75. System.out.println("Shutting down the tuner...");
  76. tuner.off();
  77. amp.off();
  78. }
  79. }

至于里面用到的组件,我就不在此赘述,点击此处下载
在这里插入图片描述

接下来我们进行测试

  1. public class HomeTheaterTestDrive {
  2. public static void main(String[] args) {
  3. Amplifier amp = new Amplifier("Amplifier");
  4. Tuner tuner = new Tuner("AM/FM Tuner", amp);
  5. StreamingPlayer player = new StreamingPlayer("Streaming Player", amp);
  6. CdPlayer cd = new CdPlayer("CD Player", amp);
  7. Projector projector = new Projector("Projector", player);
  8. TheaterLights lights = new TheaterLights("Theater Ceiling Lights");
  9. Screen screen = new Screen("Theater Screen");
  10. PopcornPopper popper = new PopcornPopper("Popcorn Popper");
  11. HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade(amp, tuner, player,
  12. projector, screen, lights, popper);
  13. homeTheaterFacade.watchMovie("Raiders of the Lost Ark");
  14. homeTheaterFacade.endMovie();
  15. }
  16. }

运行结果如下:
在这里插入图片描述

7.8 外观模式的定义

想要使用外观模式,我们创建了一个接口简化而统一的类,用来包装子系统中一个或多个复杂的类。
外观模式相当直接,很容易理解,这方面和许多其他的模式不太一样。但这并不会降低它的威力:外观模式允许我们让客户和子系统之间避免紧耦合,而且稍后你还会看到,外观模式也可以帮我们遵守一个新的面向对象原则。
在这里插入图片描述

从下图我们可以很直观的理解外观模式的意图就是提供一个简单的接口,好让一个子系统更易于使用。
在这里插入图片描述

7.9 最少知识原则

最少知识(Least Knowledge)原则告诉我们要减少对象之间的交互,只留下几个“密友”。
也就是说,当你正在设计一个系统,不管是任何对象,你都要注意它所交互的类有哪些,并注意它和这些类是如何交互的。
这个原则希望我们在设计中,不要让太多的类耦合在一起,免得修改系统中一部分,会影响到其他部分。如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,它需要花许多成本维护,也会因为太复杂而不容易被其他人了解。

7.10 要点

  • 当需要使用一个现有的类而其接口并不符合你的需要时,就使用适配器。
  • 当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观。
  • 适配器改变接口以符合客户的期望。
  • 外观将客户从一个复杂的子系统中解耦。
  • 实现一个适配器可能需要一番功夫,也可能不费功夫,视目标接口的大小与复杂度而定。
  • 实现一个外观,需要将子系统组合进外观中,然后将工作委托给子系统执行。
  • 适配器模式有两种形式:对象适配器和类适配器。类适配器需要用到多重继承。
  • 适配器将一个对象包装起来以改变其接口:装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象“包装”起来以简化其接口。

7.11 设计原则

封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之问的松耦合设计而努力
类应该对扩展开放,对修改关闭。
依赖抽象,不要依赖具体类。、
只和朋友交谈
在这里插入图片描述
下一篇 《模板方法模式》

发表评论

表情:
评论列表 (有 0 条评论,118人围观)

还没有评论,来说两句吧...

相关阅读