适配器模式

1 实际场景

1.1 日志管理系统

考虑一个记录日志的应用,由于用户对日志记录的要求很高,使得开发人员不能简单的采用一些已有的日志工具或日志框架来满足用户的要求,而需要按照用户的要求重新开发新的日志管理系统。为案例展示,只是抽取跟适配器模式相关的部分来讲述。

日志以文件的形式记录

1、先简单定义日志对象,也就是描述日志的对象模型,由于这个对象需要被写入文件中,因此这个对象需要序列化,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* @author mark.hct
* @version LogModel.java v 0.1 2023/3/1 23:15 Exp $
* @description 日志数据对象
*/
public class LogModel implements Serializable {
/**
* 日志编号
*/
private String logId;
/**
* 操作人员
*/
private String operateUser;
/**
* 操作时间,以yyyy-MM-dd HH:mm:ss的格式记录
*/
private String operateTime;
/**
* 日志内容
*/
private String logContent;

public String getLogId() {
return logId;
}

public void setLogId(String logId) {
this.logId = logId;
}

public String getOperateUser() {
return operateUser;
}

public void setOperateUser(String operateUser) {
this.operateUser = operateUser;
}

public String getOperateTime() {
return operateTime;
}

public void setOperateTime(String operateTime) {
this.operateTime = operateTime;
}

public String getLogContent() {
return logContent;
}

public void setLogContent(String logContent) {
this.logContent = logContent;
}

@Override
public String toString() {
return "LogModel{" +
"logId='" + logId + '\'' +
", operateUser='" + operateUser + '\'' +
", operateTime='" + operateTime + '\'' +
", logContent='" + logContent + '\'' +
'}';
}
}

2、操作日志文件的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author mark.hct
* @version LogFileOperateApi.java v 0.1 2023/3/1 23:18 Exp $
* @description 日志文件操作接口
*/
public interface LogFileOperateApi {
/**
* 读取日志文件,从文件里面获取存储的日志列表对象
*
* @return 存储的日志列表对象
*/
List<LogModel> readLogFile();

/**
* 写日志文件,把日志列表写出到日志文件中去
*
* @param list 要写到日志文件的日志列表
*/
void writeLogFile(List<LogModel> list);
}

3、接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @author mark.hct
* @version LogFileOperate.java v 0.1 2023/3/1 23:19 Exp $
* @description 实现对日志文件的操作
*/
public class LogFileOperate implements LogFileOperateApi {
/**
* 日志文件的路径和文件名称,默认是当前项目的根下的AdapterLog.log
*/
private String logFilePathName = "AdapterLog.log";

/**
* 构造方法,传入文件的路径和名称
*
* @param logFilePathName 文件的路径和名称
*/
public LogFileOperate(String logFilePathName) {
//先判断是否传入了文件的路径和名称,如果是,
//就重新设置操作的日志文件的路径和名称
if (logFilePathName != null && logFilePathName.trim().length() > 0) {
this.logFilePathName = logFilePathName;
}
}

@Override
public List<LogModel> readLogFile() {
List<LogModel> list = null;
ObjectInputStream oin = null;
try {
File f = new File(logFilePathName);
if (f.exists()) {
oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
list = (List<LogModel>) oin.readObject();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (oin != null) {
oin.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return list;
}

@Override
public void writeLogFile(List<LogModel> list) {
File f = new File(logFilePathName);
ObjectOutputStream oout = null;
try {
oout = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
oout.writeObject(list);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
oout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

4、客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author mark.hct
* @version Client.java v 0.1 2023/3/1 23:26 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//准备日志内容,也就是测试的数据
LogModel lm1 = new LogModel();
lm1.setLogId("001");
lm1.setOperateUser("admin");
lm1.setOperateTime("2010-03-0210:08:18");
lm1.setLogContent("这是一个测试");

List<LogModel> list = new ArrayList<LogModel>();
list.add(lm1);
//创建操作日志文件的对象
LogFileOperateApi api = new LogFileOperate("");
//保存日志文件
api.writeLogFile(list);

//读取日志文件的内容
List<LogModel> readLog = api.readLogFile();
System.out.println("readLog=" + readLog);
}
}

执行结果:

1
readLog=[LogModel{logId='001', operateUser='admin', operateTime='2010-03-0210:08:18', logContent='这是一个测试'}]

日志以数据库的形式记录

1、接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @author mark.hct
* @version LogDbOperateApi.java v 0.1 2023/3/1 23:33 Exp $
* @description 定义操作日志的应用接口,为了示例的简单,只是简单的定义了增删改查的方法
*/
public interface LogDbOperateApi {
/**
* 新增日志
*
* @param lm 需要新增的日志对象
*/
void createLog(LogModel lm);

/**
* 修改日志
*
* @param lm 需要修改的日志对象
*/
void updateLog(LogModel lm);

/**
* 删除日志
*
* @param lm 需要删除的日志对象
*/
void removeLog(LogModel lm);

/**
* 获取所有的日志
*
* @return 所有的日志对象
*/
List<LogModel> getAllLog();
}

为了示例演示方便,暂不实现数据库的接口实现。需求要求日志管理系统同时满足文件以及数据库两种方式?

1.2 有何问题

当日志管理系统新增数据库的方式时,对于原来文件保存的接口则无法使用。

如何做到使日志管理系统同时支持两种方式?

几种方式:

  • 按照数据库形式的接口,重新实现文件操作形式的操作对象。不应该对于已经实现过的对象,重复实现,违背抽象的逻辑;
  • 修改文件形式的接口,使之适配数据库的形式的接口。会导致原来依赖于此接口的代码都需要同步修改适配;

2 适配器模式

2.1 定义

适配器模式(Adapter Pattern)属于结构型模式。将一个类的接口转换成客户端适配的另外的接口,适配器模式使得原本由于接口不兼容的类,可以通过适配转接而兼容适用。

2.2 适配器模式解决问题思路

上述案例中,问题的根源在于接口的不兼容,其中功能已经实现,目标只要将接口适配,如此则可以使用两种方式进行日志管理。

按照适配器模式的实现方式,可以定义一个类来实现第二版的接口,然后在内部实现的时候,转调第一版已经实现了的功能,这样就可以通过对象组合的方式,既复用了第一版已有的功能,同时又在接口上满足了第二版调用的要求。完成上述工作的这个类就是适配器。

2.3 结构说明

image-20230305213625383
  • Client:客户端;
  • Target:客户端交互的特定领域的相关接口;
  • Adaptee:已经存在的接口,已经实现特定的功能,但是与 Target 接口不一致,需要被适配的对象;
  • Adapter:适配器,将 Adaptee 适配为 Client 需要的 Target

2.4 示例代码

1、Target 接口定义

1
2
3
4
5
6
7
8
9
10
11
/**
* @author mark.hct
* @version Target.java v 0.1 2023/3/5 21:59 Exp $
* @description 定义客户端使用的接口,与特定领域相关
*/
public interface Target {
/**
* 示意方法,客户端请求处理的方法
*/
void request();
}

2、需要被适配的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mark.hct
* @version Adaptee.java v 0.1 2023/3/5 22:00 Exp $
* @description 已经存在的接口,这个接口需要被适配
*/
public class Adaptee {
/**
* 示意方法,原本已经存在,已经实现的方法
*/
public void specificRequest() {
// 具体的功能处理
}
}

3、适配器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author mark.hct
* @version Adapter.java v 0.1 2023/3/5 22:02 Exp $
* @description 适配器
*/
public class Adapter implements Target {
/**
* 持有需要被适配的接口对象
*/
private Adaptee adaptee;

/**
* 构造方法,传入需要被适配的对象
*
* @param adaptee 需要被适配的对象
*/
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

@Override
public void request() {
// 可能转调已经实现了的方法,进行适配
adaptee.specificRequest();
}
}

4、客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mark.hct
* @version Client.java v 0.1 2023/3/5 22:05 Exp $
* @description 使用适配器的客户端
*/
public class Client {
public static void main(String[] args) {
// 创建需被适配的对象
Adaptee adaptee = new Adaptee();
// 创建客户端需要调用的接口对象
Target target = new Adapter(adaptee);
// 请求处理
target.request();
}
}

2.5 重写方案

要使用适配器模式来实现示例,关键就是要实现适配器对象,需要实现日志以数据库的形式记录的接口,在内部实现的时候,需要调用日志以文件的形式记录的功能。即,日志以数据库的记录的接口就相当于适配器模式中的 Target 接口,而以文件的形式记录的实现就相当于适配器模式中的 Adaptee 对象。

1、适配器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @author mark.hct
* @version Adapter.java v 0.1 2023/3/5 22:16 Exp $
* @description 适配器对象,把记录日志到文件的功能适配成DB需要的增删改查的功能
*/
public class Adapter implements LogDbOperateApi {
/**
* 持有需要被适配的接口对象
*/
private LogFileOperateApi adaptee;

public Adapter(LogFileOperateApi adaptee) {
this.adaptee = adaptee;
}

@Override
public void createLog(LogModel lm) {
//1:先读取文件的内容
List<LogModel> list = adaptee.readLogFile();
//2:加入新的日志对象
list.add(lm);
//3:重新写入文件
adaptee.writeLogFile(list);
}

@Override
public void updateLog(LogModel lm) {
//1:先读取文件的内容
List<LogModel> list = adaptee.readLogFile();
//2:修改相应的日志对象
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getLogId().equals(lm.getLogId())) {
list.set(i, lm);
break;
}
}
//3:重新写入文件
adaptee.writeLogFile(list);
}

@Override
public void removeLog(LogModel lm) {
//1:先读取文件的内容
List<LogModel> list = adaptee.readLogFile();
//2:删除相应的日志对象
list.remove(lm);
//3:重新写入文件
adaptee.writeLogFile(list);
}

@Override
public List<LogModel> getAllLog() {
return adaptee.readLogFile();
}
}

2、客户端的重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author mark.hct
* @version Client.java v 0.1 2023/3/5 22:19 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//准备日志内容,也就是测试的数据
LogModel lm1 = new LogModel();
lm1.setLogId("001");
lm1.setOperateUser("admin");
lm1.setOperateTime("2010-03-0210:08:18");
lm1.setLogContent("这是一个测试");
List<LogModel> list = new ArrayList<LogModel>();
list.add(lm1);
//创建操作日志文件的对象
LogFileOperateApi logFileApi = new LogFileOperate("");

//创建新版的操作日志的接口对象
LogDbOperateApi api = new Adapter(logFileApi);

//保存日志文件
api.createLog(lm1);
//读取日志文件的内容
List<LogModel> allLog = api.getAllLog();
System.out.println("allLog=" + allLog);
}
}

2.6 案例图解

image-20230305232742262

3 模式讲解

3.1 认识适配器模式

适配器模式的主要功能是进行转换匹配,目的是复用已有的功能,而不是来实现新的接口。即,客户端需要的功能应该是已经实现好了的,不需要适配器模式来实现,适配器模式主要负责把不兼容的接口转换成客户端期望的样子。

注:适配器里面可以实现功能,称这种适配器为智能适配器。同时在接口匹配和转换的过程中,也是有可能需要额外实现一定的功能,才能够转换过来,比如需要调整参数以进行匹配等。

3.2 实现方式

1、常见实现

在实现适配器的时候,适配器通常是一个类,一般会让适配器类去实现 Target 接口,然后在适配器的具体实现里面调用 Adaptee。即,适配器通常是一个 Target 类型,而不是 Adaptee 类型。如同前面的例子演示的那样。

2、智能适配器

在实际开发中,适配器也可以实现一些 Adaptee 没有的功能,但是在 Target 中定义的功能,这种情况就需要在适配器的实现里面,加入新功能的实现,这种适配器被称为智能适配器。

如果要使用智能适配器,一般新加入的功能的实现,会用到很多 Adaptee 的功能,相当于利用 Adaptee 的功能来实现更高层的功能。当然也可以完全实现新加的功能。

3、适配多个 Adaptee

实现适配器时,可以适配多个 Adaptee,即,实现某个新的 Target 时,需要调用到多个模块的功能才能满足新接口的需求。

4、缺省适配

缺省适配:为一个接口提供缺省实现。则不用直接去实现接口,而是采用继承此缺省适配对象,让子类可以有选择的去覆盖需要的方法,对于不需要的接口,则直接使用缺省的适配方法;

5、适配器的复杂度

适配器 Adapter 实现的复杂度取决于 TargetAdaptee 的相似度。

相似度高时:如,只有方法名称不一样,则适配器 Adapter 只需要简单转换一下方法就好,如文章开头的案例。

相似度低时:如,功能方法定义完全不一致,则可能需要使用 Adaptee 中多个方法来组合使用,才能实现出 Target 中一个定义的方法。

3.3 双向适配器

双向适配器:可以将 Adaptee 适配成 Target,也可以将 Target 适配成 Adaptee

如文章开头的案例中,既可以将文件形式管理日志适配成数据库的形式,也可以将数据库的形式适配成日志形式。

image-20230305233136185

1、数据形式管理日志的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* DB存储日志的实现,为了简单,这里就不去真的实现和数据库交互了,示意一下
*/
public class LogDbOperate implements LogDbOperateApi{
public void createLog(LogModel lm) {
System.out.println("now in LogDbOperate createLog,lm="+lm);
}
public List<LogModel> getAllLog() {
System.out.println("now in LogDbOperate getAllLog");
return null;
}
public void removeLog(LogModel lm) {
System.out.println("now in LogDbOperate removeLog,lm="+lm);
}
public void updateLog(LogModel lm) {
System.out.println("now in LogDbOperate updateLog,lm="+lm);
}
}

2、双向适配器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 双向适配器对象
*/
public class TwoDirectAdapter implements LogDbOperateApi,LogFileOperateApi{
/**
* 持有需要被适配的文件存储日志的接口对象
*/
private LogFileOperateApi fileLog;
/**
* 持有需要被适配的DB存储日志的接口对象
*/
private LogDbOperateApi dbLog;
/**
* 构造方法,传入需要被适配的对象
* @param fileLog 需要被适配的文件存储日志的接口对象
* @param dbLog 需要被适配的DB存储日志的接口对象
*/
public TwoDirectAdapter(LogFileOperateApi fileLog, LogDbOperateApi dbLog) {
this.fileLog = fileLog;
this.dbLog = dbLog;
}
/*-----以下是把文件操作的方式适配成为DB实现方式的接口-----*/
public void createLog(LogModel lm) {
//1:先读取文件的内容
List<LogModel> list = fileLog.readLogFile();
//2:加入新的日志对象
list.add(lm);
//3:重新写入文件
fileLog.writeLogFile(list);
}
public List<LogModel> getAllLog() {
return fileLog.readLogFile();
}
public void removeLog(LogModel lm) {
//1:先读取文件的内容
List<LogModel> list = fileLog.readLogFile();
//2:删除相应的日志对象
list.remove(lm);
//3:重新写入文件
fileLog.writeLogFile(list);
}
public void updateLog(LogModel lm) {
//1:先读取文件的内容
List<LogModel> list = fileLog.readLogFile();
//2:修改相应的日志对象
for(int i=0;i<list.size();i++){
if(list.get(i).getLogId().equals(lm.getLogId())){
list.set(i, lm);
break;
}
}
//3:重新写入文件
fileLog.writeLogFile(list);
}
/*-----以下是把DB操作的方式适配成为文件实现方式的接口-----*/
public List<LogModel> readLogFile() {
return dbLog.getAllLog();
}
public void writeLogFile(List<LogModel> list) {
//1:最简单的实现思路,先删除数据库中的数据
//2:然后循环把现在的数据加入到数据库中
for(LogModel lm : list){
dbLog.createLog(lm);
}
}
}

3、客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Client {
public static void main(String[] args) {
//准备日志内容,也就是测试的数据
LogModel lm1 = new LogModel();
lm1.setLogId("001");
lm1.setOperateUser("caixukun");
lm1.setOperateTime("2023-03-05 10:08:18");
lm1.setLogContent("这是一个测试");
List<LogModel> list = new ArrayList<LogModel>();
list.add(lm1);
//创建操作日志文件的对象
LogFileOperateApi fileLogApi = new LogFileOperate("");
LogDbOperateApi dbLogApi = new LogDbOperate();

//创建经过双向适配后的操作日志的接口对象
LogFileOperateApi fileLogApi2 = new TwoDirectAdapter(fileLogApi,dbLogApi);
LogDbOperateApi dbLogApi2 = new TwoDirectAdapter(fileLogApi,dbLogApi);

//先测试从文件操作适配到数据库,
//虽然调用的是数据库的接口,其实是文件操作在实现
dbLogApi2.createLog(lm1);
List<LogModel> allLog = dbLogApi2.getAllLog();
System.out.println("allLog="+allLog);

//再测试从数据库存储适配成文件的接口,
//也就是调用文件的接口,其实是数据实现
fileLogApi2.writeLogFile(list);
fileLogApi2.readLogFile();
}
}

注:

使用适配器有一个潜在的问题:被适配的对象不再兼容 Adaptee 的接口。因为适配器只实现了 Target 接口,所以不是所有 Adaptee 对象可以被使用的地方都可以使用适配器。

而双向适配器则解决了这个问题,双向适配器同时实现了 TargetAdaptee 的接口,使得双向适配器可以在 TargetAdaptee 被使用的地方使用。

3.4 适配器模式的优缺点

1、优点

  • 更好的复用性:如果功能已经实现,只是接口不兼容,则可以通过适配器模式让代码得到复用,不必重复建设;
  • 更好的可拓展性:实现适配器时,可以调用其它的实现接口,从而自然地拓展系统功能;

2、缺点

  • 增加代码复杂度:过多的使用适配器,会让系统层次变模糊凌乱,不容易整体把握;

3.5 思考

1、适配器模式的本质

本质:转换匹配,复用功能。通过转换调用已有的实现,从而能把已有的实现匹配成需要的接口,使之能满足客户端的需要。也就是说转换匹配是手段,而复用已有的功能才是目的。

2、使用场景

  • 若想要使用一个已经实现的功能类,但是接口不符合需求时;
  • 若想要创建一个可以复用的类,但是此类可能与一些不兼容的类一起使用;
  • 若想要使用一些已经存在的子类,但是不可能对每个子类进行适配,可以使用适配器模式直接对这些子类的父类进行适配;

3.6 相关模式

1、适配器模式和抽象工厂模式

在实现适配器时,通常需要得到被适配的对象,如果被适配的是一个接口,则可以结合一些创建型的设计模式,来的到被适配的对象实例。如:抽象工厂模式、单例模式、工厂方法模式等。

2、适配器模式和代理模式

适配器模式可以结合代理模式组合使用,在实现适配器的时候,可以通过代理来调用 Adaptee,可以获得更大的灵活性。

3、适配器模式和桥接模式

这两个模式结构相似,功能完全不同。适配器模式将一个或多个接口进行转换匹配;而桥接模式是让接口与实现部分分离,以便接口与实现可以相对独立的变化。

4、适配器模式和装饰器模式

从某种意义上讲,适配器模式能模拟实现简单的装饰模式的功能,也就是为已有功能增添功能。

如下适配器模式的实现,想大于在调用 Adaptee 中被适配方法前后增加了显得实现,适配过后,客户端得到的是被拓展过的功能,类似于装饰器模式。

1
2
3
4
5
6
public void adapterMethod(){
System.out.println("在调用Adaptee的方法之前完成一定的工作");
//调用Adaptee的相关方法
adaptee.method();
System.out.println("在调用Adaptee的方法之后完成一定的工作");
}

两种模式最大的不同:一般适配器适配过后需要改变接口,如果不需要改变接口,说明无需适配;而装饰器模式不改变接口,只是增加装饰层。