在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,讲中文的人同讲英文的人对话时需要一个翻译,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。
在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题
。
适配器模式(Adapter)
:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。适配器模式分为类结构型模式
和对象结构型模式
两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
通过内存卡案例实现,现在有两种内存卡,一种是SD卡,一种是FT卡,电脑只能读取SD卡,所以需要将FT卡适配为SD卡
// SD卡接口
public interface ISDCard {
// 读数据
String readSD();
// 写数据
void writeSD(String msg);
}
// SD卡实现类
public class SDCard implements ISDCard{
@Override
public String readSD() {
String msg = "read sd hello world";
return msg;
}
@Override
public void writeSD(String msg) {
System.out.println("write sd " + msg);
}
}
// FT卡接口
public interface IFTCard {
String readFT();
void writeFT(String msg);
}
// FT卡实现类
public class FTCard implements IFTCard{
@Override
public String readFT() {
String msg = "read ft hello world";
return msg;
}
@Override
public void writeFT(String msg) {
System.out.println("write ft " + msg);
}
}
// 电脑类,只支持读写SD卡中内容
public class Compter {
public String readSD(ISDCard sdCard){
if(sdCard == null) {
throw new NullPointerException("sd card is not null");
}
return sdCard.readSD();
}
public void writeSD(ISDCard sdCard) {
sdCard.writeSD("hello world");
}
}
// 适配器类,继承FT卡实现类,实现SD卡接口,重写SD卡方法
public class FTAdapterSD extends FTCard implements ISDCard{
@Override
public String readSD() {
System.out.println("适配FT卡读数据");
return readFT();
}
@Override
public void writeSD(String msg) {
System.out.println("适配FT卡写数据");
writeFT(msg);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Compter compter = new Compter();
SDCard sdCard = new SDCard();
System.out.println("插入SD卡");
System.out.println(compter.readSD(sdCard));
compter.writeSD(sdCard);
System.out.println("插入FT卡转换器");
FTAdapterSD ftAdapterSD = new FTAdapterSD();
System.out.println(compter.readSD(ftAdapterSD));
compter.writeSD(ftAdapterSD);
}
}
违反了合成复用原则,如果目标对象不是接口,而是一个抽象类就无法实现,因为Java不支持多继承,所以多数情况下还是使用对象适配。
// 适配器类,
// 将适配者当做对象引用进来即可。不再继承FT卡实现类,而是当做对象依赖进来
public class FTAdapterSD implements ISDCard {
private FTCard ftCard;
public FTAdapterSD(FTCard ftCard) {
this.ftCard = ftCard;
}
@Override
public String readSD() {
System.out.println("适配FT卡读数据");
return ftCard.readFT();
}
@Override
public void writeSD(String msg) {
System.out.println("适配FT卡写数据");
ftCard.writeFT(msg);
}
}
还有一种适配器模式是接口适配。当不希望实现一个接口中所有方法时,可以创建一个抽象类Adapter,实现所有方法,而此时我们只需要继承该抽象类即可
优点
缺点
现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状
分,又可按颜色
分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女、支持不同平台和不同文件格式的媒体播放器等。如果用桥接模式就能很好地解决这些问题。
桥接模式的定义如下:将抽象与实现分离
,使它们可以独立变化。它是用组合关系代替继承关系
来实现,从而降低了抽象和实现这两个可变维度的耦合度。
开发一个跨平台的播放器,可以在不同的操作系统(Windows,Mac,Linux)上播放不同格式的视频(RMVB、AVI、WMV等)该播放器包含两个维度,适合使用桥接模式
桥接模式将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
// 实现化角色,解码视频进行播放
public interface IVedioFile {
// 解码功能
void decode(String fileName);
}
// 具体实现化角色,可以对avi格式视频进行解码
public class AviVedioFile implements IVedioFile{
@Override
public void decode(String fileName) {
System.out.println("avi视频文件:" + fileName);
}
}
// 可以对Rmvb格式视频进行解码
public class RmvbVedioFile implements IVedioFile{
@Override
public void decode(String fileName) {
System.out.println("Rmvb格式播放:" + fileName);
}
}
// 抽象化角色
public abstract class AbstractOpratingSystem {
protected IVedioFile vedioFile;
public AbstractOpratingSystem(IVedioFile vedioFile) {
this.vedioFile = vedioFile;
}
abstract void play(String fileName);
}
// 抽象化扩展类,window上的视频播放
public class WindowOpratingSystem extends AbstractOpratingSystem{
public WindowOpratingSystem(IVedioFile vedioFile) {
super(vedioFile);
}
@Override
void play(String fileName) {
vedioFile.decode(fileName);
}
}
// Linux上的视频播放
public class LinuxOpratingSystem extends AbstractOpratingSystem{
public LinuxOpratingSystem(IVedioFile vedioFile) {
super(vedioFile);
}
@Override
void play(String fileName) {
vedioFile.decode(fileName);
}
}
public class Client {
public static void main(String[] args) {
WindowOpratingSystem windowOpratingSystem = new WindowOpratingSystem(new AviVedioFile());
windowOpratingSystem.play("流浪地球");
}
}
通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。
优点
缺点
由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。
在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。代理模式分为静态代理
和 动态代理
我们以卖火车票案例进行介绍
// 抽象主题,也就是售卖火车票的方法
public interface SellTickets {
void sell();
}
// 真实主题类,真正的火车票是火车站售出的
public class TrainStation implements SellTickets{
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
// 代理类(代售点)
public class ProxyPoint implements SellTickets{
protected TrainStation trainStation = new TrainStation();
@Override
public void sell() {
System.out.println("代理点收取一些费用");
trainStation.sell();
}
}
public class Client {
public static void main(String[] args) {
// 创建代理对象
ProxyPoint proxyPoint = new ProxyPoint();
// 调用代理对象售票方法
proxyPoint.sell();
}
}
JDK提供了一个动态代理类Proxy
,Proxy并不是代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance)来获取代理对象
// 定义售票接口
public interface SellTickets {
void sell();
}
// 火车站实现售票接口,获取售票方法
public class TrainStation implements SellTickets{
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
// 代理工厂类,生产代理对象
public class ProxyFactory {
// 引用目标对象,也就是火车站对象
private TrainStation trainStation = new TrainStation();
// 创建代理对象
public SellTickets getProxy() {
// 返回代理对象
/**
* ClassLoader loader,
* Class>[] interfaces,
* InvocationHandler h
*/
SellTickets proxyObject = (SellTickets)Proxy.newProxyInstance(
trainStation.getClass().getClassLoader(),
trainStation.getClass().getInterfaces(),
new InvocationHandler() {
/**
*
* @param proxy:代理对象,和 proxyObject 是同一个
* @param method:对接口中的方法封装的Method函数
* @param args:被调用的方法的实际参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代售点出售火车票,收取一些服务费用");
// 执行目标方法
Object obj = method.invoke(trainStation, args);
return obj;
}
}
);
return proxyObject;
}
}
public class Client {
public static void main(String[] args) {
// 创建代理工厂对象
ProxyFactory proxyFactory = new ProxyFactory();
// 获取代理对象
SellTickets proxy = proxyFactory.getProxy();
proxy.sell();
}
}
ProxyFactory并不是代理对象,JDK动态代理的代理对象是在运行中创建在内存中,类名为$Proxy0
执行流程:
如果没有定义SellTickets接口,之定义了火车站类,那么JDK动态代理就无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。
CGLIB是一个功能强大,高性能的代码生成包,它为没有实现接口的类提供代理,为JDK的动态代理提供很好的补充
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>3.2.9version>
dependency>
public class TrainStation {
public void sell() {
System.out.println("火车站卖票");
}
}
// 代理
public class ProxyFactory implements MethodInterceptor {
private TrainStation trainStation = new TrainStation();
public TrainStation getProxyObject() {
// 创建Enhancer对象,类似于JDK代理中的Proxy
Enhancer enhancer = new Enhancer();
// 设置父类字节码对象
enhancer.setSuperclass(TrainStation.class);
// 设置回调函数
enhancer.setCallback(this);
// 创建代理对象
TrainStation proxyObject = (TrainStation) enhancer.create();
return proxyObject;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CGLIB代替售票");
// 调用方法,并接收返回值
Object obj = method.invoke(trainStation, objects);
return obj;
}
}
public class Client {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory();
TrainStation proxyObject = proxyFactory.getProxyObject();
proxyObject.sell();
}
}
CGLIB底层使用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一要注意的是,CGLIB不能对声明为final的类或方法进行代理,因为CGLIB原理是动态生成被代理类的子类。JDK1.8之后JDK代理比CGLIB代理效率要高。所以如果有接口使用JDK代理,没有接口使用CGLIB代理
动态代理与静态代理相比较,最大的好处在于接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理一样每一个方法都要重写。
如果接口新增一个方法,如果是静态代理,代理类和实现类都要改动。而动态代理就不会有这种问题。
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
优点
缺点
当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。
比如Spring中的AOP就是使用的动态代理,可以实现事务、日志、性能检测等功能
上班族大多都有睡懒觉的习惯,每天早上上班时间都很紧张,于是很多人为了多睡一会,就会用方便的方式解决早餐问题。有些人早餐可能会吃煎饼,煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么“加码”,都还是一个煎饼。在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等,都是装饰器模式。
在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰器模式来实现。
装饰器(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰器模式的目标。
我们通过给饮料添加各种佐料,比如添加牛奶,加冰等来对元饮品进行装饰
// 抽象的饮料类
public abstract class Drick {
// 描述
public String desc;
// 价格
private float price;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
// 抽象的价格计算方式
abstract float cost();
}
// 咖啡类 抽象构件
public class Coffee extends Drick{
// 单品咖啡成本计算
@Override
float cost() {
return super.getPrice();
}
}
// 装饰者类,聚合被装饰者,抽象装饰角色
public class Decorator extends Drick{
// 组合饮品
private Drick drick;
public Decorator(Drick drick) {
this.drick = drick;
}
public void setDrick(Drick drick) {
this.drick = drick;
}
@Override
float cost() {
// 组合进行价格计算
return super.getPrice() + drick.cost();
}
@Override
public String getDesc() {
return desc + " " + getPrice() + " " + drick.getDesc();
}
}
// 具体饮品,具体构件角色
public class EspreeCoffee extends Coffee{
public EspreeCoffee() {
setDesc(" 意大利咖啡 ");
setPrice(6.0f);
}
}
public class LongBlack extends Coffee{
public LongBlack() {
setDesc(" longblack ");
setPrice(5.0f);
}
}
public class ShortBlack extends Coffee{
public ShortBlack() {
setDesc(" shortBlack ");
setPrice(4.0f);
}
}
// 具体装饰类 具体装饰角色
public class Chocolate extends Decorator{
public Chocolate(Drick drick) {
super(drick);
setDesc(" 巧克力 ");
setPrice(3.0f);
}
}
public class Milk extends Decorator{
public Milk(Drick drick) {
super(drick);
setDesc(" 牛奶 ");
setPrice(2.0f);
}
}
public class Soy extends Decorator{
public Soy(Drick drick) {
super(drick);
setDesc(" 豆浆 ");
setPrice(1.5f);
}
}
// 使用
public class Client {
public static void main(String[] args) {
// 2分巧克力 + 1份牛奶 + 一个LongBlack咖啡
Drick order = new LongBlack();
System.out.println("一杯LongBlack咖啡费用是:" + order.cost());
// 加一份牛奶
order = new Milk(order);
System.out.println("一杯LongBlack咖啡 + 一份牛奶的费用是:" + order.cost());
// 加一分巧克力
order = new Chocolate(order);
System.out.println("一杯LongBlack咖啡 + 一份牛奶 + 1份巧克力的费用是:" + order.cost());
// 在加一分巧克力
order = new Chocolate(order);
System.out.println("一杯LongBlack咖啡 + 一份牛奶 + 2份巧克力的费用是:" + order.cost());
}
}
优点
缺点
本章介绍了结构型设计模式中的适配器模式
、桥接模式
、代理模式
、装饰者模式
,下一章介绍组合模式
、外观模式
、享元模式
。
码字不易,还请看过之后给个三连啦!