基于Java适配器模式的旧系统功能扩展
2024-07-137.0k 阅读
一、Java 适配器模式概述
(一)适配器模式定义
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。在 Java 开发中,适配器模式就像是一个翻译官,把一种“语言”(接口)转换成另一种“语言”(期望的接口),从而让不同“语言”的类能够协作。
(二)适配器模式的角色
- 目标(Target)接口:客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。在旧系统功能扩展场景下,这是我们希望为旧系统功能适配后所符合的新接口规范。例如,在一个旧的支付系统中,我们希望扩展新的支付方式,新的支付接口可能定义了统一的支付方法
pay(String amount, String paymentMethod)
,这就是目标接口。 - 适配者(Adaptee)类:需要被适配的类,它包含了客户希望使用的功能,但接口与目标接口不兼容。比如,旧支付系统中已有的支付类
OldPayment
,它可能有自己特有的支付方法oldPay(String amount, String payType)
,与新的支付接口不匹配,这个OldPayment
类就是适配者。 - 适配器(Adapter)类:连接目标接口和适配者类的桥梁。它实现目标接口,并在内部调用适配者类的方法,完成接口转换。例如
PaymentAdapter
类,它实现了新的支付接口PaymentTarget
,并且在pay
方法内部调用OldPayment
的oldPay
方法,完成从旧接口到新接口的适配。
二、旧系统面临的问题及适配需求
(一)旧系统的常见问题
- 接口不兼容:随着业务的发展,新的功能需求不断涌现,而旧系统的接口往往是基于当时的业务场景设计的。例如,早期的电商系统在处理订单时,订单接口只支持简单的商品信息传递,而现在业务需要传递详细的商品属性、规格等复杂信息,旧的订单接口无法满足新需求,导致接口不兼容。
- 代码耦合度高:旧系统在长期的维护和扩展过程中,可能存在大量的硬编码和紧密耦合的代码。比如,在一个旧的物流管理系统中,物流配送算法与具体的物流供应商实现紧密耦合,当需要更换物流供应商时,需要修改大量的核心业务代码,增加了维护成本和风险。
- 技术过时:旧系统可能基于过时的技术框架或编程语言特性开发。例如,一些早期基于 Java 1.4 开发的系统,无法享受 Java 8 带来的新特性,如 Lambda 表达式、Stream API 等,导致开发效率低下,难以进行高效的功能扩展。
(二)适配需求分析
- 保持旧系统稳定:在对旧系统进行功能扩展时,不能影响旧系统的现有功能。以旧的财务系统为例,在扩展新的财务报表功能时,不能破坏原有的账务处理、凭证生成等核心功能的稳定性。
- 兼容新的业务需求:要使旧系统能够适应新的业务场景和需求。比如,随着移动支付的普及,旧的零售系统需要增加移动支付功能,以满足消费者多样化的支付需求。
- 易于维护和扩展:适配后的系统应该具有良好的可维护性和扩展性。例如,在适配后的电商系统中,如果未来需要增加新的促销活动类型,应该能够在不影响其他模块的情况下轻松实现。
三、基于 Java 适配器模式实现旧系统功能扩展
(一)类适配器实现方式
- 代码示例
假设我们有一个旧的图形绘制类
OldRectangle
,它只能绘制简单的矩形,接口如下:
// 旧的矩形绘制类
class OldRectangle {
public void drawOldRectangle(int x, int y, int width, int height) {
System.out.println("绘制旧矩形,左上角坐标: (" + x + ", " + y + "), 宽: " + width + ", 高: " + height);
}
}
现在我们希望扩展图形绘制功能,使其符合新的图形绘制接口 Shape
,新接口定义如下:
// 新的图形绘制接口
interface Shape {
void draw(int x, int y, int... parameters);
}
使用类适配器模式来实现这个功能,适配器类 RectangleAdapter
如下:
// 矩形适配器类,继承旧矩形类并实现新图形接口
class RectangleAdapter extends OldRectangle implements Shape {
@Override
public void draw(int x, int y, int... parameters) {
if (parameters.length == 2) {
int width = parameters[0];
int height = parameters[1];
drawOldRectangle(x, y, width, height);
}
}
}
在客户端代码中使用适配器:
public class Client {
public static void main(String[] args) {
Shape rectangleShape = new RectangleAdapter();
rectangleShape.draw(10, 10, 100, 200);
}
}
- 类适配器特点
- 优点:实现简单,通过继承直接复用适配者的方法,对于一些简单的适配场景,代码量少,直观易懂。
- 缺点:由于 Java 单继承的限制,适配器类不能再继承其他类,这在某些需要多重继承的场景下会受到限制。并且,这种方式对适配者类的依赖较强,如果适配者类发生较大变化,可能会影响适配器的稳定性。
(二)对象适配器实现方式
- 代码示例
同样以图形绘制为例,先定义旧的圆形绘制类
OldCircle
:
// 旧的圆形绘制类
class OldCircle {
public void drawOldCircle(int x, int y, int radius) {
System.out.println("绘制旧圆形,圆心坐标: (" + x + ", " + y + "), 半径: " + radius);
}
}
新的图形绘制接口 Shape
保持不变。使用对象适配器模式,适配器类 CircleAdapter
如下:
// 圆形适配器类,组合旧圆形类并实现新图形接口
class CircleAdapter implements Shape {
private OldCircle oldCircle;
public CircleAdapter(OldCircle oldCircle) {
this.oldCircle = oldCircle;
}
@Override
public void draw(int x, int y, int... parameters) {
if (parameters.length == 1) {
int radius = parameters[0];
oldCircle.drawOldCircle(x, y, radius);
}
}
}
在客户端代码中使用适配器:
public class Client2 {
public static void main(String[] args) {
OldCircle oldCircle = new OldCircle();
Shape circleShape = new CircleAdapter(oldCircle);
circleShape.draw(50, 50, 30);
}
}
- 对象适配器特点
- 优点:灵活性高,通过组合的方式适配适配者类,避免了单继承的限制,适配器类可以同时实现多个接口,并且可以在运行时动态地替换适配者对象。
- 缺点:相比于类适配器,对象适配器的代码相对复杂一些,需要更多的代码来处理组合关系。同时,由于是通过组合方式调用适配者方法,可能在性能上略有损耗。
(三)接口适配器实现方式(缺省适配器模式)
- 代码示例
假设我们有一个新的事件监听器接口
EventListener
,包含多个方法:
// 事件监听器接口
interface EventListener {
void onMouseEnter();
void onMouseLeave();
void onClick();
void onDoubleClick();
}
但在实际应用中,我们可能只关心其中的部分方法,例如只关心 onClick
方法。使用接口适配器模式,我们可以创建一个抽象的适配器类 AbstractEventListener
:
// 抽象事件监听器适配器类
abstract class AbstractEventListener implements EventListener {
@Override
public void onMouseEnter() {}
@Override
public void onMouseLeave() {}
@Override
public void onClick() {}
@Override
public void onDoubleClick() {}
}
然后具体的监听器类可以继承这个抽象适配器类,只实现自己关心的方法:
// 具体的点击事件监听器类
class ClickEventListener extends AbstractEventListener {
@Override
public void onClick() {
System.out.println("处理点击事件");
}
}
在客户端代码中使用:
public class Client3 {
public static void main(String[] args) {
EventListener clickListener = new ClickEventListener();
// 这里可以根据具体场景触发相应事件,这里假设触发点击事件
clickListener.onClick();
}
}
- 接口适配器特点
- 优点:对于一个接口中有大量方法,但客户端只需要使用其中部分方法的场景非常适用。它通过抽象类提供了缺省实现,简化了客户端代码,避免了客户端实现不需要的方法。
- 缺点:如果接口方法过多且变化频繁,维护抽象适配器类的缺省实现可能会变得复杂。
四、适配器模式在旧系统不同模块中的应用实例
(一)数据访问层适配
- 场景描述 在一个旧的企业资源规划(ERP)系统中,数据访问层使用 JDBC 直接操作数据库,并且数据库连接的获取和操作方法都是基于早期的 JDBC 规范。随着业务发展,需要引入新的数据库连接池技术,如 HikariCP,以提高数据库访问性能。但新的连接池提供的接口与旧的 JDBC 操作接口不兼容。
- 适配实现
- 旧的数据库访问类:
// 旧的数据库访问类
class OldDatabaseAccess {
private Connection connection;
public OldDatabaseAccess() {
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/erp", "root", "password");
} catch (SQLException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public ResultSet executeQuery(String sql) {
try {
Statement statement = connection.createStatement();
return statement.executeQuery(sql);
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
- 新的数据访问接口:
// 新的数据访问接口
interface NewDatabaseAccess {
ResultSet execute(String sql);
}
- 适配器类:
// 数据库访问适配器类
class DatabaseAccessAdapter implements NewDatabaseAccess {
private OldDatabaseAccess oldDatabaseAccess;
public DatabaseAccessAdapter(OldDatabaseAccess oldDatabaseAccess) {
this.oldDatabaseAccess = oldDatabaseAccess;
}
@Override
public ResultSet execute(String sql) {
return oldDatabaseAccess.executeQuery(sql);
}
}
在新的业务代码中,可以使用新的数据访问接口,通过适配器调用旧的数据库访问功能:
public class NewBusinessLogic {
public static void main(String[] args) {
OldDatabaseAccess oldAccess = new OldDatabaseAccess();
NewDatabaseAccess newAccess = new DatabaseAccessAdapter(oldAccess);
ResultSet resultSet = newAccess.execute("SELECT * FROM employees");
try {
while (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(二)业务逻辑层适配
- 场景描述 在一个旧的电子商务系统中,订单处理业务逻辑是基于传统的面向过程编程方式实现的。随着业务复杂度的增加,需要将订单处理逻辑重构为面向对象的方式,并且要符合新的业务规则,如订单状态的精细化管理。但原有的订单处理逻辑代码庞大,不能轻易废弃。
- 适配实现
- 旧的订单处理函数:
// 旧的订单处理函数
class OldOrderProcessor {
public void processOldOrder(String orderInfo) {
System.out.println("使用旧逻辑处理订单: " + orderInfo);
// 这里省略具体的旧订单处理逻辑
}
}
- 新的订单处理接口:
// 新的订单处理接口
interface NewOrderProcessor {
void processOrder(Order order);
}
// 新的订单类
class Order {
private String orderInfo;
private String orderStatus;
// 省略构造函数、getter 和 setter 方法
}
- 适配器类:
// 订单处理适配器类
class OrderProcessorAdapter implements NewOrderProcessor {
private OldOrderProcessor oldOrderProcessor;
public OrderProcessorAdapter(OldOrderProcessor oldOrderProcessor) {
this.oldOrderProcessor = oldOrderProcessor;
}
@Override
public void processOrder(Order order) {
oldOrderProcessor.processOldOrder(order.getOrderInfo());
// 这里可以在调用旧逻辑后,根据新业务规则更新订单状态等操作
order.setOrderStatus("PROCESSING");
}
}
在新的业务流程中,可以使用新的订单处理接口,通过适配器调用旧的订单处理逻辑:
public class NewOrderBusiness {
public static void main(String[] args) {
OldOrderProcessor oldProcessor = new OldOrderProcessor();
NewOrderProcessor newProcessor = new OrderProcessorAdapter(oldProcessor);
Order order = new Order("订单详情", "NEW");
newProcessor.processOrder(order);
System.out.println("订单状态: " + order.getOrderStatus());
}
}
(三)表示层适配
- 场景描述 在一个旧的 Web 应用中,页面展示是基于 JSP 和 Servlet 技术,并且前端页面的渲染逻辑与后端数据交互紧密耦合。随着前端技术的发展,需要引入新的前端框架,如 Vue.js,实现前后端分离。但旧的后端接口不能直接被新的前端框架调用。
- 适配实现
- 旧的 Servlet 接口:
// 旧的 Servlet 类
@WebServlet("/oldData")
public class OldDataServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 这里获取旧的数据并返回给前端
String oldData = "旧数据内容";
response.getWriter().println(oldData);
}
}
- 新的 API 接口:
// 新的 API 接口
interface NewDataAPI {
String getData();
}
- 适配器类:
// 数据适配类
class DataAdapter implements NewDataAPI {
private OldDataServlet oldDataServlet;
public DataAdapter(OldDataServlet oldDataServlet) {
this.oldDataServlet = oldDataServlet;
}
@Override
public String getData() {
// 模拟调用旧的 Servlet 获取数据
// 实际中可能需要更复杂的 Http 调用逻辑
return "模拟从旧 Servlet 获取的数据";
}
}
在新的前端(Vue.js 示例)中,可以通过调用新的 API 接口,通过适配器获取旧的数据:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF - 8">
<title>新前端获取旧数据</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
{{ data }}
</div>
<script>
const axios = require('axios');
new Vue({
el: '#app',
data: {
data: ''
},
mounted() {
axios.get('/newDataAPI')
.then(response => {
this.data = response.data;
})
.catch(error => {
console.error(error);
});
}
});
</script>
</body>
</html>
在后端需要有一个新的 Servlet 或 Spring Boot 等框架的 Controller 来暴露新的 API 接口,在其中使用适配器获取数据:
// 新的 Servlet 暴露新 API
@WebServlet("/newDataAPI")
public class NewDataAPIServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
OldDataServlet oldServlet = new OldDataServlet();
NewDataAPI dataAPI = new DataAdapter(oldServlet);
String data = dataAPI.getData();
response.getWriter().println(data);
}
}
五、适配器模式在旧系统扩展中的注意事项
(一)性能考虑
- 调用开销:在对象适配器模式中,由于是通过组合方式调用适配者方法,每次调用可能会有一定的方法调用开销。例如,在频繁调用数据库访问方法时,这种开销可能会对系统性能产生一定影响。在设计适配器时,如果性能要求较高,可以考虑缓存适配者的某些结果,减少不必要的重复调用。
- 资源占用:适配器类本身可能会占用一定的内存和系统资源。特别是在类适配器模式中,如果适配器类继承了适配者类,可能会导致类的继承体系变得复杂,增加内存开销。因此,在使用适配器模式时,要对适配器类的资源占用进行评估,避免因适配器过多而导致系统资源紧张。
(二)适配者变化的影响
- 接口变化:如果适配者类的接口发生变化,可能会影响适配器的正常工作。例如,适配者类的方法参数或返回值类型改变,适配器类需要相应地进行修改。为了降低这种影响,可以在适配器类和适配者类之间增加一层抽象,使得适配器依赖于抽象而不是具体的适配者类,这样当适配者类发生变化时,只需要修改适配者类的具体实现,而适配器类可以保持相对稳定。
- 功能变化:当适配者类的功能发生变化时,适配器类也需要进行调整。比如,适配者类中某个方法的业务逻辑改变,适配器类可能需要重新实现目标接口的方法,以保证适配后的功能符合预期。在这种情况下,要对适配器类和相关的业务代码进行全面的测试,确保功能的正确性和稳定性。
(三)与其他设计模式的结合使用
- 策略模式:在某些场景下,可以将适配器模式与策略模式结合使用。例如,在旧系统中存在多种不同的支付方式适配,每种支付方式的适配者类不同,但都需要适配到统一的支付接口。可以将不同的支付适配器看作是策略模式中的不同策略,通过一个上下文类来动态选择使用哪种支付适配器,从而实现更加灵活的支付功能扩展。
- 装饰器模式:适配器模式和装饰器模式也可以协同工作。当需要在适配旧系统功能的同时,为其添加一些额外的功能时,可以先使用适配器模式将旧系统功能适配到新接口,然后再使用装饰器模式为适配后的功能添加装饰。比如,在适配旧的文件上传功能到新的接口后,使用装饰器模式为其添加文件大小限制、文件类型校验等额外功能。