适配器模式

今天继续设计模式之旅,最近感觉有点浮燥,学习总是静不下心,总是想着要同时学好多技术,尽快学成,有点想急于求成的心态吧,对于技术这东西,除非自己的机智是非常之聪明(我只能说我是属于有梦想的"庸才"),否则只能脚踏实地的一点点循序渐进,注重扎实的基础训练,只有这样才能让自己真正的进步,不然如果带着这急切的心理,每天并发学习的量很大,学习的速度是提升了,但学习的效果那就保证了,而要明白的是你学习这些技术,是不是真正能为你所用,而不只是知道其表面上的概念,所以博客开篇前先整理下自己最近不好的情绪,回规到正常的轨道上来,好了言归正传,注重基础的模式训练------适配器模式。

首先举一个生活中的例子,来初识下适配器模式的大至作用,我们知道有些笔记本的插座是外国标准的,如下图:

这时就需要用到一个转接头,我想大家都应该看到过:

所以,加上这个转接头,我们就可以正常用中国的插座进行供电了:

是不是有了一个很直观的了解呢?哈哈,接下来具体再一一进一步阐述它:

定义:

  一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作

结构和说明:

Client:客户端,调用自己需要的领域接口Target

Target:定义客户端需要的跟特定领域相关的接口

Adaptee:已经存在的接口,但与客户端要求的特定领域接口不一致,需要被适配

Adapter:适配器,把Adapter适配成为Client需要的Target

对于以上这些结构先不用过多揣摩,先看下以下代码,就比较清晰地能够初识适配器模式了:

Target.java【客户端最终要调用的接口】:

/**
 * 定义客户端使用的接口,与特定领域相关
 */
public interface Target {
    /**
     * 示意方法,客户端请求处理的方法
     */
    public void request();
}

Adaptee.java【需要被适配的接口类,也就是客户端调用最终会委派到这个类来】:

/**
 * 已经存在的接口,这个接口需要被适配
 */
public class Adaptee {
    /**
     * 示意方法,
     */
    public void specificRequest() {
        System.out.println("specificRequest");
    }
}

Adapter.java【传说中的适配器类,该模式中最核心的类,把握它的写法】:

/**
 * 适配器
 */
public class Adapter implements Target {
    /**
     * 持有需要被适配的接口对象
     */
    private Adaptee adaptee;

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

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

Client.java:

/**
 * 使用适配器的客户端
 */
public class Client {
    public static void main(String[] args) {
        // 创建需被适配的对象
        Adaptee adaptee = new Adaptee();
        // 创建客户端需要调用的接口对象
        Target target = new Adapter(adaptee);
        // 请求处理,最终被适配调用到另外的方法去了
        target.request();
    }
}

再回过头来,理解上面的结构图,是不是就比较容易体现适配器模式是干嘛的呢,接下来会继续对其进行加深。

接下来以一个系统日志管理的功能为例,来进一步说明适配器的使用场合:

第一版是以文件的形式记录的

代码结构如下:

具体代码:

LogModel.java:

/**
 * 日志数据对象
 */
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;
    }

    public String toString() {
        return "logId=" + logId + ",operateUser=" + operateUser + ",operateTime=" + operateTime + ",logContent=" + logContent;
    }
}

LogFileOperateApi.java:

/**
 * 日志文件操作接口
 */
public interface LogFileOperateApi {
    /**
     * 读取日志文件,从文件里面获取存储的日志列表对象
     * 
     * @return 存储的日志列表对象
     */
    public List<LogModel> readLogFile();

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

LogFileOperate.java:

/**
 * 实现对日志文件的操作
 */
public class LogFileOperate implements LogFileOperateApi {
    /**
     * 日志文件的路径和文件名称,默认是当前classpath下的AdapterLog.log
     */
    private String logFilePathName = "AdapterLog.log";

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

    /**
     * 从文件中反序列化日志记录
     */
    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;
    }

    /**
     * 将日志记录序列化本地文件中
     */
    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();
            }
        }
    }
}

Client.java:

public class Client {
    public static void main(String[] args) {
        // 准备日志内容,也就是测试的数据
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-02 10: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);
    }
}

但是,第二版需升级为数据库来管理日志

这时先定义好操作数据库日志的接口:

LogDbOperateApi.java:

/**
 * 定义操作日志的应用接口,为了示例的简单, 只是简单的定义了增删改查的方法
 */
public interface LogDbOperateApi {
    /**
     * 新增日志
     * 
     * @param lm
     *            需要新增的日志对象
     */
    public void createLog(LogModel lm);

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

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

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

它的具体实现先不实现,因为下面还有一些要求

这时有个要求:就是在第二版的实现里面,能同时兼容第一版的功能,也就是客户端调用第二版数据库操作日志的接口,而实际上用第一版记录文件的功能,这时适配器模式就派上用场了,具体实现如下:

框架图:

Adapter.java:

 

/**
 * 适配器对象,把记录日志到文件的功能适配成第二版需要的增删改查的功能
 */
public class Adapter implements LogDbOperateApi {
    /**
     * 持有需要被适配的接口对象
     */
    private LogFileOperateApi adaptee;

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

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

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

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

    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);
    }
}

 

这时面向客户端的接口还是操作数据库日志,但实现还是兼容第一版的,如下:

public class Client {
    public static void main(String[] args) {
        // 准备日志内容,也就是测试的数据
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-02 10: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("allLog44=" + allLog);
    }
}

用一个简单示意图来描述第二版的过程:

 


通过这个日志的例子,应该对于适配器模式的作用应该更进一步了,下面还会对其进行进一步总结。

适配器模式的调用顺序示意图:

 

适配器的常用实现:

  适配器通常是一个类,一般会让适配器类去实现Target接口,然后在适配器的具体实现里调用Adaptee。【可以细细体会下】

适配器可以适配多个Adaptee:

比如我们想在执行方法之前与之后打印执行的时间,这时就可以建一个日志时间的Adaptee:

TimeUtil.java:

 

/**
 * 记录执行时间的adaptee对象,由Adapter中去调
 */
public class TimeUtil {
    private long a1;
    private long a2;

    public void begin() {
        a1 = System.currentTimeMillis();
    }

    public void end() {
        a2 = System.currentTimeMillis();
    }

    public void show() {
        System.out.println("times11===" + (a2 - a1));
    }
}

 

Adapter.java【这时里面就执有2个Adaptee】:

/**
 * 适配器对象,把记录日志到文件的功能适配成第二版需要的增删改查的功能
 */
public class Adapter implements LogDbOperateApi {
    /**
     * 持有需要被适配的接口对象
     */
    private LogFileOperateApi adaptee;

    private TimeUtil adaptee2;

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

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

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

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

    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);
    }
}

Client.java【这时客户端的调用就需要传两个adaptee对象】:

public class Client {
    public static void main(String[] args) {
        // 准备日志内容,也就是测试的数据
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-02 10:08:18");
        lm1.setLogContent("这是一个测试");

        List<LogModel> list = new ArrayList<LogModel>();
        list.add(lm1);

        // 创建操作日志文件的对象
        LogFileOperateApi logFileApi = new LogFileOperate("");

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

        // 保存日志文件
        api.createLog(lm1);

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

这就是简单对这句话的解释。

说明:上面的代码都是基于之前的来改变的,没有特珠的变化。

 

适配器可以提供一个缺省适配:

  先用代码来对这问话进行一个解决,然后再来谈缺省适配的意义所在。

DefaultAdapter.java【还是适配数据库日志】:

/**
 * 缺省的适配器,里面都是默认的实现方式
 */
public class DefaultAdapter implements LogDbOperateApi {

    @Override
    public void createLog(LogModel lm) {
        System.out.println("DB createLog===========");
    }

    @Override
    public void updateLog(LogModel lm) {
        System.out.println("DB updateLog===========");
    }

    @Override
    public void removeLog(LogModel lm) {

    }

    @Override
    public List<LogModel> getAllLog() {
        return null;
    }

}

MyAdapter.java:

/**
 * 继续缺省的适配器,可以扩展我们自己的实现,没有实现的就采用默认的行为, 这里只扩 展了getAllLog、removeLog采用文件日志的方式,其它的方法则是默认行为
 */
public class MyAdapter extends DefaultAdapter {
    /**
     * 持有需要被适配的接口对象
     */
    private LogFileOperateApi adaptee;

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

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

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

Client.java:

public class Client {
    public static void main(String[] args) {
        // 准备日志内容,也就是测试的数据
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-02 10:08:18");
        lm1.setLogContent("这是一个测试");

        List<LogModel> list = new ArrayList<LogModel>();
        list.add(lm1);

        // 创建操作日志文件的对象
        LogFileOperateApi logFileApi = new LogFileOperate("");

        // 创建新版的操作日志的接口对象
        LogDbOperateApi api = new MyAdapter(logFileApi);
// 保存日志文件
        api.createLog(lm1);

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

根据以上代码,来体会一下缺省适配器的意义:让程序更灵活,如果有自己新的实现时,我们只要继承默认适配器,重写有新的实现的方法既可,如果没有新的实现,则完全可以就用默认适配器就可以了,这就是意义所在!

适配器也可以实现双向的适配【对于它的实际使用场景,还有待实际去发现,先就这种实现方式进行认识下】

直接上代码:

TwoDirectAdapter.java:

 

/**
 * 双向适配器对象
 */
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);
    }

    public void removeAll() {
        System.out.println("now in two direct remove all");
    }

    /*-----以下是把DB操作的方式适配成为文件实现方式的接口-----*/
    public List<LogModel> readLogFile() {
        return dbLog.getAllLog();
    }

    public void writeLogFile(List<LogModel> list) {
        // 1:最简单的实现思路,先删除数据库中的数据
        dbLog.removeAll();
        // 2:然后循环把现在的数据加入到数据库中
        for (LogModel lm : list) {
            dbLog.createLog(lm);
        }
    }
}

 

Client.java:

public class Client {
    public static void main(String[] args) {
        // 准备日志内容,也就是测试的数据
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-02 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("allLog555=" + allLog);

        // 再测试从数据库存储适配成第一版的接口,也就是调用第一版的接口,其实是数据实现
        System.out.println("--------------------------->File Api  ");
        fileLogApi2.writeLogFile(list);
        fileLogApi2.readLogFile();
    }
}

我们可以看到,通过这个双向的适配器,客户端可以灵活地选择到底用哪种方案去实现,用一个简单示例图来描述一下其流程:

到此,关于适配器模式的各种使用方式已经展现完毕,最后再进一步总结一下

 

适配器的优缺点:

  1、更好的复用性

  2、更好的可扩展性

  3、过多的使用适配器,会让系统非常零乱,不容易整体进行把握。【尤其对于看代码的人来说,明明是一个操作数据库日志的方法,而最终会被适配成其实方式,比如文件操作,这样就很容易让人迷糊,适配器最好的使用场景就是维护阶段,在开发阶段尽量少用】

适配器的本质:

  转换匹配、复用功能

使用场景:

  1、如果你想要使用一个已经存在的类,但是它的接口不符合你的要求,这种情况可以使用适配器模式,来把已有的实现转换成你需要的接口。

  2、如果我想创建一个可以复用的类,这个类可能和一些不兼容的类一起工作,这种情况可以使用适配器模式,到时候需要什么就适配什么。

好啦,适配器模式学决到此,下一个模式继续见!

posted on 2013-10-27 15:38  cexo  阅读(249)  评论(0)    收藏  举报

导航