7.适配器模式和外观模式

1.适配器模式引入

1.1 现实世界的适配器

真实世界的适配器。比如,如果需要在欧洲国家使用美国制造的笔记本电脑,那就可能需要一个交流电的适配器。

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

有些交流适配器相当简单,只是改变插座的形状来匹配你的插头,直接传送电流。而有些复杂的,是改变电流符合装置的需求。

1.2 面向对象适配器

假设已有一个软件系统,希望它能和一个新的厂商类库搭配使用,但是这个新厂商所设计的接口,与旧厂商的接口不同。

如果不想改变现有的代码,解决这个问题的话,应该写一个类,将新厂商接口转结成所期望的接口。

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

1.3 小举例--火鸡转换器

一只走路像鸭子,叫起来像鸭子的就一定是鸭子吗?

也可能是一只包装了鸭子适配器的火鸡。

以这个例子为例,看看鸭子接口和类的一个稍微简化版本:

鸭子实现了Duck接口,具备了呱呱叫和飞行的能力。

public interface Duck {
    
    public void  quack();
    public 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");
	}
}

然后开始建立“街头顽禽”:

火鸡不会呱呱叫,只会咯咯(gobble)的叫,火鸡会飞,虽然飞不远

public interface Turkey {
	public void gobble();
	public 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;

    //去的要 适配的对象的引用,此处使用构造器来进行获取
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }

    //接下来要实现接口中的所有方法,并将不同的方法进行调用与转换
    @Override
    public void quack() {
        turkey.gobble();
    }

    @Override
    public void fly() {
        for (int i=0;i<5;i++){
            //因为火鸡飞的距离很短,所以需要连续调用5次,来与鸭子飞行的距离对应
            turkey.fly();
        }
    }
}

测试适配器

public class DuckTestDrive {

    public static void main(String[] args) {
        MallardDuck mallardDuck=new MallardDuck();

        WildTurkey wildTurkey=new WildTurkey();
        Duck turkeyAdapter=new TurkeyAdapter(wildTurkey);

        System.out.println("The Turkey says:...");
        wildTurkey.gobble();
        wildTurkey.fly();

        System.out.println("The Duck says...");
        mallardDuck.quack();
        mallardDuck.fly();

        System.out.println("The TurkeyAdapter says...");
        testDuck(turkeyAdapter);
    }

    //客户需求方法,要测试一个鸭子叫和飞
    private static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

输出:

2.适配器模式解析

2.1 适配器模式过程配置

 客户使用适配器的过程

1.客户通过目标接口调用适配器的方法对适配器发出请求

2.适配器使用被适配者接口把请求转换成被适配者的一个或多个调用接口

3.客户在接收到调用的结果,但并未察觉这一切是适配器在起转换作用。

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

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

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

回答:适配器模式的工作是将一个接口转换成另一个。虽然大多数的适配器模式所采取的的例子都是让一个适配器包装一个被适配者。当需要一个适配器包装多个被适配者的时候,这涉及到另一个模式,被称为外观模式

问题3:如果系统中 新旧并存,旧的部分期望旧的厂商接口,但是已经使用新厂商的接口编写了一部分,这个时候该怎么办?此处用了适配器,那里却使用未包装的接口,让人觉得混乱。

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

2.2 定义适配器模式

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

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

适配器模式的运行时类图为:

(1)目标(Target)——客户所期待得到的接口,目标可以是具体的或抽象的类,也可以是接口。

(2)源\被适配者(Adaptee)——已经存在的、需要适配的类。源可以是具体的或抽象的类,也可以是接口。

(3)适配器(Adapter)——适配器是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是实类。

2.3 对象和类的适配器

有“两种”适配器:“对象”适配器和“类”适配器。上面2.2中的对象类图是“对象”适配器的图,本节来讲“类”适配器:

需要多重继承才能实现它,但是Java中不支持多继承。但是当使用多重继承语言的时候,还是可能会遇到这样的需求。<注:此处指的是 支持多重继承语言的 语言,而不是java>

类适配器不是使用组合来适配 被适配者, 而是继承 被适配者 和目标类

 位移的差别在于 适配器继承了Target和Adaptee。而对象适配器利用组合的方法将请求传给被适配者。

 对象适配器和类适配器分别使用两种不同的适配方法(分别是组合与继承)。推荐使用 对象适配器

2.4 枚举适配器

旧世界的枚举器:早期的集合(Collection)类型(例如,Vector、Stack、Hashtable)都实现了一个名为elements()的方法。该方法会返回一个Enumeration(枚举)。这个Enumeration接口可以注意走过此集合内的每个元素,而无需知道他们在集合内是如何被管理的。

新世界的迭代器:当Sun推出更新后的集合类时,开始使用Iterator(迭代器)接口,这个接口和枚举接口很像,都可以遍历此集合类型内的每个元素,但不同的是,迭代器还提供了删除元素的能力。

 而今天:我们经常面对遗留代码,这些代码将暴露枚举器接口,但是有希望在新的代码中只使用迭代器,想解决这个问题,我们需要构造一个枚举适配器来将枚举器接口,转换成迭代器接口:

先看看这两个接口,找出他们的方法映射关系,即:要找出每一个适配器方法在适配者中的对应方法是什么

 设计适配器

适配器类应该是:需要一个适配器,实现了目标接口,而此目标接口是由被适配者所组合的。hasNext()和next()方法还是很容易实现的,直接把他们从目标对应到被适配者就可以了。目前类图为:

 处理remove()方法

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

编写一个EnumeratorIterator适配器

 2.5 装饰者与适配器的差异

装饰者模式:装饰者模式的工作都是和“责任”相关的。一旦涉及到装饰者,就表示有一些新的行为或责任要加入到设计中。

适配器模式:将被适配者 转换成 目标对象,将被适配者里面的方法与所需的目标对象在适配器中进行对应。

3.外观模式

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

外观模式(Facade-Pattern),改变接口的原因是为了简化接口,它将一个或数个类的复杂的一切都隐藏在背后,只显露出一个干净美好的外观。

 3.1 需求引入

需求:建立自己的家庭影院。这套系统里包含:内含DVD播放器、投影机、自动屏幕、环绕立体声,还有爆米花机。

这些组件的组成:

 观赏电影(用困难的方式):在看电影之前,必须要执行一些任务:

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

将这些任务写成类和方法的调用

麻烦不止这样:

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

外观模式来解决这团混乱。。。

3.2 外观模式解决问题

首先看看外观如何运作:

1.为家庭影院系统创建一个外观,HomeThearerFacade的心累,它对外暴露出几个简单的方法,比如watchMovie()。

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

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

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

 

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

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

:外观会新增功能吗,或者它只是将每一个请求转由子系统执行?
:外观可以附加“聪明的”功能,让使用子系统更方便。比方说,虽然你的穿庭影院外观没有实现任何新行为,但是外观却够聪明,知道爆米花机要先开启然后才能开始爆米花(同样,也要先开机才能放电影)。

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

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

3.3 构建家庭影院外观

1.第一步是使用组合让外观能够访问子系统中所有的组件:

public class HomeTheaterFacade {
    //组合:将会用到的子系统组件全部做好成员引用
    Amplifier amplifier;
    Tuner tuner;
    CdPlayer cdPlayer;
    Projector projector;
    TheaterLights lights;
    Screen screen;
    PopcornPopper popcornPopper;

    //外观将子系统中每一个组件的引用都传入它的构造器中,外观将他们赋值给相应的实例变量
    public HomeTheaterFacade(Amplifier amplifier,
                             Tuner tuner,
                             CdPlayer cdPlayer,
                             Projector projector,
                             TheaterLights lights,
                             Screen screen,
                             PopcornPopper popcornPopper) {
        this.amplifier = amplifier;
        this.tuner = tuner;
        this.cdPlayer = cdPlayer;
        this.projector = projector;
        this.lights = lights;
        this.screen = screen;
        this.popcornPopper = popcornPopper;
    }

    //其他方法




}

2.实现简化的接口:

现在将子系统的组件整合成一个统一的接口,来实现watchMovie()和endMovie()方法:

public void watchMovie(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.setStreamingPlayer(player);
        amp.setSurroundSound();
        amp.setVolume(5);
        player.on();
        player.play(movie);
    }


    public void endMovie() {
        System.out.println("Shutting movie theater down...");
        popper.off();
        lights.on();
        screen.up();
        projector.off();
        amp.off();
        player.stop();
        player.off();
    }

3. 用轻松的方式来观赏电影

public class HomeTheaterTestDrive {
	public static void main(String[] args) {
		//实例化组件
		Amplifier amp = new Amplifier("Amplifier");
		Tuner tuner = new Tuner("AM/FM Tuner", amp);
		StreamingPlayer player = new StreamingPlayer("Streaming Player", amp);
		CdPlayer cd = new CdPlayer("CD Player", amp);
		Projector projector = new Projector("Projector", player);
		TheaterLights lights = new TheaterLights("Theater Ceiling Lights");
		Screen screen = new Screen("Theater Screen");
		PopcornPopper popper = new PopcornPopper("Popcorn Popper");
 		//根据子系统所有的组件来实例化外观
		HomeTheaterFacade homeTheater = 
				new HomeTheaterFacade(amp, tuner, player, 
						projector, screen, lights, popper);
 		//使用简化的接口,先开启电影然后再关闭电影
		homeTheater.watchMovie("Raiders of the Lost Ark");
		homeTheater.endMovie();
	}
}

输出结果:

3.4 定义外观模式

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

外观模式的意图是要 提供一个简单的接口,好让一个子系统更易于使用

 3.5 “最少知识”原则

最少知识原则:减少对象之间的交互,只留下几个“密友”。  只和你的密友谈话。

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

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

如何不要赢得太多的朋友和影响太多的对象?

该原则提供了一些方针:就任何对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:

  1. 该对象本身
  2. 被当做方法的参数而传递进来的对象
  3. 此方法所创建或实例化的任何对象
  4. 对象的任何组件

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

方针4告诉我们,把“组件”想象成是被实例变量所引用的任何对象,把这想象成是“有一个”(has-a)的关系。

比如说:

 将方法调用保持在界限内

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

4.总结

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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值