使用C# (.NET Core) 实现适配器模式 (Adapter Pattern) 和外观模式 (Facade Pattern)
本文的概念内容来自深入浅出设计模式一书
现实世界中的适配器(模式)
我带着一个国标插头的笔记本电脑, 来到欧洲, 想插入到欧洲标准的墙壁插座里面, 就需要用中间这个电源适配器.
面向对象的适配器
你有个老系统, 现在来了个新供应商的类, 但是它们的接口不同, 如何使用这个新供应商的类呢?
首先, 我们不想修改现有代码, 你也不能修改供应商的代码. 那么你只能写一个可以适配新供应商接口的类了:
这里, 中间的适配器实现了你的类所期待的接口, 并且可以和供应商的接口交互以便处理你的请求.
适配器可以看作是中间人, 它从客户接收请求, 并把它们转化为供应商可以理解的请求:
所有的新代码都写在适配器里面了.
鸭子的例子
有这么一句话不知道您听过没有: 如果它路像个鸭子, 叫起来也像个鸭子, 那它就是个鸭子. (例如: Python里面的duck typing)
这句话要是用来形容适配器模式就得这么改一下: 如果它走路像个鸭子, 叫起来也像个鸭子, 那么它可能是一个使用了鸭子适配器的火鸡....
看一下代码的实现:
鸭子接口:
namespace AdapterPattern.Abstractions { public interface IDuck { void Quack(); void Fly(); } }
野鸭子:
using AdapterPattern.Abstractions; namespace AdapterPattern { public class MallardDuck : IDuck { public void Fly() { System.Console.WriteLine("Flying"); } public void Quack() { System.Console.WriteLine("Quack"); } } }
火鸡接口:
namespace AdapterPattern.Abstractions { public interface ITurkey { void Gobble(); void Fly(); } }
野火鸡:
using AdapterPattern.Abstractions; namespace AdapterPattern.Turkies { public class WildTurkey : ITurkey { public void Fly() { System.Console.WriteLine("Gobble gobble"); } public void Gobble() { System.Console.WriteLine("I'm flying a short distance"); } } }
火鸡适配器:
using AdapterPattern.Abstractions; namespace AdapterPattern.Adapters { public class TurkeyAdapter : IDuck { private readonly ITurkey turkey; public TurkeyAdapter(ITurkey turkey) { this.turkey = turkey; } public void Fly() { for (int i = 0; i < 5; i++) { turkey.Fly(); } } public void Quack() { turkey.Gobble(); } } }
测试运行:
using System; using AdapterPattern.Abstractions; using AdapterPattern.Adapters; using AdapterPattern.Turkies; namespace AdapterPattern { class Program { static void Main(string[] args) { DuckTestDrive(); } static void DuckTestDrive() { IDuck duck = new MallardDuck(); var turkey = new WildTurkey(); IDuck turkeyAdapter = new TurkeyAdapter(turkey); System.Console.WriteLine("Turkey says........."); turkey.Gobble(); turkey.Fly(); System.Console.WriteLine("Duck says........."); TestDuck(duck); System.Console.WriteLine("TurkeyAdapter says........."); TestDuck(turkeyAdapter); } static void TestDuck(IDuck duck) { duck.Quack(); duck.Fly(); } } }
这个例子很简单, 就不解释了.
理解适配器模式
Client 客户实现了某种目标接口, 它发送请求到适配器, 适配器也实现了该接口, 并且适配器保留着被适配者的实例, 适配器把请求转化为可以在被适配者身上执行的一个或者多个动作.
客户并不知道有适配器做着翻译的工作.
其他:
适配器可以适配两个或者多个被适配者.
适配器也可以是双向的, 只需要实现双方相关的接口即可.
适配器模式定义
适配器模式把一个类的接口转化成客户所期待的另一个接口. 适配器让原本因接口不兼容而无法一起工作的类成功的工作在了一起.
类图:
其中 Client只知道目标接口, 适配器实现了这个目标接口, 适配器是通过组合的方式与被适配者结合到了一起, 所有的请求都被委托给了被适配者.
对象适配器和类适配器
一共有两类适配器: 对象适配器和类适配器.
之前的例子都是对象适配器.
为什么没有提到类适配器?
因为类适配器需要多继承, 这一点在Java和C#里面都是不可以的. 但是其他语言也许可以例如C++?
它的类图是这样的:
这个图看着也很眼熟, 这两种适配器唯一的区别就是: 类适配器同时继承于目标和被适配者, 而对象适配器使用的是组合的方式来把请求传递给被适配者.
通过鸭子的例子来认识两种适配器的角色
类适配器:
类适配器里面, 客户认为它在和鸭子谈话, 目标就是鸭子类, 客户调用鸭子上面的方法. 火鸡没有和鸭子一样的方法, 但是适配器可以接收鸭子的方法调用并把该动作转化为调用火鸡上面的方法. 适配器让火鸡可以响应一个针对于鸭子的请求, 实现方法就是同时继承于鸭子类和火鸡类
对象适配器:
对象适配器里, 客户仍然认为它在和鸭子说话, 目标还是鸭子类, 客户调用鸭子类的方法, 适配器实现了鸭子类的接口, 但是当它接收到方法调用的时候, 它把该动作转化委托给了火鸡. 火鸡并没有实现和鸭子一样的接口, 多亏了适配器, 火鸡(被适配者)将会接收到客户针对鸭子接口的方法调用.
两种适配器比较:
对象适配器: 使用组合的方式, 不仅能是配一个被适配者的类, 还可以适配它的任何一个子类.
类适配器: 只能适配一个特定的类, 但是它不需要重新实现整个被适配者的功能. 而且它还可以重写被适配者的行为.
对象适配器: 我使用的是组合而不是继承, 我通过多写几行代码把事情委托给了被适配者. 这样很灵活.
类适配器: 你需要一个适配器和一个被适配者, 而我只需要一个类就行.
对象适配器: 我对适配器添加的任何行为对被适配者和它的子类都起作用.
...
又一个适配器的例子 (Java)
老版本的java有个接口叫做Enumeration:
后来又出现了一个Iterator接口:
现在我想把Enumeration适配给Iterator:
这个应该很简单, 可以这样设计:
只有一个问题, Enumeration不支持remove动作, 也就是说适配器也无法让remove变成可能, 所以只能这样做: 抛出一个不支持该操作的异常(C#: NotSupportedException), 这也就是适配器也无法做到完美的地方.
看一下这个java适配器的实现:
装饰模式 vs 适配器模式
你可能发现了, 这两个模式有一些相似, 那么看看它们之间的对话:
装饰模式: 我的工作全都是关于职责, 使用我的时候, 肯定会涉及到在设计里添加新的职责或行为.
适配器模式: 我主要是用来转化接口.
装饰模式: 当我装饰一个大号接口的时候, 真需要写很多代码.
适配器模式: 想把多个类整合然后提供给客户所需的接口, 这也是很麻烦的工作. 但是熟话说: "解耦的客户都是幸福的客户..."
装饰模式: 用我的时候, 我也不知道已经套上多少了装饰器了.
适配器模式: 适配器干活的时候, 客户也不知道我们的存在. 但是我们允许客户在不修改现有代码的情况下使用新的库, 靠我们来转化就行.
装饰模式: 我们只允许为类添加新的行为, 而无需修改现有代码.
适配器模式: 所以说, 我们总是转化我们所包裹的接口.
装饰模式: 我们则是扩展我们包装的对象, 为其添加行为或职责.
从这段对话可以看出, 装饰模式和适配器模式的根本区别就是它们的意图不同.
另一种情况
现在我们可以知道, 适配器模式会把类的接口转化成客户所需要的样子.
但是还有另外一种情况也需要转化接口, 但却处于不同的目的: 简化接口. 这就需要使用外观模式(Facade Pattern).
外观模式会隐藏一个或多个类的复杂性, 并提供一个整洁干净的外观(供外界使用).
现在在总结一下这三种模式的特点:
装饰者模式: 不修改接口, 但是添加职责.
适配器模式: 把一个接口转化成另外一个.
外观模式: 把接口变得简单.
一个需求 -- 家庭影院
这个家庭影院有DVD播放器, 投影仪, 屏幕, 环绕立体音响, 还有个爆米花机:
你可能花了几周的时间去连线, 组装.....现在你想看一个电影, 步骤如下:
- 打开爆米花机
- 开始制作爆米花
- 把灯光调暗
- 把屏幕放下来
- 把投影仪打开
- 把投影仪的输入媒介设为DVD
- 把投影仪调整为宽屏模式
- 打开功放
- 把功放的输入媒介设为DVD
- 把功放设置为环绕立体声
- 把功放的音量调到中档
- 把DVD播放器打开
- 开始播放DVD
具体用程序描述就是这样的:
- 目前一共是这些步骤😂...但是还没完:
- 当电影播放完了, 得把所有的设备关掉, 那么反向重新操作一遍?
- 要是听CD或者收音机是不是也同样的麻烦😫?
- 如果系统升级, 那么你还得学习一遍新的流程吧?
这个需求, 就需要使用外观模式了.
使用外观模式, 你可以通过实现一个外观类把一个复杂的子系统简单化, 因为这个外观类会提供一个更合理的接口.
HomeTheaterFacade这个外观类只暴露了几个简单的方法, 例如看电影watchMovie(). 这个外观类把整个家庭影院看作是它的子系统, 通过调用子系统的相关方法来实现对外的简单方法.
而客户只需要调用这些简单的方法即可. 但是外观类的子系统仍然保留着对外界的可直接访问性, 如果你需要高级功能, 就可以直接调用.
以下几点需要理解:
- 外观类并没有"封装"子系统, 它只不过是对子系统的功能额外提供了一套简单的方法. 外界仍然可以直接访问子系统的方法.
- 外观类也可以添加额外的功能.
- 针对一个子系统可以创建若干个外观类
- 外观模式让客户和子系统解耦.
- 适配器模式是把一个或多个类的接口转化成客户所需要的一个接口, 而外观模式则是提供了一个简单的接口. 它们之间的根本不同是它们的目的, 适配器是要改变接口, 以便可以符合客户要求; 外观模式则是为子系统提供一个简化的接口.
代码:
这里只贴HomeTheaterFacade的代码吧, 其他的东西太简单了 (其他代码在这里: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp):
using System; using FacadePattern.Equipments; namespace FacadePattern.Facades { public class HomeTheaterFacade { private readonly Amplifier _amp; private readonly DvdPlayer _dvd; private readonly Projector _projector; private readonly TheaterLights _lights; private readonly Screen _screen; private readonly PopcornPopper _popper; public HomeTheaterFacade(Amplifier amp, DvdPlayer dvd, Projector projector, TheaterLights lights, Screen screen, PopcornPopper popper) { _amp = amp; _dvd = dvd; _projector = projector; _lights = lights; _screen = screen; _popper = popper; } public void WatchMovie() { Console.WriteLine("Get ready to watch a movie"); _popper.On(); _popper.Pop(); _lights.Dim(5); _screen.Down(); _projector.On(); _projector.WideScreenMode(); _amp.On(); _amp.SetDvd(); _amp.SetSurroundSound(); _amp.SetVolume(5); _dvd.On(); _dvd.Play("Ready Player One"); } public void EndMovie() { Console.WriteLine("Shutting movie theater down..."); _popper.Off(); _lights.On(); _screen.Up(); _projector.Off(); _amp.Off(); _dvd.Stop(); _dvd.Eject(); _dvd.Off(); } } }
测试:
using FacadePattern.Equipments; using FacadePattern.Facades; namespace FacadePattern { class Program { static void Main(string[] args) { var facade = new HomeTheaterFacade(new Amplifier(), new DvdPlayer(), new Projector(), new TheaterLights(), new Screen(), new PopcornPopper()); facade.WatchMovie("Ready Player One"); facade.EndMovie(); } } }
OK.
外观模式定义
外观模式为拥有一套接口的子系统提供了一个为一个简化接口. 外观类定义了一个高级接口, 这个接口可以使子系统用起来更简单.
设计原则 -- 不要知道的太多
不要知道的太多, 只和你最近的朋友谈话.
意思是说, 当你设计系统的时候, 对于任何一个对象, 要小心它所交互的类的个数, 并知道这些类是怎么来的..
一个问题, 下面这段代码耦合了多少个类?
这个原则有一些指导性建议, 在某个对象的任何一个方法里面, 我们只可以调用属于下列对象的方法:
- 对象本身
- 从该方法参数传进来的对象
- 该方法创建或者实例化的对象
- 对象的组件
如果调用一个另一个方法返回的对象会有什么害处?
这样做就是向另一个对象的子部件进行请求, 同时也增加了我们直接接触的对象的个数.
所以不遵循规则的代码是这样的:
遵循规则的应该是这样的:
保持在界内调用方法
看这个例子:
engine是这个类的组件, 可以调用它的start()方法 .
start()方法里key是传进去的, 可以调用key的方法.
doors是我们在方法内创建的对象, 可以调用doors的方法.
可以调用本类的updateDashboardDisplay()方法.
这个原则的缺点就是: 它可能会需要很多的包装类来处理其他组件的方法调用., 这样会提高复杂度也会增加开发时的时间.
再看下面两个写法:
第一种是"错误"的, 第二种是正确的.
外观模式和少知道原则
Client 客户只有一个朋友, HomeTheaterFacade.
HomeTheaterFacade管理子系统里面的组件以便客户可以简单灵活的使用.
如果升级系统, 并不会影响客户
尽量让子系统符合少知道原则, 有必要的话可以引进多层外观.
总结
少知道原则: 只跟最近的朋友讲话.
适配器模式: 转化一个类的接口以便客户可以使用.
外观模式: 为一个子系统的一套接口提供一个统一的接口. 外观定义了一个让子系统更容易使用的高级接口.
C#源码: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp