一、前言
在设计模式中,结构型设计模式处理类或对象组合,可助力构建灵活、可维护软件结构。此前探讨过组合模式(将对象组合成树形结构,统一处理单个与组合对象,如文件系统管理)和装饰器模式(动态给对象添加职责,不改变结构增强功能,如 Java I/O 中BufferedInputStream
装饰FileInputStream
)。
但系统规模扩大后,多子系统相互依赖,客户端直接交互复杂,如电商下单涉及多子系统,增加开发、维护和扩展难度,耦合度高。
针对这些痛点,外观模式应运而生,为子系统提供统一简单接口,客户端只与外观接口交互,降低耦合度,简化系统使用。本文将深入探讨外观模式原理、应用场景,通过项目代码示例,助大家掌握运用外观模式构建高效可维护系统架构的方法。
二、外观模式原型设计及说明
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。
因此他主要的作用是降低客户端与复杂子系统之间的耦合度以及简化客户端对复杂系统的操作,隐藏内部实现细节。
我们先通过一个简单的 UML 图来直观地了解外观模式的结构。使用 PlantUML 代码绘制的外观模式 UML 图如下:
这里我们可以看到外观模式涉及以下核心角色:
✅外观(Facade):提供一个简化的接口,封装了系统的复杂性。外观模式的客户端通过与外观对象交互,而无需直接与系统的各个组件打交道。
这里PaymentFacade就是外观类,它提供了一个统一的接口executePayment() ,隐藏了子系统的复杂性。
✅子系统(Subsystem):由多个相互关联的类组成,负责系统的具体功能。外观对象通过调用这些子系统来完成客户端的请求。
对应的WechatPayAdapter、AlipayAdapter和UnionPayAdapter是子系统类,它们分别实现了具体的支付功能。
✅客户端(Client):使用外观对象来与系统交互,而不需要了解系统内部的具体实现。对应图中的Client救赎代表客户端,是使用系统功能的对象。
从图中可以清晰地看到,客户端只与PaymentFacade发生依赖关系,而PaymentFacade则与各个子系统类存在关联关系。这种结构使得客户端无需了解子系统的具体实现,只需要通过PaymentFacade的接口就能完成支付操作,大大降低了客户端与子系统之间的耦合度 。
就像我们使用电脑时,只需要按下开机按钮(相当于外观接口),而无需了解电脑内部 CPU、内存、硬盘等组件(相当于子系统)是如何协同工作的。
三、外观模式的适用场景与最佳实践
在对外观模式的设计原型进行全面了解后可以发现,当客户端无需知悉系统内部的复杂逻辑以及组件间的交互关系,或者需要为整个系统设定一个明确的入口点时,外观模式具有显著的有效性。因此,在以下场景中,我们均可合理运用外观模式:
(一)三大典型应用场景详解
复杂模块整合:在大型企业级应用中,系统往往由多个复杂的模块组成,如电商系统中的订单管理、库存管理、支付系统、物流配送等模块。这些模块之间相互依赖、交互复杂,如果客户端直接与这些模块进行交互,会导致代码复杂度过高,维护困难。外观模式可以为这些复杂的模块提供一个统一的接口,将模块间的复杂交互封装起来,使得客户端只需要与外观接口交互,大大降低了客户端的使用难度和代码复杂度。
例如,在一个在线教育平台中,课程购买功能涉及到课程信息查询、用户信息验证、支付处理、订单生成等多个模块。假设我们有课程模块CourseModule、用户模块UserModule、支付模块PaymentModule和订单模块OrderModule,具体代码如下:
// 课程模块
public class CourseModule {
public void queryCourseInfo(String courseId) {
System.out.println("查询课程信息,课程ID:" + courseId);
}
}
// 用户模块
public class UserModule {
public void validateUser(String userId) {
System.out.println("验证用户信息,用户ID:" + userId);
}
}
// 支付模块
public class PaymentModule {
public void processPayment(double amount) {
System.out.println("处理支付,金额:" + amount);
}
}
// 订单模块
public class OrderModule {
public void generateOrder(String userId, String courseId) {
System.out.println("生成订单,用户ID:" + userId + ",课程ID:" + courseId);
}
}
// 外观类
public class CoursePurchaseFacade {
private CourseModule courseModule;
private UserModule userModule;
private PaymentModule paymentModule;
private OrderModule orderModule;
public CoursePurchaseFacade() {
this.courseModule = new CourseModule();
this.userModule = new UserModule();
this.paymentModule = new PaymentModule();
this.orderModule = new OrderModule();
}
public void purchaseCourse(String userId, String courseId, double amount) {
courseModule.queryCourseInfo(courseId);
userModule.validateUser(userId);
paymentModule.processPayment(amount);
orderModule.generateOrder(userId, courseId);
}
}
客户端只需要调用CoursePurchaseFacade.purchaseCourse()方法,就可以完成课程购买的全部操作,而无需了解各个模块的具体实现细节 。
public class Client {
public static void main(String[] args) {
CoursePurchaseFacade facade = new CoursePurchaseFacade();
facade.purchaseCourse("user1", "course1", 99.9);
}
}
分层架构:在分层架构中,各层之间的交互也可能变得复杂。外观模式可以在层与层之间提供一个统一的接口,简化层间的依赖关系。以经典的 MVC 架构为例,在控制器层和服务层之间,使用外观模式可以将服务层的多个业务方法封装成一个统一的接口提供给控制器层调用。
比如在一个用户管理模块中,服务层可能有UserService负责用户信息的增删改查,RoleService负责角色管理等多个服务类。
// 用户服务类
public class UserService {
public void addUser(String user) {
System.out.println("添加用户:" + user);
}
public void deleteUser(String userId) {
System.out.println("删除用户,用户ID:" + userId);
}
}
// 角色服务类
public class RoleService {
public void assignRole(String userId, String role) {
System.out.println("为用户 " + userId + " 分配角色:" + role);
}
}
// 外观类
public class UserFacade {
private UserService userService;
private RoleService roleService;
public UserFacade() {
this.userService = new UserService();
this.roleService = new RoleService();
}
public void addUserAndAssignRole(String user, String userId, String role) {
userService.addUser(user);
roleService.assignRole(userId, role);
}
}
控制器层只需要与UserFacade交互,而不需要直接调用多个服务类,使得各层之间的接口更加清晰,降低了层与层之间的耦合度 。
// 模拟控制器层
public class Controller {
public static void main(String[] args) {
UserFacade userFacade = new UserFacade();
userFacade.addUserAndAssignRole("newUser", "user1", "admin");
}
}
遗留系统封装:当企业需要对遗留系统进行改造或与新系统集成时,遗留系统的复杂接口和内部实现可能会成为障碍。外观模式可以为遗留系统提供一个新的简单接口,隐藏遗留系统的复杂性,使得新系统可以方便地与遗留系统进行交互。
例如,一个企业的老财务系统接口复杂且文档不全,新的业务系统需要调用财务系统的部分功能。假设老财务系统有一个复杂的方法oldCalculateTotal用于计算财务总额,代码如下(简化示意):
// 老财务系统类
public class OldFinanceSystem {
public double oldCalculateTotal() {
// 复杂的计算逻辑
return 1000.0;
}
}
通过创建一个FinanceFacade外观类,封装对老财务系统的调用:
// 外观类
public class FinanceFacade {
private OldFinanceSystem oldFinanceSystem;
public FinanceFacade() {
this.oldFinanceSystem = new OldFinanceSystem();
}
public double calculateTotal() {
return oldFinanceSystem.oldCalculateTotal();
}
}
新业务系统只需要调用FinanceFacade的接口,就可以实现与老财务系统的交互,而不需要深入了解老财务系统的内部结构和接口细节 。如下是新业务系统调用示例:
// 新业务系统
public class NewBusinessSystem {
public static void main(String[] args) {
FinanceFacade financeFacade = new FinanceFacade();
double total = financeFacade.calculateTotal();
System.out.println("财务总额:" + total);
}
}
(二)Spring JdbcTemplate 案例分析
Spring 框架中的 JdbcTemplate 是外观模式的一个典型应用。它作为门面封装了 JDBC API,隐藏了 JDBC 复杂的操作细节,为开发者提供了更加便捷、简单的数据库访问方式。
在传统的 JDBC 操作中,开发者需要手动管理数据库连接、创建Statement或PreparedStatement、执行 SQL 语句、处理结果集等,代码繁琐且容易出错。例如:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class TraditionalJdbcExample {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
// 创建Statement
statement = connection.createStatement();
// 执行SQL
resultSet = statement.executeQuery("SELECT * FROM user");
while (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (resultSet != null) resultSet.close();
if (statement != null) statement.close();
if (connection != null) connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
而使用 JdbcTemplate 后,这些复杂的操作都被封装起来,开发者只需要关注 SQL 语句和业务逻辑即可。例如:
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.util.List;
import java.util.Map;
public class JdbcTemplateExample {
public static void main(String[] args) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("password");
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
String sql = "SELECT * FROM user";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
for (Map<String, Object> map : list) {
System.out.println(map.get("name"));
}
}
}
这里JdbcTemplate就是外观类,它封装了DriverManagerDataSource(数据源,相当于子系统)获取连接等操作,以及Statement的创建和执行等细节。开发者通过调用JdbcTemplate的queryForList方法,就可以轻松地执行 SQL 查询并获取结果,大大提高了开发效率和代码的简洁性。
(三)实际开发中的应用建议
⚠️避免过度抽象:在使用外观模式时,要注意避免过度抽象。虽然外观模式可以简化接口,但如果抽象过度,可能会导致外观接口变得不实用,无法满足实际业务需求。
例如,在设计支付外观接口时,如果将所有支付渠道的差异都抽象掉,只提供一个非常通用的支付方法,可能会导致在处理某些特殊支付场景时无法满足需求。因此,在设计外观接口时,要根据实际业务需求,合理抽象,保留必要的灵活性 。
⚠️关注接口稳定性:外观模式提供的接口是客户端与子系统交互的桥梁,因此接口的稳定性非常重要。一旦外观接口确定,应尽量避免频繁修改,否则会影响到客户端的使用。如果子系统发生变化,应尽量通过内部调整来适应,而不是直接修改外观接口。
当支付渠道的接口发生变化时,可以在支付适配器中进行适配,而不是修改支付外观接口,以保证接口的稳定性和兼容性 。
⚠️采取性能隔离策略:在企业级应用中,子系统的性能可能会有所差异。为了防止某个子系统的性能问题影响到整个系统的性能,应采取性能隔离策略。
比如可以为每个子系统设置独立的线程池或资源池,当某个子系统出现性能瓶颈时,不会影响到其他子系统的正常运行。在支付系统中,微信支付、支付宝支付等子系统可以分别使用独立的线程池来处理支付请求,避免因某个支付渠道的高并发请求导致整个支付系统的性能下降 。
四、外观模式的简易使用:统一支付网关设计
(一)业务场景描述
在当今的电商和在线支付领域,支付方式的多样性给用户带来了极大的便利,但同时也给开发者带来了不小的挑战。微信支付、支付宝支付和银联支付作为主流的支付方式,各自有着不同的接口规范、参数要求和业务流程。
例如,微信支付在发起支付请求时,需要特定的appId、timeStamp、nonceStr等参数,并且签名算法也有其独特的规则;支付宝支付则需要app_id、method、format等参数,签名方式也与微信支付不同;银联支付的接口规范更为复杂,涉及到更多的业务字段和交互流程 。
当一个电商系统需要支持多种支付方式时,如果直接在业务代码中分别调用各个支付渠道的接口,代码会变得异常复杂,维护和扩展也会变得困难重重。不同支付渠道的接口变化、升级都可能导致大量的代码修改,而且各个支付渠道的错误处理、结果回调等逻辑也需要分别处理,这无疑增加了开发的难度和风险 。
因此,我们迫切需要一种设计模式来简化这种复杂的调用,外观模式正是解决这一问题的关键。通过设计一个统一的支付网关,将各个支付渠道的差异封装起来,为业务系统提供一个简单、统一的支付接口,使得业务系统可以方便地集成多种支付方式,同时降低了系统的耦合度和维护成本 。
(二)核心业务实现
在我们的测试项目中,统一支付网关的代码结构主要由PaymentFacade类和各支付渠道的Adapter实现类组成。
PaymentFacade类是整个支付网关的核心,它承担着聚合支付请求和路由到具体支付渠道的重要职责。比如:
@Service
public class PaymentFacade {
@Autowired
private WechatPayAdapter wechatPay;
@Autowired
private AlipayAdapter alipay;
// 支付方法,处理支付请求
public PaymentResult pay(PaymentRequest request) {
// 解析支付渠道
PaymentChannel channel = resolveChannel(request);
// 根据支付渠道进行路由
switch (channel) {
case WECHAT:
return wechatPay.process(request);
case ALIPAY:
return alipay.process(request);
default:
throw new PaymentException("Unsupported channel");
}
}
// 解析支付渠道的方法
private PaymentChannel resolveChannel(PaymentRequest request) {
// 根据请求中的支付渠道信息进行解析
return request.getChannel();
}
}
这里我们用pay方法接收一个PaymentRequest对象,首先通过resolveChannel方法解析出支付渠道,然后根据不同的支付渠道调用相应的Adapter实例的process方法来处理支付请求 。
这种设计使得支付逻辑清晰,易于维护和扩展。当需要添加新的支付渠道时,只需要添加一个新的Adapter实现类,并在PaymentFacade中注入该实例,然后在pay方法中添加相应的路由逻辑即可。
各支付渠道的Adapter实现类,如WechatPayAdapter、AlipayAdapter等,负责适配不同支付接口。它们实现了统一的支付接口,将各个支付渠道的特定接口和业务逻辑封装起来。以WechatPayAdapter为例:
@Component
public class WechatPayAdapter {
// 处理微信支付请求的方法
public PaymentResult process(PaymentRequest request) {
// 构建微信支付所需的参数
Map<String, String> params = buildWechatPayParams(request);
// 调用微信支付接口
String result = wechatPayService.pay(params);
// 处理支付结果
return handleWechatPayResult(result);
}
// 构建微信支付参数的方法
private Map<String, String> buildWechatPayParams(PaymentRequest request) {
// 根据PaymentRequest构建微信支付所需的参数
Map<String, String> params = new HashMap<>();
params.put("appId", request.getAppId());
params.put("timeStamp", request.getTimeStamp());
// 其他参数构建
return params;
}
// 处理微信支付结果的方法
private PaymentResult handleWechatPayResult(String result) {
// 解析微信支付结果,封装成统一的PaymentResult返回
PaymentResult paymentResult = new PaymentResult();
if ("success".equals(result)) {
paymentResult.setStatus(PaymentStatus.SUCCESS);
} else {
paymentResult.setStatus(PaymentStatus.FAILURE);
}
return paymentResult;
}
}
在这个WechatPayAdapter类中,process方法首先通过buildWechatPayParams方法构建微信支付所需的参数,然后调用微信支付服务wechatPayService.pay来发起支付请求,最后通过handleWechatPayResult方法处理支付结果,并将其封装成统一的PaymentResult对象返回 。
这样,PaymentFacade类无需关心微信支付的具体实现细节,只需要调用WechatPayAdapter的process方法即可完成微信支付操作,实现了不同支付渠道的解耦和封装 。
(三)功能测试
这里我们在单元测试中,通过 Mock 对象替换实际依赖,我们可以更专注地测试PaymentFacade类的业务逻辑。以测试PaymentFacade的pay方法为例,使用 Mock 框架(如 Mockito)创建WechatPayAdapter和AlipayAdapter的 Mock 对象,并注入到PaymentFacade中,然后对pay方法进行测试,即可验证其逻辑的正确性。
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
public class PaymentFacadeTest {
@MockBean
private WechatPayAdapter wechatPay;
@MockBean
private AlipayAdapter alipay;
@Autowired
private PaymentFacade paymentFacade;
@Test
public void testPayWithWechat() {
PaymentRequest request = new PaymentRequest();
request.setChannel(PaymentChannel.WECHAT);
PaymentResult mockResult = new PaymentResult();
Mockito.when(wechatPay.process(request)).thenReturn(mockResult);
PaymentResult result = paymentFacade.pay(request);
assertEquals(mockResult, result);
}
@Test
public void testPayWithAlipay() {
PaymentRequest request = new PaymentRequest();
request.setChannel(PaymentChannel.ALIPAY);
PaymentResult mockResult = new PaymentResult();
Mockito.when(alipay.process(request)).thenReturn(mockResult);
PaymentResult result = paymentFacade.pay(request);
assertEquals(mockResult, result);
}
}
五、总结
外观模式作为一种强大的结构型设计模式,在软件开发中具有不可忽视的核心价值。它通过提供统一接口,将复杂的子系统封装起来,极大地降低了客户端与子系统交互的难度。就像我们使用智能手机时,无需了解手机内部复杂的硬件和软件架构,只需通过简单的操作界面(外观接口)就能完成各种功能,如打电话、发短信、浏览网页等 。
从依赖解耦的角度来看,外观模式使得客户端与子系统之间的依赖关系变得松散。客户端只依赖于外观类,而不依赖于具体的子系统实现。这意味着当子系统发生变化时,只要外观类的接口不变,客户端代码就无需修改,提高了系统的可维护性和扩展性 。
此外,外观模式还提升了系统的架构整洁度。它将复杂的子系统交互逻辑封装在外观类中,使得系统的结构更加清晰、简洁。各个子系统专注于自身的功能实现,而外观类则负责协调子系统之间的协作,这种分工明确的架构使得系统更易于理解和维护 。
但是外观模式并非适用于所有场景。在需频繁深度定制子系统功能时,因它提供统一简化接口,限制对具体子系统功能的灵活调用,所以可能不适用,比如金融系统支付功能需高度定制化处理的情况。当子系统间耦合过深难以封装时,外观模式也难发挥作用,因其前提是封装子系统复杂性,像遗留系统子系统耦合紧密,需先重构降低耦合度,再考虑用外观模式。
所以希望大家对于设计模式,都要做到依事而定~