适配器情势和外观情势,完毕适配器格局

本文的概念内容来自深入浅出设计模式一书

《Head First设计模式》读书笔记

现实世界中的适配器(模式)

澳门新葡新京 1

我带着一个国标插头的笔记本电脑, 来到欧洲,
想插入到欧洲标准的墙壁插座里面, 就需要用中间这个电源适配器.

适配器模式和外观者模式

面向对象的适配器

澳门新葡新京 2

你有个老系统, 现在来了个新供应商的类, 但是它们的接口不同,
如何使用这个新供应商的类呢?

首先, 我们不想修改现有代码, 你也不能修改供应商的代码.
那么你只能写一个可以适配新供应商接口的类了:

澳门新葡新京 3

这里, 中间的适配器实现了你的类所期待的接口,
并且可以和供应商的接口交互以便处理你的请求.

适配器可以看作是中间人, 它从客户接收请求,
并把它们转化为供应商可以理解的请求:

澳门新葡新京 4

所有的新代码都写在适配器里面了.

一,写在最前面

鸭子的例子

有这么一句话不知道您听过没有: 如果它路像个鸭子, 叫起来也像个鸭子,
那它就是个鸭子. (例如: 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();
        }
    }
}

澳门新葡新京 5

这个例子很简单, 就不解释了.

1,为什么要将这两个设计模式写在一起?

不仅这两个设计模式都比较简单,而且我们可以通过这两个设计模式更好的理解OO思想。

理解适配器模式

澳门新葡新京 6

Client 客户实现了某种目标接口, 它发送请求到适配器, 适配器也实现了该接口,
并且适配器保留着被适配者的实例,
适配器把请求转化为可以在被适配者身上执行的一个或者多个动作.

客户并不知道有适配器做着翻译的工作.

其他:

适配器可以适配两个或者多个被适配者.

适配器也可以是双向的, 只需要实现双方相关的接口即可.

2,在本章节的最后会引入了最少知识设计原则。

适配器模式定义

适配器模式把一个类的接口转化成客户所期待的另一个接口.
适配器让原本因接口不兼容而无法一起工作的类成功的工作在了一起.

类图:

澳门新葡新京 7

其中 Client只知道目标接口, 适配器实现了这个目标接口,
适配器是通过组合的方式与被适配者结合到了一起,
所有的请求都被委托给了被适配者.

二,适配器模式

对象适配器和类适配器

一共有两类适配器: 对象适配器类适配器.

之前的例子都是对象适配器.

为什么没有提到类适配器? 

因为类适配器需要多继承, 这一点在Java和C#里面都是不可以的.
但是其他语言也许可以例如C++?

它的类图是这样的:

澳门新葡新京 8

这个图看着也很眼熟, 这两种适配器唯一的区别就是:
类适配器同时继承于目标和被适配者,
而对象适配器使用的是组合的方式来把请求传递给被适配者.

1,生活中的适配器

如果你自己接过水管或者自己接过洗衣机的水管头,你肯定有过类似的体验,无论你怎么接,它都会漏水,然后去店里一问,就知道有水管转换接头这么个东西。他可以让两个粗细不同对接不上的水管无缝的对接在一起。

通过鸭子的例子来认识两种适配器的角色

类适配器:

澳门新葡新京 9

类适配器里面, 客户认为它在和鸭子谈话, 目标就是鸭子类,
客户调用鸭子上面的方法. 火鸡没有和鸭子一样的方法,
但是适配器可以接收鸭子的方法调用并把该动作转化为调用火鸡上面的方法.
适配器让火鸡可以响应一个针对于鸭子的请求,
实现方法就是同时继承于鸭子类和火鸡类

对象适配器:

澳门新葡新京 10

对象适配器里, 客户仍然认为它在和鸭子说话, 目标还是鸭子类,
客户调用鸭子类的方法, 适配器实现了鸭子类的接口,
但是当它接收到方法调用的时候, 它把该动作转化委托给了火鸡.
火鸡并没有实现和鸭子一样的接口, 多亏了适配器,
火鸡(被适配者)将会接收到客户针对鸭子接口的方法调用.

两种适配器比较:

对象适配器: 使用组合的方式, 不仅能是配一个被适配者的类,
还可以适配它的任何一个子类.

类适配器: 只能适配一个特定的类, 但是它不需要重新实现整个被适配者的功能.
而且它还可以重写被适配者的行为.

澳门新葡新京,对象适配器: 我使用的是组合而不是继承,
我通过多写几行代码把事情委托给了被适配者. 这样很灵活.

类适配器: 你需要一个适配器和一个被适配者, 而我只需要一个类就行.

对象适配器: 我对适配器添加的任何行为对被适配者和它的子类都起作用.

2,面向对象的适配器

两套系统的使用接口不一致,但是你想将两套系统给对接起来,你就必须设计一个适配器将两个接口对接起来,这样你就可以在一个系统中调用另外一个系统的实现方案。
  适配器模式定义:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

又一个适配器的例子 (Java)

 老版本的java有个接口叫做Enumeration:

澳门新葡新京 11

后来又出现了一个Iterator接口:

澳门新葡新京 12

现在我想把Enumeration适配给Iterator:

澳门新葡新京 13

这个应该很简单, 可以这样设计:

澳门新葡新京 14

只有一个问题, Enumeration不支持remove动作,
也就是说适配器也无法让remove变成可能, 所以只能这样做:
抛出一个不支持该操作的异常(C#: NotSupportedException),
这也就是适配器也无法做到完美的地方
.

看一下这个java适配器的实现:

澳门新葡新京 15

三,适配器模式(引入书中的demo,而并不只是简单设计一个水管的接口转换,后面会介绍到)

装饰模式 vs 适配器模式

你可能发现了, 这两个模式有一些相似, 那么看看它们之间的对话:

装饰模式: 我的工作全都是关于职责, 使用我的时候,
肯定会涉及到在设计里添加新的职责或行为.

适配器模式: 我主要是用来转化接口.

装饰模式: 当我装饰一个大号接口的时候, 真需要写很多代码.

适配器模式: 想把多个类整合然后提供给客户所需的接口, 这也是很麻烦的工作.
但是熟话说: “解耦的客户都是幸福的客户…”

装饰模式: 用我的时候, 我也不知道已经套上多少了装饰器了.

适配器模式: 适配器干活的时候, 客户也不知道我们的存在.
但是我们允许客户在不修改现有代码的情况下使用新的库, 靠我们来转化就行.

装饰模式: 我们只允许为类添加新的行为, 而无需修改现有代码.

适配器模式: 所以说, 我们总是转化我们所包裹的接口.

装饰模式: 我们则是扩展我们包装的对象, 为其添加行为或职责.

从这段对话可以看出,
装饰模式和适配器模式的根本区别就是它们的意图不同.

 

1,将火鸡伪装成一个Duck这是我们的目的,看到这句话可能会想到装饰者模式,注意比较,先看适配器模式代码,一个Duck接口和一个Turkey接口。

public interface Duck {
public void quak();
public void fly();
}

public interface Turkey {
public void gobble();
public void fly();
}

另一种情况

现在我们可以知道, 适配器模式会把类的接口转化成客户所需要的样子.

但是还有另外一种情况也需要转化接口, 但却处于不同的目的: 简化接口.
这就需要使用外观模式(Facade Pattern). 

外观模式会隐藏一个或多个类的复杂性,
并提供一个整洁干净的外观(供外界使用).

现在在总结一下这三种模式的特点:

装饰者模式: 不修改接口, 但是添加职责.

适配器模式: 把一个接口转化成另外一个.

外观模式: 把接口变得简单.

 

2,先看两个Duck和Turkey实现类

public class MallarDuck implements Duck{

@Override
public void quak() {
    System.out.println("Quack~");
}

@Override
public void fly() {
    System.out.println("I'm flying~");
}
}

public class WildTurkey implements Turkey{

@Override
public void gobble() {
    System.out.println("Gobble gobble");
}

@Override
public void fly() {
    System.out.println("I'm flying a short distance");
}
}

一个需求 — 家庭影院

这个家庭影院有DVD播放器, 投影仪, 屏幕, 环绕立体音响, 还有个爆米花机:

澳门新葡新京 16

你可能花了几周的时间去连线, 组装…..现在你想看一个电影, 步骤如下:

  1. 打开爆米花机
  2. 开始制作爆米花
  3. 把灯光调暗
  4. 把屏幕放下来
  5. 把投影仪打开
  6. 把投影仪的输入媒介设为DVD
  7. 把投影仪调整为宽屏模式
  8. 打开功放
  9. 把功放的输入媒介设为DVD
  10. 把功放设置为环绕立体声
  11. 把功放的音量调到中档
  12. 把DVD播放器打开
  13. 开始播放DVD

具体用程序描述就是这样的:

澳门新葡新京 17

  • 目前一共是这些步骤😂…但是还没完:
  • 当电影播放完了, 得把所有的设备关掉, 那么反向重新操作一遍?
  • 要是听CD或者收音机是不是也同样的麻烦😫?
  • 如果系统升级, 那么你还得学习一遍新的流程吧?

这个需求, 就需要使用外观模式了.

使用外观模式, 你可以通过实现一个外观类把一个复杂的子系统简单化,
因为这个外观类会提供一个更合理的接口**.

澳门新葡新京 18

HomeTheaterFacade这个外观类只暴露了几个简单的方法,
例如看电影watchMovie(). 这个外观类把整个家庭影院看作是它的子系统,
通过调用子系统的相关方法来实现对外的简单方法.

而客户只需要调用这些简单的方法即可.
但是外观类的子系统仍然保留着对外界的可直接访问性, 如果你需要高级功能,
就可以直接调用.

以下几点需要理解:

  • 外观类并没有”封装”子系统,
    它只不过是对子系统的功能额外提供了一套简单的方法.
    外界仍然可以直接访问子系统的方法.
  • 外观类也可以添加额外的功能.
  • 针对一个子系统可以创建若干个外观类
  • 外观模式让客户和子系统解耦.
  • 适配器模式是把一个或多个类的接口转化成客户所需要的一个接口,
    而外观模式则是提供了一个简单的接口.
    它们之间的根本不同是它们的目的, 适配器是要改变接口,
    以便可以符合客户要求; 外观模式则是为子系统提供一个简化的接口.

代码:

这里只贴HomeTheaterFacade的代码吧, 其他的东西太简单了 (其他代码在这里:
):

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();
        }
    }
}

澳门新葡新京 19

OK.

3,到这里,分别有一个Duck具体类和一个WildTurkey具体类,那么我就需要一个adapter来将wildTurkey转换成Duck接口。

public class TurkeyAdapter implements Duck{
private Turkey turkey;

public TurkeyAdapter(Turkey turkey) {
    this.turkey = turkey;
}

@Override
public void quak() {
    turkey.gobble();
}

//火鸡没有鸭子飞得远,那么我们就可以飞五次来假装成和鸭子一样的距离。
@Override
public void fly() {
    for(int i = 0; i < 5 ; i++){
        turkey.fly();
    }
}
}

外观模式定义

外观模式为拥有一套接口的子系统提供了一个为一个简化接口.
外观类定义了一个高级接口, 这个接口可以使子系统用起来更简单.

澳门新葡新京 20

4,然后我们看看测试代码:

public class Test {
public static void main(String[] args) {
    MallarDuck duck = new MallarDuck();
    duck.quak();
    duck.fly();
    System.out.println("--------------");
    WildTurkey turkey = new WildTurkey();
    TurkeyAdapter adapter = new TurkeyAdapter(turkey);
    adapter.quak();
    adapter.fly();
}
}

设计原则 — 不要知道的太多

不要知道的太多,
只和你最近的朋友谈话
.

意思是说, 当你设计系统的时候, 对于任何一个对象,
要小心它所交互的类的个数, 并知道这些类是怎么来的..

一个问题, 下面这段代码耦合了多少个类?

澳门新葡新京 21

这个原则有一些指导性建议, 在某个对象的任何一个方法里面,
我们只可以调用属于下列对象的方法:

  • 对象本身
  • 从该方法参数传进来的对象
  • 该方法创建或者实例化的对象
  • 对象的组件

如果调用一个另一个方法返回的对象会有什么害处?

这样做就是向另一个对象的子部件进行请求,
同时也增加了我们直接接触的对象的个数.

所以不遵循规则的代码是这样的:

澳门新葡新京 22

遵循规则的应该是这样的:
澳门新葡新京 23

5,测试结果

Quack~
I'm flying~
--------------
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

保持在界内调用方法

看这个例子:

澳门新葡新京 24

engine是这个类的组件, 可以调用它的start()方法 .

start()方法里key是传进去的, 可以调用key的方法.

doors是我们在方法内创建的对象, 可以调用doors的方法.

可以调用本类的updateDashboardDisplay()方法.

这个原则的缺点就是:
它可能会需要很多的包装类来处理其他组件的方法调用.,
这样会提高复杂度也会增加开发时的时间.

再看下面两个写法:

澳门新葡新京 25

第一种是”错误”的, 第二种是正确的.

四,适配器模式和装饰者模式

我们乍一看,怎么像把火鸡给包装成鸭子了,这不就是装饰者模式的思想吗?
  通过仔细分析,我们可以发现,装饰者模式主要就是组合和委托,那么他们的类型是一定不能变得,一定得一致,中途也没有类型的转换。但是适配器模式一定要进行接口的转换。这就是他们最大的区别。

外观模式和少知道原则

澳门新葡新京 26

Client 客户只有一个朋友, HomeTheaterFacade.

HomeTheaterFacade管理子系统里面的组件以便客户可以简单灵活的使用.

如果升级系统, 并不会影响客户

尽量让子系统符合少知道原则, 有必要的话可以引进多层外观.

五,外观者模式

总结

少知道原则: 只跟最近的朋友讲话.

适配器模式: 转化一个类的接口以便客户可以使用.

外观模式: 为一个子系统的一套接口提供一个统一的接口.
外观定义了一个让子系统更容易使用的高级接口.

 

C#源码: 

1,定义

提供一个简易的接口,来访问子系统中的一群接口。外观定义了一个高层接口,让子系统容易使用。

2,demo背景介绍(例子本身不重要,关键理解思想)

比如我们看家庭影院。我们需要打开电视,打开DVD,打开功放,设置输入模式为DVD,设置声道为双声道,设置房间灯光亮度等等。这么多设置,我们希望将这些动作简化为一次。

3,实现思路

创建一个类,实例化所有需要设置的对象,然后通过一个命令方法,里面包括了所有的对象的方法,但是只有一个命令入口。只需要调用这一个入口就可以实现所有的设置。

六,外观者模式demo代码

1,需要设置的对象类:

public class Amplifier {
public void on(){
    System.out.println("The amplifier is on~");
}
}

public class Light {
public void adjust(){
    System.out.println("Adjuast the light~");
}
}

public class TV {
public void on(){
    System.out.println("The TV is on~");
}
}

2,设置一个类来把这些对象命令包装组合起来

public class HomeTheater {
Amplifier amp;
Light light;
TV tv;
public HomeTheater(Amplifier amp,Light light,TV tv) {
    this.amp = amp;
    this.light = light;
    this.tv = tv;
}
public void WatchMove(){
    amp.on();
    light.adjust();
    tv.on();
}
}

3,测试类

public class Test {
public static void main(String[] args) {
    Amplifier amp = new Amplifier();
    Light light = new Light();
    TV tv = new TV();
    HomeTheater homeTheater = new HomeTheater(amp, light, tv);
    homeTheater.WatchMove();
}
}

4,输出结果

The amplifier is on~
Adjuast the light~
The TV is on~

七,最少知识原则

不要让太多的类耦合在一起,这样会导致一个问题:修改系统的一个部分可能会导致修改的部分会影响太多。这就是一个维护与开发的问题,从长远看,开发的时候多花点时间建立一个易于维护的系统是更优的选择。
  不采用下面这种方式,耦合太多的类:

public float getTemp(){
    return station.getThermometer().getTemperture();
}

一般遵循的原则只应该调用属于以下范围的方法:

  • 该对象本身
  • 被当做方法的参数而传递进来的对象
  • 此方法创建或者实例化的任何对象
  • 对象的任何组件

如果觉得总结不错,麻烦大家点击一下喜欢,便是对我最大的支持和肯定!

相关文章