设计模式-适配器模式
一、问题引入
说起适配器其实在我们的生活中是非常常见的,比如:如果你到日本出差,你会发现日本的插座电压都是110V的,而我们的手机充电器和笔记本充电器都是220V,所以你到了日本之后就没办法充电了,这时候我们通常会怎么办呢,当然是使用一个升压的变压器将电压升高到220V,这样我们的手机通过一个变压器(适配器)就能使用原本不能使用的插座了。
又比如说,有的国家的插座都是三孔的,而我们的手机大部分都是两孔的,这是你也没办法直接把充电器插到插座上,这时我们可以使用一个适配器,适配器本身是三孔的,它可以直接插到三孔的插头上,适配器本身可以提供一个两孔的插座,然后我们的手机充电器就可以插到适配器上了,这样我们原本只能插到两孔上的插头就能用三孔的插座了。
在我们的面向对象里也存在这个问题,假设一个软件系统,你希望它能和一个新的厂商类库搭配使用,但是这个新厂商所设计出来的接口,不同于旧厂商的接口,就像下图这样:
你不想改变现有的代码,解决这个问题(而且你也不能改变厂商的代码)。所以该怎么做?这个嘛,你可以写一个类,将新厂商的接口转化成你所希望的接口。
这个适配器工作起来就如同一个中间人,它将客户所发出的请求转换成厂商类能理解的请求。
这样的话,就能在不改变现有代码的情况下使用原本不匹配的类库了。
二、适配器模式的相关概念
经过上边的三个例子,我们可以总结出适配器模式的使用过程:
1、客户通过目标接口调用适配器的方法对适配器发出请求。
2、适配器使用被适配者接口把请求转化成被适配者的一个或多个调用接口。
3、客户接收到调用的结果,但并未察觉这一切是适配在起转化作用。
所以适配器模式的正式定义就是:
适配器模式将一个类的接口,转化成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
三、对象适配器
适配器其实是分为对象适配器和类适配器两种,两种的工作原理不太一样。对象适配器是使用组合的方法,在Adapter中会保留一个原对象(Adaptee)的引用,适配器的实现就是讲Target中的方法委派给Adaptee对象来做,用Adaptee中的方法实现Target中的方法。
这种类型的好处就是,Adpater只需要实现Target中的方法就好啦。
现在我们通过一个用火鸡冒充鸭子的例子来看看如何使用适配器模式。
package com.designpattern.adapter.object; public abstract class Duck { /** * 嘎嘎叫 */ public abstract void quack(); public abstract void fly(); }
package com.designpattern.adapter.object; public abstract class Turkey { /** * 火鸡叫 */ public abstract void gobble(); public abstract void fly(); }
package com.designpattern.adapter.object; public class WildTurkey extends Turkey { public void gobble() { System.out.println("Gobble gobble"); } public void fly() { System.out.println("I'm flying a short distance"); } }
package com.designpattern.adapter.object; /** * 用火鸡冒充鸭子 * @author 98583 * */ public class TurkeyAdapter extends Duck { /** * 保留火鸡的引用 */ Turkey turkey; public TurkeyAdapter(Turkey turkey) { this.turkey = turkey; } /** * 利用火鸡的叫声来实现鸭子的叫声 */ public void quack() { turkey.gobble(); } /** * 利用火鸡的飞的方法来实现鸭子的飞的方法 */ public void fly() { for (int i = 0; i < 5; i++) { turkey.fly(); } } }
package com.designpattern.adapter.object; /** * 用火鸡冒充鸭子 * @author 98583 * */ public class Client { public static void main(String[] args) { WildTurkey turkey = new WildTurkey(); Duck turkeyAdapter = new TurkeyAdapter(turkey); System.out.println("The Turkey says..."); turkey.gobble(); turkey.fly(); System.out.println("\nThe TurkeyAdapter says..."); testDuck(turkeyAdapter); } static void testDuck(Duck duck) { duck.quack(); duck.fly(); } }
鸭子和火鸡有相似之处,他们都会飞,虽然飞的不远,他们不太一样的地方就是叫声不太一样,现在我们有一个火鸡的类,有鸭子的抽象类也就是接口。我们的适配器继承自鸭子类并且保留了火鸡的引用,重写鸭子的飞和叫的方法,但是是委托给火鸡的方法来实现的。在客户端中,我们给适配器传递一个火鸡的对象,就可以把它当做鸭子来使用了。
四、类适配器
与对象适配器不同的是,类适配器是通过类的继承来实现的。Adpater直接继承了Target和Adaptee中的所有方法,并进行改写,从而实现了Target中的方法。
这种方式的缺点就是必须实现Target和Adaptee中的方法,由于Java不支持多继承,所以通常将Target设计成接口,Adapter继承自Adaptee然后实现Target接口。
我们使用类适配器的方式来实现一下上边的用火鸡来冒充鸭子。
package com.designpattern.adapter.classmethod;
/**
* 由于Java不支持多继承,所以通常将Target声明为接口
* @author 98583
*
*/
public interface Duck {
/**
* 嘎嘎叫
*/
public void quack();
public void duckFly();
}
package com.designpattern.adapter.classmethod;
/**
* 目前已有的火鸡类的抽象类
* @author 98583
*
*/
public abstract class Turkey {
/**
* 火鸡叫
*/
public abstract void gobble();
public abstract void turkeyFly();
}
package com.designpattern.adapter.classmethod;
/**
* 用火鸡冒充鸭子,不再保留火鸡类的引用,需要实现鸭子类和火鸡类的方法
* @author 98583
*
*/
public class TurkeyAdapter extends Turkey implements Duck {
/**
* 利用火鸡的叫声来实现鸭子的叫声
*/
public void quack() {
gobble();
}
/**
* 利用火鸡的飞的方法来实现鸭子的飞的方法
*/
public void turkeyFly() {
for (int i = 0; i < 5; i++) {
System.out.println("I'm flying a short distance");
}
}
/**
* 使用火鸡类的方法来实现鸭子类的方法
*/
public void duckFly() {
turkeyFly();
}
/**
* 火鸡的叫声
*/
public void gobble() {
System.out.println("Gobble gobble");
}
}
package com.designpattern.adapter.classmethod;
/**
* 用火鸡冒充鸭子
* @author 98583
*
*/
public class Client {
public static void main(String[] args) {
Duck turkeyAdapter = new TurkeyAdapter();
System.out.println("\nThe TurkeyAdapter says...");
testDuck(turkeyAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.duckFly();
}
}
其实两种方法的效果是一样的,只是用的方法不一样。Java不支持多继承,所以将Duck声明为接口,Adapter继承自火鸡类并且实现了Duck的方法,但是实现Duck的方法不再是委派给火鸡类的对象,而是直接调用火鸡类的方法,因为在Adapter中实现了火鸡类的方法,所以可以直接调用。
五、缺省适配器
鲁达剃度的故事就很好的说明了缺省适配器的作用。一般的和尚都是吃斋,念经,打坐,撞钟和习武,但是鲁达只是喝酒喝习武,所以‘鲁达不能剃度(不能当做和尚使用),要想让鲁达可以当做和尚使用就要让他实现和尚的所有方法,但是这样做时候鲁达就不是鲁达了。我们可以找一个中间者,比如鲁达是天星的一位,我们可以让天星实现和尚所有的方法,再让鲁达继承自天星。代码如下:
这是定义的和尚接口,和尚都应该做以下的事。
package com.designpattern.adapter.defaultmethod; public interface Monk { public void chizha(); public void nianjing(); public void dazuo(); public void zhuangzhong(); public void xiwu(); }
这是天星类,为每个方法提供一个空实现,其他继承自该类的子类可以重写父类的方法。
package com.designpattern.adapter.defaultmethod; public abstract class Star implements Monk{ public void chizha(){} public void nianjing(){} public void dazuo(){} public void zhuangzhong(){} public void xiwu(){} }
鲁达继承自天星,并且添加了喝酒的方法。
package com.designpattern.adapter.defaultmethod; public class Luda extends Star{ public void xiwu(){ System.out.println("鲁达习武"); } public void hejiu(){ System.out.println("鲁达喝酒"); } }
我们看到通过天星类(缺省适配器),鲁达不需要再实现自己不需要的方法了。
六、优缺点
优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
类适配器模式还具有如下优点:
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器模式还具有如下优点:
一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
缺点:
类适配器模式的缺点如下:
对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
对象适配器模式的缺点如下:
与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
七、适用环境
1、系统需要使用现有的类,而这些类的接口不符合系统的需要。
2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
感谢您的阅读,您的支持是我写博客动力。