设计模式——7、适配器模式与外观模式
  • 作者:ZJWave
  • 分类: 设计模式
  • 发表:2019-05-27 16:08
  • 围观:5086
  • 评论:0

在本文,我们将要进行一项任务,其不可能的程度,简直就像是将一个方块放进一个圆洞中。听起来不可能?有了设计模式,就有可能。还记得装饰者模式吗?我们将对象包装其阿里,赋予它们新的职责。而现在则是以不同目的,包装某些对象:让它们的接口看起来不想自己而像是别的东西。为何要这样做?因为这样就可以在设计中,将类的接口转换成想要的接口,以便实现不同的接口。不仅如此,我们还要探讨另一个模式,将对象包装起来以简化其接口。

1.适配器模式介绍

我们周围的适配器

OO适配器是什么,你一定不难理解,因为现实中到处都是。比方说:如果你需要在欧洲国家使用美国制造的笔记本电脑,你可能需要使用一个交流电的适配器……

你知道适配器的作用:它位于美式插头和欧式插座的中间,它的工作是将欧式插座转换成美式插座,好让美式插头可以插件这个插座得到电力。或者也可以这么认为:适配器改变了插座的接口,以符合美式笔记本电脑的需求。

某些交流电适配器相当简单,它们只是改变插座的形状来匹配你的插头,直接把电流传送过去。但是有些适配器内部则是相当复杂,可能会改变电流符合装置的需求。

好了,这是真实世界的适配器,那面向对象适配器又是什么?其实,OO适配器和真实世界的适配器扮演者同样的角色:将一个接口转换成另一个接口,以符合客户的期望。

面向对象适配器

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

你不想改变现有的代码,解决这个问题(而且你也不能改变厂商的代码)。所以该怎么做?这个嘛,你可以写一个类,将新厂商的接口转接成你所期望的接口。

这个适配器工作起来就如同一个中间人,它将客户所发出的请求转换成厂商类能理解的请求。

如果它走起路来像只鸭子,叫起来像一直鸭子,那么他必定可能是一只鸭子包装了鸭子适配器的火鸡……

让我们来看看使用中的适配器。一个鸭子接口:

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

绿头鸭是鸭子的子类。

public class MallardDuck implements Duck {
    @Override
    public void quack() {
        System.out.println("Quack");
    }

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

很简单的实现:只是打印出鸭子在做些什么。

为您介绍最新的“街头顽禽”:

public interface Turkey {
    /**
     * 火鸡不会呱呱叫,只会咯咯(gobble)叫
     */
    void gobble();

    /**
     * 火鸡会飞,虽然飞不远
     */
    void fly();
}
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");
    }
}

这是火鸡的一个具体实现,就和鸭子一样,只是打印出火鸡的动作说明。

现在,假设你缺鸭子对象,想用一些火鸡对象来冒充。显而易见,因为火鸡的接口不同,所以我们不能公然拿来用。

那么,就写个适配器吧:

/**
 * 首先,你需要实现想转换成的类型接口,
 * 也就是你的客户所期望看到的接口。
 */
public class TurkeyAdapter implements Duck {
    Turkey turkey;

    /**
     * 接着,需要取得要适配的对象引用,
     * 这里我们利用构造器取得这个引用。
     * @param turkey
     */
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }

    /**
     * 现在我们需要实现接口中所有的方法。
     * quack()在类之间的转换很简单,只要调用gobble()就可以了
     */
    @Override
    public void quack() {
        turkey.gobble();
    }

    /**
     * 固然两个接口都具备了fly()方法,火鸡的飞行距离很短,
     * 不像鸭子可以长途飞行。要让鸭子的飞行和火鸡的飞行能够对应,
     * 必须连续五次调用火鸡的flay()来完成。
     */
    @Override
    public void fly() {
        for (int i = 0; i < 5; i++) {
            turkey.fly();
        }
    }
}

测试适配器

public class DuckTestDrive {
    public static void main(String[] args) {
        //先创建一只鸭子
        Duck duck = new MallardDuck();

        //和一只火鸡
        WildTurkey turkey = new WildTurkey();
        //然后将火鸡包装进一个适配器中,使它看起来像一直鸭子
        TurkeyAdapter turkeyAdapter = new TurkeyAdapter(turkey);

        //接着测试这只火鸡,让它咯咯叫,让它飞行
        System.out.println("The Turkey says...");
        turkey.gobble();
        turkey.fly();

        //接着,调用testDuck()方法来测试鸭子,
        //这个方法需要传入一个鸭子对象。
        System.out.println("\nThe Duck says...");
        testDuck(duck);

        //重要的测试来了,我们试着传入一个假装是鸭子的火鸡
        System.out.println("\nThe TurkeyAdapter says...");
        testDuck(turkeyAdapter);
    }

    /**
     * 这是我们的testDuck()方法,
     * 取得一只鸭子,并调用它的quack()和fly()方法
     * @param duck
     */
    private static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

2.适配器模式解析

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

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

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

问:一个适配器需要做多少“适配”的工作?如果我需要实现一个很大的目标接口,似乎有“很多”工作要做。

答:的确是如此。实现一个适配器所需要进行的工作,的确和目标接口的大小成正比。如果不用适配器,你就必须改写客户端的代码来调用这个新的接口,将会花许多力气来做大量的调查工作和代码改写工作。相比之下,提供一个适配器类,将所有的改变封装在一个类中,是比较好的做法。

问:一个适配器只能够封装一个类吗?

答:适配器模式的工作是将一个接口转换成另一个。虽然大多数的适配器模式所采取的例子都是让一个适配器包装一个被适配者,但我们都知道这个世界其实复杂多了,所以你可能遇到一些状况,需要让一个适配器包装多个被适配者。

这设计另一个模式,被称为外观模式(Facade Pattern),人们常常将外观模式和适配器模式混为一谈。

问:万一我的系统中新旧并存,旧的部分期望旧的厂商接口,但我们却已经使用新厂商的接口编写了这一部分,这个时候该怎么办?这里使用适配器,那里却使用未包装的接口,这是在是让人感到混乱。如果我只是固守着旧的代码,完全不要管适配器,这样子会不会好一些?

答:不需要如此。可以创建一个双向的适配器,支持两边的接口。想创建一个双向的适配器,就必须实现所涉及的两个接口,这样,这个适配器可以当做旧的接口,或者当做新的接口使用。

3.定义适配器模式

玩够了鸭子、火鸡和交流电适配器,现在让我们进入真实世界,并看看适配器模式的正式定义:

适配器模式将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

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

我们已经看过了这个模式的运行时行为,现在来看它的类图:

这个适配器模式充满着良好的OO设计原则:使用对象组合,以修改的接口包装被适配者:这种做法还有额外的优点,那就是,被适配者的任何子类,都可以搭配着适配器使用。

也请留意,这个模式是如何把客户和接口绑定起来,而不是和实现绑定起来的。我们可以使用数个适配器,每一个都负责转换不同组的后台类。或者,也可以加上新的实现,只要他们遵守目标接口就可以。

3.1 对象和类的适配器

现在,尽管已经定义了这个模式,但其实我们还没有告诉你有关的一切。实际上有“两种”适配器:“对象”适配器和“类”适配器。本文涵盖了对象适配器和类适配器。前面是对象适配器的图。

究竟什么是“类”适配器?为什么我们还没告诉你这种适配器?因为你需要多重继承才能实现它,这在Java中是不可能的。但是当你在使用多重继承语言的时候,还是可能遇到这样的需求。让我们看看多重继承的类图。

看起来很熟悉吗?没错,唯一的差别就在于适配器继承了Target和Adaptee。而对象适配器利用组合的方式将请求传送给被适配者。

4.适配器的实际应用

让我们看看真实世界中一个简单的适配器(至少比鸭子更实际些)……

4.1 将枚举适配到迭代器

旧世界的枚举器

如果你已经是用过Java,可能记得早期的集合(collection)类型(例如:Vector、Stack、Hashtable)都实现了一个名为elements()的方法。该方法会返回一个Enumeration(举)。这个Enumeration接口可以逐一走过此集合内的每个元素,而无需知道它们在集合内是如何被管理的。

新世界的迭代器

Java后续更新集合类时,开始使用了Iterator(迭代器)接口,这个接口和枚举接口很想,都可以让你遍历此集合类型内的每个元素,但不同的是,迭代器还提供了删除元素的能力。

而今天……

我们经常面对遗留代码,这些代码暴露出枚举器接口,但我们又希望在新的代码中只使用迭代器。想解决这个问题,看来我们需要构造一个适配器。

我们先看看这两个接口,找出它们的方法映射关系。换句话说,我们要找出每一个适配器方法在被适配者中的对应方法是什么。

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

处理remove()

好了,我们知道枚举不支持删除,因为枚举是一个“只读”接口。适配器无法实现一个有实际功能的remove(),最多只能抛出一个运行时异常。幸运地,迭代器接口的设计者实现料到了这样的需要,所以将remove()方法定义成会抛出UnsupportedOperationException。

在这个例子中,我们看到了适配器并不完美,客户必须小心潜在的异常,但只要客户够小心,而且适配器的文档能做出说明,这也算是一个合理的解决方案。

编写一个EnumerationIterator适配器

这是一份简单而有效的代码,适合依然会产生枚举的遗留类。

/**
 * 因为我们将枚举适配成迭代器,
 * 适配器需要实现迭代器接口……
 * 适配器必须看起来就像是一个迭代器
 */
public class EnumerationIterator implements Iterator{

    Enumeration e;

    /**
     * 我们利用组合的方式,将枚举结合进入适配器中,
     * 所以用一个实例变量记录枚举
     * @param e
     */
    public EnumerationIterator(Enumeration e) {
        this.e = e;
    }

    /**
     * 迭代器的hasNext()方法其实是委托给枚举的hasMoreElements()方法……
     * @return
     */
    @Override
    public boolean hasNext() {
        return e.hasMoreElements();
    }

    /**
     * 迭代器的next()方法其实是委托各枚举的nextElement()方法
     * @return
     */
    @Override
    public Object next() {
        return e.nextElement();
    }

    /**
     * 很不幸,我们不能支持迭代器的remove()方法,所以必须放弃。
     * 在这里,我们的做法是抛出一个异常。
     */
    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

5.装饰者模式和适配器模式比较

装饰者:我很重要,我的工作全都是和“责任”相关的。你知道的,当事情一涉及到装饰者,就表示有一些新的行为或责任要加入到你的设计中。

适配器:你们这些guy老是把光环放在自己身上,但我们这些适配器却隐身于沟渠中,干着脏活……转换接口。我们的工作或许不是光彩夺目,但我们的客户却很感激我们让他们的生活变得更容易。

装饰者:你说的可能是真的,但可不要认为我们工作不努力。当我们必须装饰一个大型接口时,咳!可是需要很多代码的。

适配器:当你必须将若干类整合在一起来提供你的客户所期望的接口时,不妨扮演适配器的角色看看,这才够棘手。不过,我们有一句格言:“被解耦的客户才是快乐的客户”。

装饰者:很俏皮!别认为我们独揽了所有的光环,有时候我只是一个装饰者,天晓得还有多少其它的装饰者会再将我包装起来。当一个方法调用委托给我时,我根本不知道有多少其他装饰者已经处理过这个调用了,而我也根本不知道我对这个请求所做的付出是否会得到别人的注意。

适配器:哎呀,我们其实同病相怜。只要适配器工作顺利,客户甚至不会意识到外面的存在。根本没有人会感谢适配器所做的一切。但是,关于我们适配器的好处是,我们允许客户使用新的库和子集合,无须改变“任何”代码,由我们负责做转换即可。嘿!这是我们的市场。

装饰者:我们装饰者也可以做到,但是我们可以让“新行为”加入类中,而无需修改现有的代码。我还是认为适配器只是一种装饰者的变体,我的意思是说,适配器就和我们一样,都是用来包装对象的。

适配器:不不不。不是这样。我们“一定会”进行接口的转换,但你们“绝不会”这么做。我宁可认为装饰者其实是一种适配器的变体,只是你们不会改变接口。

装饰者:不!我们的工作是扩展我们包装的对象的行为或责任,并不是“简单传送”就算了。

适配器:你说谁“简单传送”?来呀!转换几个接口让我瞧瞧,看你能持续多久!

装饰者:或许我们应该体会到,我们看起来虽然很类似,但其实我们的意图差异颇大。

适配器:没错,你这么说就对了。

6.外观模式

我们还有另一个模式

你已经知道适配器模式是如何将一个类的接口转换成另一个符合客户期望的接口的。你也知道在Java中要做到这一点,必须将一个不兼容接口的对象包装起来,变成兼容的对象。

我们现在要看一个改变接口的新模式,但是它改变接口的原因是为了简化接口。这个模式被巧妙地螟蛉畏外观模式(Facade-Pattern),之所以这么称呼,是因为它将一个或数个类的复杂的一切都隐藏在背后,只显露出一个干净美好的外观。

甜蜜的家庭影院

在外面进入外观模式的细节之前,让我们看一个流行的热潮:建立自己的家庭影院。

通过一番比较研究,你组装了一套杀手级的系统,内含DVD播放器、投影机、自动屏幕、环绕立体声,甚至还有爆米花机。

看看这些组件的组成:

你花了好几个星期布线、挂上投影机、连接所有的装置并进行微调。现在,你准备开始享受一部电影……

观赏电影(用困难的方式)

挑选一部DVD影片,放松,准备开始看电影。想看电影,必须先执行一些任务。

  1. 打开爆米花机
  2. 开始爆米花
  3. 将灯光调暗
  4. 放下屏幕
  5. 打开投影机
  6. 将投影机的输入切换到DVD
  7. 将投影机设置在宽屏模式
  8. 打开功放
  9. 将功放的输入设置为DVD
  10. 将功放设置为环绕立体声
  11. 将功放音量调到中(5)
  12. 打开DVD播放器
  13. 开始播放DVD

让我们将这些任务写成类和方法的调用,涉及到六个不同的类!

//打开爆米花机,开始爆米花。
popper.on();
popper.pop();

//灯光调暗到10%的亮度……
//把屏幕放下。
lights.dim(10);
screen.down();

//打开投影机,并将它设置在宽屏模式……
projector.on();
projector.setInput(dvd);
projector.wideScreenMode();

//打开功放,设置为DVD,调整成环绕立体声模式,音量调到5
amp.on();
amp.setDvd(dvd);
amp.setSurroundSound();
amp.setVolume(5);

//打开DVD播放机……终于可以看电影了!
dvd.on();
dvd.play(movie);

但还不只是这样……

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

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

灯光、相机、外观

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

让我们看看外观如何运作:

1.现在是为家庭影院系统创建一个外观的时候了,于是我们创建了一个名为HomeTheaterFacade的新类,它对外暴露出几个简单的方法,例如watchMovie()

2.这个外观类将家庭影院的诸多组件视为一个子系统,通过调用这个子系统,来实现watchMovie()方法。

3.现在,你的客户代码可以调用此家庭影院外观所提供的方法,而不必再调用这个子系统的方法。所以,想要看电影,我们只要调用一个方法(也就是watchMovie())就可以了。灯光、DVD播放器、投影机、功放、屏幕、爆米花,一口气全部搞定。

4.外观只是提供你更直接的操作,并未将原来的子系统阻隔起来。如果你需要子系统类的更高层功能,还是可以使用原来的子系统。

外观 vs. 适配器

问:如果外观封装了子系统的类,那么需要低层功能的客户如何接触这些类?

答:外观没有“封装”子系统的类,外观只提供简化的接口。所以客户如果觉得有必要,依然可以直接使用子系统的类。这是外观模式一个很好的特征:提供简化的接口的同时,依然将系统完整的功能暴露出来,以供需要的人使用。

问:外观会新增功能吗,或者它只是将每一个请求转由子系统执行?

答:外观可以附加“聪明的”功能,让使用子系统更方便。比方说,虽然你的家庭影院外观没有实现任何新行为,但是外观却够聪明,知道爆米花机要先开启然后才能开始爆米花(同样,也要先开机才能放电影)。

问:每个子系统只能有一个外观吗?

答:不,你可以为一个子系统创建许多个外观。

问:除了能够提供一个比较简单的接口之外,外观模式还有其他的优点吗?

答:外观模式也允许你将客户实现从任何子系统中解耦。比方说,你得到了大笔加薪,所以想要升级你的家庭影院,采用全新的和以前不一样接口的组件。如果当初你的客户端代码是针对外观而不是针对子系统编写的,现在你就不需要改变客户代码,只需要修改外观代码(而且有可能厂商会提供新版的外观代码)。

问:我可不可以这样说,适配器模式和外观模式之间的差异在于:适配器包装一个类,而外观可以代表许多类?

答:不对!提醒你,适配器模式将一个或多个接口变成客户所期望的一个接口。虽然大多数教科书所采用的例子中适配器只适配一个类,但是你可以适配许多类来提供一个接口让客户端编码。类似地,一个外观也可以只针对一个拥有复杂接口的类提供简化的接口。两种模式的差异,不在于它们“包装”了几个类,而在于它们的意图。适配器模式的意图是,“改变”接口符合客户的期望;而外观模式的意图是,提供子系统的一个简化接口。

外观不只是简化了接口,也将客户从组建的子系统中解耦。

外观和适配器可以包装许多类,但是外观的意图是简化接口,而适配器的意图是将接口转换成不同接口。

6.1 构造家庭影院外观

让我们逐步构造家庭影院外观:第一步是使用组合让外观能够访问子系统中所有的组件。

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, TheaterLights lights, Screen screen, PopcornPopper popper) {
        this.amp = amp;
        this.tuner = tuner;
        this.dvd = dvd;
        this.cd = cd;
        this.projector = projector;
        this.lights = lights;
        this.screen = screen;
        this.popper = popper;
    }

    //其他的方法
}

实现简化的接口

现在该是时候将子系统的组件整合成一个统一的接口了。让我们实现watchMovie()和endMovie()两个方法:

    /**
     * watchMovie()将我们之前手动进行的每项任务依次处理。
     * 请注意,每项任务都是委托子系统中响应的组件处理的。
     * @param movie
     */
    public void watcheMovie(String movie){
        System.out.println("Get ready to watch a movie...");
        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);
    }

    /**
     * 而endMovie()负责关闭一切。
     * 每项任务也都是委托子系统中合适的组件处理的
     */
    public void endMovie(){
        System.out.println("Shutting movie theater down...");
        popper.off();
        lights.on();
        screen.up();
        projector.off();
        amp.off();
        dvd.stop();
        dvd.eject();
        dvd.off();
    }

观赏电影(用轻松的方式)

public class HomeTheaterTestDriver {
    public static void main(String[] args) {
        //在这里实例化组件
        Amplifier amp = new Amplifier();
        Tuner tuner = new Tuner();
        DvdPlayer dvd = new DvdPlayer();
        CdPlayer cd = new CdPlayer();
        Projector projector = new Projector();
        TheaterLights lights = new TheaterLights();
        Screen screen = new Screen();
        PopcornPopper popper = new PopcornPopper();
        //首先,根据子系统所有的组件来实例化外观
        HomeTheaterFacade homeTheater = new HomeTheaterFacade(amp, tuner, dvd, cd, projector, lights, screen, popper);
        //使用简化的接口,先开启电影,然后关闭电影
        homeTheater.watcheMovie("Raiders of the Lost Ark");
        homeTheater.endMovie();
    }
}

6.2 定义外观模式

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

在介绍这个新的原则之前,先来看看外观模式的正式定义:

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

这很容易理解,但是请务必记得模式的意图。这个定义清楚地告诉我们,外观的意图是要提供一个简单的接口,好让一个子系统更易于使用。从这个模式的类图可以感受到这一点:

全部内容就是这样,你又多学会了一个模式!现在来看一个新的OO原则。请注意,这个原则可能有点挑战性!

6.3 “最少知识原则”

最少知识(Least Knowledge)原则告诉我们要减少对象之间的交互,只留下几个“密友”。这个原则通常是这么说的:

最少知识原则:只和你的密友谈话

这到底是什么意思?这是说,当你正在设计一个系统,不管是任何对象,你都要注意它所交互的类由哪些,并注意它和这些类是如何交互的。

这个原则希望我们在设计中,不要让太多的类耦合在一起,免得修改系统中一部分,会影响到其他部分。如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,它需要花许多成本维护,也会因为太复杂而不容易被其他人了解。

究竟要怎样才能避免这样呢?这个原则提供了一些方针:就任何对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:

  • 该对象本身
  • 被当做方法的参数而传递进来的对象
  • 此方法所创建或实例化的任何对象
  • 对象的任何组件(把“组件”想象成是被实例变量所引用的任何对象,换句话说,把这想象成是“有一个”(HAS-A)关系。)

这些方针告诉我们,如果某对象是调用其他的方法的返回结果,不要调用该对象的方法!

这听起来有点严厉,如果调用从另一个调用中返回的对象的方法,会有什么害处呢?如果我们这样做,相当于向另一个对象的子部分发出请求(从而增加我们直接认识的对象数目)。在这种情况下,原则要我们改为要求该对象为我们做出请求,这么一来,我们就不需要认识该对象的组件了(让我们的朋友圈子维持在最小的状态)。比方说:

不采用这个原则:

public float getTemp(){
    Thermometer thermometer = station.getThermometer();
    return thermometer.getTemperature();
}

这里,我们从气象站取得了温度计(thermometer)对象,然后再从温度计对象取得温度。

采用这个原则:

public float getTemp(){
    return station.getTemperature();
}

应用此原则时,我们在气象站中加进一个方法,用来向温度计请求温度。这可以减少我们所依赖的类的数目。

讲方法调用保持在界限内……

这是一个汽车类,展示调用方法的各种做法,同时还能够遵守最少知识原则:

public class Car {
    //这是类的一个组件,我们能够调用它的放啊发生
    Engine engine;

    public Car() {
        //初始化发动机
    }

    
    public void start(Key key){
        // 在这里创建了一个新的对象,它的方法可以被调用。
        Doors doors = new Doors();
        //被当做参数传进来的对象,其方法可以被调用
        boolean authorized = key.turns();
        
        if (authorized) {
            //可以调用对象组件的方法。
            engine.start();
            //可以调用同一个对象内的本地方法(local method)。
            updateDashboardDisplay();
            //可以调用你所创建或实例化的对象的方法。
            doors.lock();
        }
    }

    private void updateDashboardDisplay() {
        //更新显示
    }
}

问:还有另一个原则,叫做德墨忒尔法则(Law of Demeter),它和最少知识原则有什么关系?

答:其实两个名词指的是同一个原则。我们倾向于使用最少知识原则来称呼它是因为以下两个原因:

(1)这个名字更直接

(2)法则(Law)给人的感觉是强制的。事实上,没有任何原则是法律(law)。 

事实上,没有任何原则是法律(law),所有的原则都应该在有帮助的时候才遵守。所有的设计都不免需要折衷(在抽象和速度之间取舍,在空间和时间之间平衡……)虽然原则提供了方针,但在采用原则之前,必须全盘考虑所有因素。

问:采用最少知识原则有什么缺点嘛?

答:是的,虽然这原则觉少了对象之间的依赖,研究显示这会减少软件的维护成本,但是采用这个原则也会导致更多的“包装”类被制造出来,以处理和其他组件的沟通,这可能会导致复杂度和开发时间的增加,并降低运行时的性能。

7.总结

本文加入的模式,让你可以改变接口,并降低客户和系统之间的耦合。

OO原则:

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 类应该对扩展开放,对修改关闭。
  • 依赖抽象,不要依赖具体类。
  • 最少知识原则(只和朋友谈)

OO模式:

  • 适配器模式——将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
  • 外观模式——提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

本文要点:

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

转载请注明原文链接:ZJ-Wave

Top