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
|
public class LogModel implements Serializable {
private String logId;
private String operateUser;
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
|
public interface LogFileOperateApi {
List<LogModel> readLogFile();
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
|
public class LogFileOperate implements LogFileOperateApi {
private String logFilePathName = "AdapterLog.log";
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
|
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
|
public interface LogDbOperateApi {
void createLog(LogModel lm);
void updateLog(LogModel lm);
void removeLog(LogModel lm);
List<LogModel> getAllLog(); }
|
为了示例演示方便,暂不实现数据库的接口实现。需求要求日志管理系统同时满足文件以及数据库两种方式?
1.2 有何问题
当日志管理系统新增数据库的方式时,对于原来文件保存的接口则无法使用。
如何做到使日志管理系统同时支持两种方式?
几种方式:
- 按照数据库形式的接口,重新实现文件操作形式的操作对象。不应该对于已经实现过的对象,重复实现,违背抽象的逻辑;
- 修改文件形式的接口,使之适配数据库的形式的接口。会导致原来依赖于此接口的代码都需要同步修改适配;
2 适配器模式
2.1 定义
适配器模式(Adapter Pattern)属于结构型模式。将一个类的接口转换成客户端适配的另外的接口,适配器模式使得原本由于接口不兼容的类,可以通过适配转接而兼容适用。
2.2 适配器模式解决问题思路
上述案例中,问题的根源在于接口的不兼容,其中功能已经实现,目标只要将接口适配,如此则可以使用两种方式进行日志管理。
按照适配器模式的实现方式,可以定义一个类来实现第二版的接口,然后在内部实现的时候,转调第一版已经实现了的功能,这样就可以通过对象组合的方式,既复用了第一版已有的功能,同时又在接口上满足了第二版调用的要求。完成上述工作的这个类就是适配器。
2.3 结构说明
Client:客户端;
Target:客户端交互的特定领域的相关接口;
Adaptee:已经存在的接口,已经实现特定的功能,但是与 Target 接口不一致,需要被适配的对象;
Adapter:适配器,将 Adaptee 适配为 Client 需要的 Target;
2.4 示例代码
1、Target 接口定义
1 2 3 4 5 6 7 8 9 10 11
|
public interface Target {
void request(); }
|
2、需要被适配的对象
1 2 3 4 5 6 7 8 9 10 11 12 13
|
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
|
public class Adapter implements Target {
private Adaptee 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
|
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
|
public class Adapter implements LogDbOperateApi {
private LogFileOperateApi adaptee;
public Adapter(LogFileOperateApi adaptee) { this.adaptee = adaptee; }
@Override public void createLog(LogModel lm) { List<LogModel> list = adaptee.readLogFile(); list.add(lm); adaptee.writeLogFile(list); }
@Override public void updateLog(LogModel lm) { List<LogModel> list = adaptee.readLogFile(); for (int i = 0; i < list.size(); i++) { if (list.get(i).getLogId().equals(lm.getLogId())) { list.set(i, lm); break; } } adaptee.writeLogFile(list); }
@Override public void removeLog(LogModel lm) { List<LogModel> list = adaptee.readLogFile(); list.remove(lm); 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
|
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 案例图解
3 模式讲解
3.1 认识适配器模式
适配器模式的主要功能是进行转换匹配,目的是复用已有的功能,而不是来实现新的接口。即,客户端需要的功能应该是已经实现好了的,不需要适配器模式来实现,适配器模式主要负责把不兼容的接口转换成客户端期望的样子。
注:适配器里面可以实现功能,称这种适配器为智能适配器。同时在接口匹配和转换的过程中,也是有可能需要额外实现一定的功能,才能够转换过来,比如需要调整参数以进行匹配等。
3.2 实现方式
1、常见实现
在实现适配器的时候,适配器通常是一个类,一般会让适配器类去实现 Target 接口,然后在适配器的具体实现里面调用 Adaptee。即,适配器通常是一个 Target 类型,而不是 Adaptee 类型。如同前面的例子演示的那样。
2、智能适配器
在实际开发中,适配器也可以实现一些 Adaptee 没有的功能,但是在 Target 中定义的功能,这种情况就需要在适配器的实现里面,加入新功能的实现,这种适配器被称为智能适配器。
如果要使用智能适配器,一般新加入的功能的实现,会用到很多 Adaptee 的功能,相当于利用 Adaptee 的功能来实现更高层的功能。当然也可以完全实现新加的功能。
3、适配多个 Adaptee
实现适配器时,可以适配多个 Adaptee,即,实现某个新的 Target 时,需要调用到多个模块的功能才能满足新接口的需求。
4、缺省适配
缺省适配:为一个接口提供缺省实现。则不用直接去实现接口,而是采用继承此缺省适配对象,让子类可以有选择的去覆盖需要的方法,对于不需要的接口,则直接使用缺省的适配方法;
5、适配器的复杂度
适配器 Adapter 实现的复杂度取决于 Target 与 Adaptee 的相似度。
相似度高时:如,只有方法名称不一样,则适配器 Adapter 只需要简单转换一下方法就好,如文章开头的案例。
相似度低时:如,功能方法定义完全不一致,则可能需要使用 Adaptee 中多个方法来组合使用,才能实现出 Target 中一个定义的方法。
3.3 双向适配器
双向适配器:可以将 Adaptee 适配成 Target,也可以将 Target 适配成 Adaptee。
如文章开头的案例中,既可以将文件形式管理日志适配成数据库的形式,也可以将数据库的形式适配成日志形式。
1、数据形式管理日志的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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;
private LogDbOperateApi dbLog;
public TwoDirectAdapter(LogFileOperateApi fileLog, LogDbOperateApi dbLog) { this.fileLog = fileLog; this.dbLog = dbLog; } public void createLog(LogModel lm) { List<LogModel> list = fileLog.readLogFile(); list.add(lm); fileLog.writeLogFile(list); } public List<LogModel> getAllLog() { return fileLog.readLogFile(); } public void removeLog(LogModel lm) { List<LogModel> list = fileLog.readLogFile(); list.remove(lm); fileLog.writeLogFile(list); } public void updateLog(LogModel lm) { List<LogModel> list = fileLog.readLogFile(); for(int i=0;i<list.size();i++){ if(list.get(i).getLogId().equals(lm.getLogId())){ list.set(i, lm); break; } } fileLog.writeLogFile(list); } public List<LogModel> readLogFile() { return dbLog.getAllLog(); } public void writeLogFile(List<LogModel> list) { 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 对象可以被使用的地方都可以使用适配器。
而双向适配器则解决了这个问题,双向适配器同时实现了 Target 和 Adaptee 的接口,使得双向适配器可以在 Target 或 Adaptee 被使用的地方使用。
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.method(); System.out.println("在调用Adaptee的方法之后完成一定的工作"); }
|
两种模式最大的不同:一般适配器适配过后需要改变接口,如果不需要改变接口,说明无需适配;而装饰器模式不改变接口,只是增加装饰层。