适配器模式(Java)

一:同时支持数据库和文件的日志管理

实现一个记录日志的应用,由于用户对日志记录的要求很高,使得开发人员不能简单地采用一些已有的日志工具或日志框架来满足用户的要求,而需要按照用户的要求重新开发新的日志管理系统。当然这里不可能完全按照实际系统那样去完整实现,只是抽取跟适配器模式相关的部分来讲述。

1:日志管理第一版

在第一版的时候,用户要求日志以文件的形式记录。开发人员遵照用户的要求,对日志文件的存取实现如下:

/**
 * 日志数据对象
 * @author Peter
 */
public class LogModel implements Serializable{

	private static final long serialVersionUID = 8132233778319924045L;

	/**
	 * 日志编号
	 */
	private String logId;
	
	/**
	 * 操作人员
	 */
	private String operateUser;
	
	/**
	 * 操作时间,以yyyy-MM-dd HH:mm:ss的格式记录
	 */
	private String operateTime;
	
	/**
	 * 日志内容
	 */
	private String logContent;

	//省略getter和setter方法

	@Override
	public String toString() {
		return "LogModel [logId=" + logId + ", operateUser=" + operateUser
				+ ", operateTime=" + operateTime + ", logContent=" + logContent
				+ "]";
	}
}
/**
 * 日志文件操作接口
 * @author Peter
 *
 */
public interface LogFileOperateApi {

	/**
	 * 读取日志文件,从文件里面获取存储的日志列表对象
	 * @return
	 */
	public List<LogModel> readLogFile();
	
	/**
	 * 写日志文件,把日志列表写出到日志文件中去
	 * @param list
	 */
	public void writeLogFile(List<LogModel> list);
}
/**
 * 实现对日志文件的操作
 * @author Peter
 */
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;
		}
	}
	
	//读取日志文件
	public List<LogModel> readLogFile() {
		List<LogModel> list = null;
		ObjectInputStream oin = null;
		File f = new File(logFilePathName);
		if(f.exists()){
			try {
				oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
				list = (List<LogModel>) oin.readObject();
			} catch (Exception e) {

				e.printStackTrace();
			}finally{
				if(oin!=null){
					try {
						oin.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						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 (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				oout.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}
public class Client {

	public static void main(String[] args) {
		//准备日志内容,即测试的数据
		LogModel lml = new LogModel();
		lml.setLogId("001");
		lml.setOperateUser("admin");
		lml.setOperateTime("2018-08-18 18:18:18");
		lml.setLogContent("这是一个测试");
		
		List<LogModel> list = new ArrayList<LogModel>();
		list.add(lml);
		
		//创建操作日志文件的对象
		LogFileOperateApi api = new LogFileOperate("");
		//保存日志文件
		api.writeLogFile(list);
		
		//读取日志文件
		List<LogModel> readLog = api.readLogFile();
		System.out.println("readLog = " + readLog);
	}
}

2:日志管理第二版

用户使用日志管理第一版一段时间后,开始考虑升级系统,决定要采用数据库管理日志。很快,按照数据的日志管理也实现出来了,并定义了日志管理的操作接口。主要是针对日志的增删改查方法。
/**
 * 定义操作的日志接口
 * @author Peter
 */
public interface LogDbOperateApi {

	/**
	 * 新增日志
	 * @param lml
	 */
	public void createLog(LogModel lml);
	
	/**
	 * 修改日志
	 * @param lml
	 */
	public void updateLog(LogModel lml);
	
	/**
	 * 删除日志
	 * @param lml
	 */
	public void removeLog(LogModel lml);
	
	/**
	 * 获取所有日志
	 * @return
	 */
	public List<LogModel> getAllLog();
}

能不能让日志管理第二版实现同时支持数据库存储和文件存储两种方式?

3:支持中遇到的问题


二:适配器模式

用来解决上述问题的一个合理的方法就是适配器模式。

1:适配器模式的定义

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

2:应用适配器模式解决问题的思路

仔细分析上面的问题,问题的根源在于接口的不兼容,功能是基本实现了的,也就是说,只要想办法让两边的接口匹配起来,就可以复用第一版的功能了。

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

完成上述工作的这个类就是适配器。

3:适配器的结构和说明


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

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

Adaptee:已经存在的接口,通常能满足客户端的功能要求,但是接口与客户端要求的特定领域接口不一致,需要被适配。

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

4:适配器模式示例代码

/**
 * 定义客户端使用的接口,与特定领域相关
 * @author Peter
 *
 */
public interface Target {

	/**
	 * 示意方法,客户端请求处理的方法
	 */
	public void request();	
}
/**
 * 已经存在的接口,这个接口需要被适配
 * @author Peter
 *
 */
public class Adaptee {

	/**
	 * 示意方法,原本已经存在,已经实现的方法
	 */
	public void specificRequest(){
		//具体的功能实现
		System.out.println("Adaptee中的方法");
	}
}
/**
 * 适配器
 * @author Peter
 */
public class Adapter implements Target {

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

	public void request() {
		//可能转调已经实现了的方法,进行适配
		adaptee.specificRequest();
	}
}
/**
 * 使用适配器的客户端
 * @author Peter
 */
public class Client {

	public static void main(String[] args) {
		//创建需要被适配的对象
		Adaptee adaptee = new Adaptee();
		//创建客户端需要调用的接口对象
		Target target = new Adapter(adaptee);
		//请求处理
		target.request();
	}
}

5:使用适配器模式来实现上面的日志管理

/**
 * 日志数据对象
 * @author Peter
 */
public class LogModel implements Serializable{

	private static final long serialVersionUID = 8132233778319924045L;

	/**
	 * 日志编号
	 */
	private String logId;
	
	/**
	 * 操作人员
	 */
	private String operateUser;
	
	/**
	 * 操作时间,以yyyy-MM-dd HH:mm:ss的格式记录
	 */
	private String operateTime;
	
	/**
	 * 日志内容
	 */
	private String logContent;
        //省略getter和setter方法
	@Override
	public String toString() {
		return "LogModel [logId=" + logId + ", operateUser=" + operateUser
				+ ", operateTime=" + operateTime + ", logContent=" + logContent
				+ "]";
	}
}
/**
 * 日志文件操作接口
 * @author Peter
 *
 */
public interface LogFileOperateApi {

	/**
	 * 读取日志文件,从文件里面获取存储的日志列表对象
	 * @return
	 */
	public List<LogModel> readLogFile();
	
	/**
	 * 写日志文件,把日志列表写出到日志文件中去
	 * @param list
	 */
	public void writeLogFile(List<LogModel> list);
}
/**
 * 实现对日志文件的操作
 * @author Peter
 */
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;
		}
	}

	//读取日志文件
	public List<LogModel> readLogFile() {
		List<LogModel> list = null;
		ObjectInputStream oin = null;
		File f = new File(logFilePathName);
		if(f.exists()){
			try {
				oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
				list = (List<LogModel>) oin.readObject();
			} catch (Exception e) {

				e.printStackTrace();
			}finally{
				if(oin!=null){
					try {
						oin.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						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 (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				oout.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}
/**
 * 日志数据库操作接口
 * @author Peter
 */
public interface LogDbOperateApi {

	/**
	 * 新增日志
	 * @param lml
	 */
	public void createLog(LogModel lml);
	
	/**
	 * 修改日志
	 * @param lml
	 */
	public void updateLog(LogModel lml);
	
	/**
	 * 删除日志
	 * @param lml
	 */
	public void removeLog(LogModel lml);
	
	/**
	 * 获取所有日志
	 * @return
	 */
	public List<LogModel> getAllLog();
}
/**
 * 适配器对象,将记录日志的功能适配成第二版需要的增删改查的功能
 * @author Peter
 */
public class Adapter implements LogDbOperateApi {

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

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

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

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

	//查询日志
	public List<LogModel> getAllLog() {
		return adaptee.readLogFile();
	}
}
public class Client {

	public static void main(String[] args) {
		//准备日志内容,即测试的数据
		LogModel lml = new LogModel();
		lml.setLogId("001");
		lml.setOperateUser("admin");
		lml.setOperateTime("2018-08-18 18:18:18");
		lml.setLogContent("这是一个测试");
		
		List<LogModel> list = new ArrayList<LogModel>();
		list.add(lml);
		
		//创建操作日志文件的对象
		LogFileOperateApi logFileApi = new LogFileOperate("");
		//创建新版操作日志接口对象
		LogDbOperateApi api = new Adapter(logFileApi);
		//保存日志文件
		api.createLog(lml);
		
		//读取日志文件
		List<LogModel> readLog = api.getAllLog();
		System.out.println("readLog = " + readLog);
	}
}

5:实现小结

(1)第一版:原有文件存取日志的方式,运行得很好


(2)第二版:现有的新的基于数据库的实现


(3)现在想要在第二版的实现里面,能够同时兼容第一版的功能,那么就应有一个类来实现第二版的接口,然后在这个类里面去调用已由的功能实现,这个类就是适配器。

(4)示例的整体结构


三:适配器模式讲解

1:适配器模式功能

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

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

2:Adaptee和target的关系

适配器模式中被适配的接口Adaptee和适配成为的接口Target是没有关联的,也就是说,Adaptee和target中的方法即可以相同也可以不同。极端情况下两个接口里面的方法可能是完全不同的,当然这种情况下也可以完全相同。

这里所说的相同和不同,是指方法定义的名称、参数列表、返回值,以及方法本身的功能都可以相同或不同。

3:对象组合

适配器的实现方式其实是依靠对象组合的方式。通过给适配器对象组合被适配的对象,然后当客户端调用Target的时候,适配器会把相应的功能委托给被适配的对象去完成。

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


5:适配器模式的实现

(1)适配器的常见实现

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

(2)智能适配器

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

如果要使用智能适配器,一般新加入功能的实现会用到很多Adaptee的功能,相当于利用Adaptee的功能来实现更高层的功能。当然也可以完全实现新加入的功能,和已有的功能都不相关,变相地扩展了功能。

(3)适配多个Adaptee

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

(4)适配器Adapter实现的复杂程度

适配器Adapter实现的复杂程度取决于Target和Adaptee的相似程度。如果相似程度很高,比如只有方法名称不一样,Adapter只需要简单的转调一下接口就可以了。如果相似程度很低,比如两边的接口的方法所定义的功能完全不一样,在Target中定义的一个方法,可能在Adaptee中定义了三个更小的方法,那么这个时候在实现Adapter的时候,就需要组合调用了。

(5)缺省适配

缺省适配的意思是,为一个接口提供缺省实现。有了它,就不用直接去实现接口,而是采用继承这个缺省适配对象,从而让子类可以有选择地去覆盖实现需要的方法,对于不需要的方法,使用缺省适配的方法就可以了。

6:双向适配器

适配器可以实现双向的适配,即Adaptee适配成为Target,其实也可以把Target适配称为Adaptee。即这个适配器可以同时当做Target和Adaptee来使用。

以前面的日志管理为例,在第二版不够稳定的情况下,希望在两版共存期间,主要还是使用第一版,同时希望第一版的日志也能记录到数据库中,即客户虽然操作的接口是第一版日志接口,界面也是第一版的,但是可以使用第二版的将日志记录到数据库的功能。


代码实现:

/**
 * 日志数据对象
 * @author Peter
 */
public class LogModel implements Serializable{

	private static final long serialVersionUID = 8132233778319924045L;

	/**
	 * 日志编号
	 */
	private String logId;
	
	/**
	 * 操作人员
	 */
	private String operateUser;
	
	/**
	 * 操作时间,以yyyy-MM-dd HH:mm:ss的格式记录
	 */
	private String operateTime;
	
	/**
	 * 日志内容
	 */
	private String logContent;
        //省略setter和getter方法
	@Override
	public String toString() {
		return "LogModel [logId=" + logId + ", operateUser=" + operateUser
				+ ", operateTime=" + operateTime + ", logContent=" + logContent
				+ "]";
	}
}
/**
 * 日志文件操作接口
 * @author Peter
 *
 */
public interface LogFileOperateApi {

	/**
	 * 读取日志文件,从文件里面获取存储的日志列表对象
	 * @return
	 */
	public List<LogModel> readLogFile();
	
	/**
	 * 写日志文件,把日志列表写出到日志文件中去
	 * @param list
	 */
	public void writeLogFile(List<LogModel> list);
}
/**
 * 实现对日志文件的操作
 * @author Peter
 */
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;
		}
	}

	//读取日志文件
	public List<LogModel> readLogFile() {
		List<LogModel> list = null;
		ObjectInputStream oin = null;
		File f = new File(logFilePathName);
		if(f.exists()){
			try {
				oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
				list = (List<LogModel>) oin.readObject();
			} catch (Exception e) {

				e.printStackTrace();
			}finally{
				if(oin!=null){
					try {
						oin.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						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 (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				oout.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}
/**
 * 日志数据库操作接口
 * @author Peter
 */
public interface LogDbOperateApi {

	/**
	 * 新增日志
	 * @param lml
	 */
	public void createLog(LogModel lml);
	
	/**
	 * 修改日志
	 * @param lml
	 */
	public void updateLog(LogModel lml);
	
	/**
	 * 删除日志
	 * @param lml
	 */
	public void removeLog(LogModel lml);
	
	/**
	 * 获取所有日志
	 * @return
	 */
	public List<LogModel> getAllLog();
}
/**
 * 实现日志数据库操作
 * @author Peter
 */
public class LogDbOperate implements LogDbOperateApi {

	//新增日志
	public void createLog(LogModel lml) {
		System.out.println("数据库新增日志" + lml);
	}

	//更新日志
	public void updateLog(LogModel lml) {
		System.out.println("数据库更新日志" + lml);
	}

	//删除日志
	public void removeLog(LogModel lml) {
		System.out.println("数据库删除日志" + lml);
	}

	//查询日志
	public List<LogModel> getAllLog() {
		System.out.println("数据库查询所有日志");
		return null;
	}
}
/**
 * 双向适配器对象 
 * @author Peter
 */
public class TwoDirectorAdapter implements LogDbOperateApi, LogFileOperateApi {

	/**
	 * 持有需要被适配的文件存储日志的接口对象
	 */
	private LogFileOperateApi fileLog;

	/**
	 * 持有需要被适配的数据库存储日志接口对象
	 */
	private LogDbOperateApi dbLog;

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

	/* 以下是把文件操作的方式适配成为DB实现方式的接口 */
	// 增加日志
	public void createLog(LogModel lml) {
		// 先读取文件的内容
		List<LogModel> readLogFile = fileLog.readLogFile();
		// 加入新的日志对象
		readLogFile.add(lml);
		// 重新写入文件
		fileLog.writeLogFile(readLogFile);
	}

	// 查询日志
	public List<LogModel> getAllLog() {
		return fileLog.readLogFile();
	}

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

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

	/*以下是把Db操作的方式适配成为文件实现方式的接口*/
	public List<LogModel> readLogFile() {

		return dbLog.getAllLog();
	}
	
	public void writeLogFile(List<LogModel> list) {

		for(LogModel lml : list){
			dbLog.createLog(lml);
		}
	}
}
public class Client {
	public static void main(String[] args) {
		//准备日志内容,即测试的数据
		LogModel lml = new LogModel();
		lml.setLogId("001");
		lml.setOperateUser("admin");
		lml.setOperateTime("2018-08-18 18:18:18");
		lml.setLogContent("这是一个测试");
		
		List<LogModel> list = new ArrayList<LogModel>();
		list.add(lml);
		
		//创建操作日志文件的对象
		LogFileOperateApi fileLogApi = new LogFileOperate("");
		LogDbOperateApi dbLogApi = new LogDbOperate();
		
		//创建经过双向适配后的操作日志的接口对象
		LogFileOperateApi fileLogApi2 = new TwoDirectorAdapter(fileLogApi, dbLogApi);
		LogDbOperateApi dbLogApi2 = new TwoDirectorAdapter(fileLogApi, dbLogApi);
		
		//先测试从文件适配操作第二版,虽然调用的是第二版的接口其实是文件操作在实现
		dbLogApi2.createLog(lml);
		List<LogModel> allLog = dbLogApi2.getAllLog();
		System.out.println("allLog = " + allLog);
		
		//再测试从数据库存储适配成第一版的接口,也是调用的是第一版的接口,其实是数据库的实现
		fileLogApi2.writeLogFile(list);
		fileLogApi2.readLogFile();
	}
}

注:事实上,使用适配器有一个潜在的问题,就是被适配的对象不再兼容Adaptee的接口,因为适配器只是实现了Target的接口。这导致并不是所有Adaptee对象可以被使用的地方都能使用适配器。但是双向适配器就解决了这样的问题,双向适配器同时实现了Target和Adaptee的接口,使得双向适配器可以在Target或Adaptee被使用的地方使用,以提供对所有客户的透明性。尤其是在两个不同的客户需要用不同的方式查看同一个对象时,适合使用双向适配器。

四:对象适配器和类适配器

1:定义

在标准的适配器模式里,根据适配器的实现方式,把适配器分成两种:对象适配器和类适配器。

对象适配器的实现:依赖于对象组合;类适配器的实现:采用多重继承对一个接口与另一个接口进行匹配。由于Java不支持多重继承,所以到目前为止,还没有涉及。下面是它的类图说明。虽然Java里面不能实现多重继承,但是我们可以有一种变通,就是让适配器Adapter实现Target接口,继承Adaptee的实现。


2:用类适配器实现上面的日志管理
/**
 * 日志数据对象
 * @author Peter
 */
public class LogModel implements Serializable{

	private static final long serialVersionUID = 8132233778319924045L;

	/**
	 * 日志编号
	 */
	private String logId;
	
	/**
	 * 操作人员
	 */
	private String operateUser;
	
	/**
	 * 操作时间,以yyyy-MM-dd HH:mm:ss的格式记录
	 */
	private String operateTime;
	
	/**
	 * 日志内容
	 */
	private String logContent;
        //省略getter和setter方法
	@Override
	public String toString() {
		return "LogModel [logId=" + logId + ", operateUser=" + operateUser
				+ ", operateTime=" + operateTime + ", logContent=" + logContent
				+ "]";
	}
}
/**
 * 日志文件操作接口
 * @author Peter
 */
public interface LogFileOperateApi {

	/**
	 * 读取日志文件,从文件里面获取存储的日志列表对象
	 * @return
	 */
	public List<LogModel> readLogFile();
	
	/**
	 * 写日志文件,把日志列表写出到日志文件中去
	 * @param list
	 */
	public void writeLogFile(List<LogModel> list);
}
/**
 * 实现对日志文件的操作
 * @author Peter
 */
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;
		}
	}

	//读取日志文件
	public List<LogModel> readLogFile() {
		List<LogModel> list = null;
		ObjectInputStream oin = null;
		File f = new File(logFilePathName);
		if(f.exists()){
			try {
				oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
				list = (List<LogModel>) oin.readObject();
			} catch (Exception e) {
				e.printStackTrace();
			}finally{
				if(oin!=null){
					try {
						oin.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						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 (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				oout.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}
/**
 * 日志数据库操作接口
 * @author Peter
 */
public interface LogDbOperateApi {

	/**
	 * 新增日志
	 * @param lml
	 */
	public void createLog(LogModel lml);
	
	/**
	 * 修改日志
	 * @param lml
	 */
	public void updateLog(LogModel lml);
	
	/**
	 * 删除日志
	 * @param lml
	 */
	public void removeLog(LogModel lml);
	
	/**
	 * 获取所有日志
	 * @return
	 */
	public List<LogModel> getAllLog();
}
/**
 * 类适配器对象 
 * @author Peter
 */
public class ClassAdapter extends LogFileOperate implements LogDbOperateApi {

	public ClassAdapter(String logFilePathName) {
		super(logFilePathName);
	}

	/* 以下是把文件操作的方式适配成为DB实现方式的接口 */
	// 增加日志
	public void createLog(LogModel lml) {
		// 先读取文件的内容
		List<LogModel> readLogFile = this.readLogFile();
		// 加入新的日志对象
		readLogFile.add(lml);
		// 重新写入文件
		this.writeLogFile(readLogFile);
	}

	// 查询日志
	public List<LogModel> getAllLog() {
		return this.readLogFile();
	}

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

	// 更新日志
	public void updateLog(LogModel lml) {
		// 先读取文件的内容
		List<LogModel> readLogFile = this.readLogFile();
		// 修改相应的日志对象
		for (int i = 0; i < readLogFile.size(); i++) {
			if (readLogFile.get(i).getLogId().equals(lml.getLogId())) {
				readLogFile.set(i, lml);
				break;
			}
		}
		// 重新写入文件
		this.writeLogFile(readLogFile);
	}
}
public class Client {

	public static void main(String[] args) {
		//准备日志内容,即测试的数据
		LogModel lml = new LogModel();
		lml.setLogId("002");
		lml.setOperateUser("admin");
		lml.setOperateTime("2019-09-18 18:18:18");
		lml.setLogContent("这是一个测试");
		
		List<LogModel> list = new ArrayList<LogModel>();
		list.add(lml);
		
		LogDbOperateApi adapter = new ClassAdapter("");
		adapter.createLog(lml);
		adapter.updateLog(lml);
		adapter.getAllLog();
		adapter.removeLog(lml);
	}
}

在实现中,主要是适配器的实现方式与以前不一样了,与对象适配器实现同样的功能相比,类适配器在实现上有如下改变:

(1)需要继承LogFileOperate的实现,然后再实现LogDbOperateApi接口;

(2)需要按照继承LogFileOperate的要求,提供传入文件路径和名称的构造方法;

(3)不再需要持有LogFileOperate的对象了,因为适配器本身就是LogFileOperate对象的子类;

(4)以前调用被适配对象的方法的地方,全部修改成调用自己的方法。

3:类适配器和对象适配器的权衡

从实现上:类适配器使用对象继承的方式,是静态的定义方式;而对象适配器使用对象组合的方式,是动态组合的方式。

对于类适配器,由于适配器直接继承了Adaptee,使得适配器不能和Adaptee的子类一起工作,因为继承是静态的关系,当适配器继承了Adaptee后,就不可能再去处理Adaptee的子类了;对于对象适配器,允许一个Adapter和多个Adaptee,包括Adaptee和它所有的子类一起工作。因为对象适配器采用的是对象组合的关系,只要对象类型正确,是不是子类都无所谓。

对于类适配器,适配器可以重定义Adaptee的部分行为,相当于子类覆盖父类的部分实现方法;对于对象适配器,要重定义Adaptee的行为比较困难,这种情况下,需要定义Adaptee的子类来实现重定义,然后让适配器组合子类。

对于类适配器,仅仅引入了一个对象,并不需要额外的引用来间接得到Adaptee。对于对象适配器,需要额外的引用来间接得到Adaptee;

在Java开发中,建议尽量使用对象适配器的实现方式。当然,具体问题具体分析,根据需要来选用实现方式,最合适的才是最好的。

五:适配器模式的优缺点

1:适配器模式的优点

更好的复用性(如果功能是已经有了的,只是接口不兼容,那么通过适配器模式就可以让这些功能得到更好的服用)。

更好的可扩展性(在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能)。

2:适配器模式的缺点

过多地使用适配器,会让系统非常零乱,不容易整体进行把握。(比如,明明看到调用的是A接口,其实内部被适配成了B接口来实现,一个系统如果太多出现这种情况,无异于异常灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构)。

六:小思适配器模式

1:适配器模式的本质是:转换匹配,复用功能。

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

在进行转换匹配的过程中,适配器还可以在转换调用的前后实现一些功能处理,也就是实现智能的适配。

2:何时选用适配器模式

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

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

想使用一些已经存在的子类,但是不可能对每一个子类都进行适配,这种情况可以选用对象适配器,直接适配这些子类的父类就可以了。


猜你喜欢

转载自blog.csdn.net/u013132035/article/details/80644622