每天认识一个设计模式-外观模式:化繁为简的接口魔法 ...

打印 上一主题 下一主题

主题 1732|帖子 1732|积分 5196

一、前言

在设计模式中,布局型设计模式处置处罚类或对象组合,可助力构建灵活、可维护软件布局。此前探讨过组合模式(将对象组合成树形布局,统一处置处罚单个与组合对象,如文件系统管理)和装饰器模式(动态给对象添加职责,不改变布局增强功能,如 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,具体代码如下:
  1. // 课程模块
  2. public class CourseModule {
  3.     public void queryCourseInfo(String courseId) {
  4.         System.out.println("查询课程信息,课程ID:" + courseId);
  5.     }
  6. }
  7. // 用户模块
  8. public class UserModule {
  9.     public void validateUser(String userId) {
  10.         System.out.println("验证用户信息,用户ID:" + userId);
  11.     }
  12. }
  13. // 支付模块
  14. public class PaymentModule {
  15.     public void processPayment(double amount) {
  16.         System.out.println("处理支付,金额:" + amount);
  17.     }
  18. }
  19. // 订单模块
  20. public class OrderModule {
  21.     public void generateOrder(String userId, String courseId) {
  22.         System.out.println("生成订单,用户ID:" + userId + ",课程ID:" + courseId);
  23.     }
  24. }
  25. // 外观类
  26. public class CoursePurchaseFacade {
  27.     private CourseModule courseModule;
  28.     private UserModule userModule;
  29.     private PaymentModule paymentModule;
  30.     private OrderModule orderModule;
  31.     public CoursePurchaseFacade() {
  32.         this.courseModule = new CourseModule();
  33.         this.userModule = new UserModule();
  34.         this.paymentModule = new PaymentModule();
  35.         this.orderModule = new OrderModule();
  36.     }
  37.     public void purchaseCourse(String userId, String courseId, double amount) {
  38.         courseModule.queryCourseInfo(courseId);
  39.         userModule.validateUser(userId);
  40.         paymentModule.processPayment(amount);
  41.         orderModule.generateOrder(userId, courseId);
  42.     }
  43. }
复制代码
客户端只需要调用CoursePurchaseFacade.purchaseCourse()方法,就可以完成课程购买的全部操作,而无需相识各个模块的具体实现细节 。
  1. public class Client {
  2.     public static void main(String[] args) {
  3.         CoursePurchaseFacade facade = new CoursePurchaseFacade();
  4.         facade.purchaseCourse("user1", "course1", 99.9);
  5.     }
  6. }
复制代码
分层架构:在分层架构中,各层之间的交互也大概变得复杂。外观模式可以在层与层之间提供一个统一的接口,简化层间的依靠关系。以经典的 MVC 架构为例,在控制器层和服务层之间,使用外观模式可以将服务层的多个业务方法封装成一个统一的接口提供给控制器层调用。​
比如在一个用户管理模块中,服务层大概有UserService负责用户信息的增删改查,RoleService负责脚色管理等多个服务类。
  1. // 用户服务类
  2. public class UserService {
  3.     public void addUser(String user) {
  4.         System.out.println("添加用户:" + user);
  5.     }
  6.     public void deleteUser(String userId) {
  7.         System.out.println("删除用户,用户ID:" + userId);
  8.     }
  9. }
  10. // 角色服务类
  11. public class RoleService {
  12.     public void assignRole(String userId, String role) {
  13.         System.out.println("为用户 " + userId + " 分配角色:" + role);
  14.     }
  15. }
  16. // 外观类
  17. public class UserFacade {
  18.     private UserService userService;
  19.     private RoleService roleService;
  20.     public UserFacade() {
  21.         this.userService = new UserService();
  22.         this.roleService = new RoleService();
  23.     }
  24.     public void addUserAndAssignRole(String user, String userId, String role) {
  25.         userService.addUser(user);
  26.         roleService.assignRole(userId, role);
  27.     }
  28. }
复制代码
控制器层只需要与UserFacade交互,而不需要直接调用多个服务类,使得各层之间的接口更加清晰,降低了层与层之间的耦合度 。
  1. // 模拟控制器层
  2. public class Controller {
  3.     public static void main(String[] args) {
  4.         UserFacade userFacade = new UserFacade();
  5.         userFacade.addUserAndAssignRole("newUser", "user1", "admin");
  6.     }
  7. }
复制代码
遗留系统封装:当企业需要对遗留系统举行改造或与新系统集成时,遗留系统的复杂接口和内部实现大概会成为障碍。外观模式可以为遗留系统提供一个新的简单接口,隐藏遗留系统的复杂性,使得新系统可以方便地与遗留系统举行交互。​
比方,一个企业的老财务系统接口复杂且文档不全,新的业务系统需要调用财务系统的部分功能。假设老财务系统有一个复杂的方法oldCalculateTotal用于计算财务总额,代码如下(简化示意):
  1. // 老财务系统类
  2. public class OldFinanceSystem {
  3.     public double oldCalculateTotal() {
  4.         // 复杂的计算逻辑
  5.         return 1000.0;
  6.     }
  7. }
复制代码
 通过创建一个FinanceFacade外观类,封装对老财务系统的调用:
  1. // 外观类
  2. public class FinanceFacade {
  3.     private OldFinanceSystem oldFinanceSystem;
  4.     public FinanceFacade() {
  5.         this.oldFinanceSystem = new OldFinanceSystem();
  6.     }
  7.     public double calculateTotal() {
  8.         return oldFinanceSystem.oldCalculateTotal();
  9.     }
  10. }
复制代码
新业务系统只需要调用FinanceFacade的接口,就可以实现与老财务系统的交互,而不需要深入相识老财务系统的内部布局和接口细节 。如下是新业务系统调用示例:
  1. // 新业务系统
  2. public class NewBusinessSystem {
  3.     public static void main(String[] args) {
  4.         FinanceFacade financeFacade = new FinanceFacade();
  5.         double total = financeFacade.calculateTotal();
  6.         System.out.println("财务总额:" + total);
  7.     }
  8. }
复制代码
(二)Spring JdbcTemplate 案例分析​

Spring 框架中的 JdbcTemplate 是外观模式的一个典型应用。它作为门面封装了 JDBC API,隐藏了 JDBC 复杂的操作细节,为开辟者提供了更加便捷、简单的数据库访问方式。​
在传统的 JDBC 操作中,开辟者需要手动管理数据库毗连、创建Statement或PreparedStatement、执行 SQL 语句、处置处罚结果集等,代码繁琐且轻易堕落。比方:
  1. import java.sql.Connection;
  2. import java.sql.DriverManager;
  3. import java.sql.ResultSet;
  4. import java.sql.Statement;
  5. public class TraditionalJdbcExample {
  6.     public static void main(String[] args) {
  7.         Connection connection = null;
  8.         Statement statement = null;
  9.         ResultSet resultSet = null;
  10.         try {
  11.             // 加载驱动
  12.             Class.forName("com.mysql.jdbc.Driver");
  13.             // 获取连接
  14.             connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
  15.             // 创建Statement
  16.             statement = connection.createStatement();
  17.             // 执行SQL
  18.             resultSet = statement.executeQuery("SELECT * FROM user");
  19.             while (resultSet.next()) {
  20.                 System.out.println(resultSet.getString("name"));
  21.             }
  22.         } catch (Exception e) {
  23.             e.printStackTrace();
  24.         } finally {
  25.             // 关闭资源
  26.             try {
  27.                 if (resultSet != null) resultSet.close();
  28.                 if (statement != null) statement.close();
  29.                 if (connection != null) connection.close();
  30.             } catch (Exception e) {
  31.                 e.printStackTrace();
  32.             }
  33.         }
  34.     }
  35. }
复制代码
 而使用 JdbcTemplate 后,这些复杂的操作都被封装起来,开辟者只需要关注 SQL 语句和业务逻辑即可。比方:
  1. import org.springframework.jdbc.core.JdbcTemplate;
  2. import org.springframework.jdbc.datasource.DriverManagerDataSource;
  3. import java.util.List;
  4. import java.util.Map;
  5. public class JdbcTemplateExample {
  6.     public static void main(String[] args) {
  7.         DriverManagerDataSource dataSource = new DriverManagerDataSource();
  8.         dataSource.setDriverClassName("com.mysql.jdbc.Driver");
  9.         dataSource.setUrl("jdbc:mysql://localhost:3306/test");
  10.         dataSource.setUsername("root");
  11.         dataSource.setPassword("password");
  12.         JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  13.         String sql = "SELECT * FROM user";
  14.         List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
  15.         for (Map<String, Object> map : list) {
  16.             System.out.println(map.get("name"));
  17.         }
  18.     }
  19. }
复制代码
这里JdbcTemplate就是外观类,它封装了DriverManagerDataSource(数据源,相当于子系统)获取毗连等操作,以及Statement的创建和执行等细节。开辟者通过调用JdbcTemplate的queryForList方法,就可以轻松地执行 SQL 查询并获取结果,大大进步了开辟效率和代码的简洁性。
(三)实际开辟中的应用建议​

⚠️避免过度抽象:在使用外观模式时,要注意避免过度抽象。虽然外观模式可以简化接口,但如果抽象过度,大概会导致外观接口变得不实用,无法满足实际业务需求。
比方,在设计付出外观接口时,如果将全部付出渠道的差别都抽象掉,只提供一个非常通用的付出方法,大概会导致在处置处罚某些特殊付出场景时无法满足需求。因此,在设计外观接口时,要根据实际业务需求,公道抽象,生存须要的灵活性 。​
⚠️关注接口稳固性:外观模式提供的接口是客户端与子系统交互的桥梁,因此接口的稳固性非常紧张。一旦外观接口确定,应只管避免频繁修改,否则会影响到客户端的使用。如果子系统发生变化,应只管通过内部调整来顺应,而不是直接修改外观接口。
当付出渠道的接口发生变化时,可以在付出适配器中举行适配,而不是修改付出外观接口,以保证接口的稳固性和兼容性 。​
⚠️采取性能隔离策略:在企业级应用中,子系统的性能大概会有所差别。为了防止某个子系统的性能问题影响到整个系统的性能,应采取性能隔离策略。
比如可以为每个子系统设置独立的线程池或资源池,当某个子系统出现性能瓶颈时,不会影响到其他子系统的正常运行。在付出系统中,微信付出、付出宝付出等子系统可以分别使用独立的线程池来处置处罚付出请求,避免因某个付出渠道的高并发请求导致整个付出系统的性能下降 。
四、外观模式的浅显使用:统一付出网关设计 

(一)业务场景描述​

在当今的电商和在线付出范畴,付出方式的多样性给用户带来了极大的便利,但同时也给开辟者带来了不小的挑衅。微信付出、付出宝付出和银联付出作为主流的付出方式,各自有着不同的接口规范、参数要求和业务流程。
比方,微信付出在发起付出请求时,需要特定的appId、timeStamp、nonceStr等参数,并且签名算法也有其独特的规则;付出宝付出则需要app_id、method、format等参数,签名方式也与微信付出不同;银联付出的接口规范更为复杂,涉及到更多的业务字段和交互流程 。​
当一个电商系统需要支持多种付出方式时,如果直接在业务代码中分别调用各个付出渠道的接口,代码会变得非常复杂,维护和扩展也会变得困难重重。不同付出渠道的接口变化、升级都大概导致大量的代码修改,而且各个付出渠道的错误处置处罚、结果回调等逻辑也需要分别处置处罚,这无疑增加了开辟的难度和风险 。
因此,我们急迫需要一种设计模式来简化这种复杂的调用,外观模式正是办理这一问题的关键。通过设计一个统一的付出网关,将各个付出渠道的差别封装起来,为业务系统提供一个简单、统一的付出接口,使得业务系统可以方便地集成多种付出方式,同时降低了系统的耦合度和维护本钱 。
(二)核心业务实现

在我们的测试项目中,统一付出网关的代码布局主要由PaymentFacade类和各付出渠道的Adapter实现类组成。​
PaymentFacade类是整个付出网关的核心,它负担着聚合付出请求和路由到具体付出渠道的紧张职责。比如:
  1. @Service
  2. public class PaymentFacade {
  3.     @Autowired
  4.     private WechatPayAdapter wechatPay;
  5.     @Autowired
  6.     private AlipayAdapter alipay;
  7.     // 支付方法,处理支付请求
  8.     public PaymentResult pay(PaymentRequest request) {
  9.         // 解析支付渠道
  10.         PaymentChannel channel = resolveChannel(request);
  11.         // 根据支付渠道进行路由
  12.         switch (channel) {
  13.             case WECHAT:
  14.                 return wechatPay.process(request);
  15.             case ALIPAY:
  16.                 return alipay.process(request);
  17.             default:
  18.                 throw new PaymentException("Unsupported channel");
  19.         }
  20.     }
  21.     // 解析支付渠道的方法
  22.     private PaymentChannel resolveChannel(PaymentRequest request) {
  23.         // 根据请求中的支付渠道信息进行解析
  24.         return request.getChannel();
  25.     }
  26. }
复制代码
 这里我们用pay方法接收一个PaymentRequest对象,首先通过resolveChannel方法剖析出付出渠道,然后根据不同的付出渠道调用相应的Adapter实例的process方法来处置处罚付出请求 。
这种设计使得付出逻辑清晰,易于维护和扩展。当需要添加新的付出渠道时,只需要添加一个新的Adapter实现类,并在PaymentFacade中注入该实例,然后在pay方法中添加相应的路由逻辑即可。
各付出渠道的Adapter实现类,如WechatPayAdapter、AlipayAdapter等,负责适配不同付出接口。它们实现了统一的付出接口,将各个付出渠道的特定接口和业务逻辑封装起来。以WechatPayAdapter为例:
  1. @Component
  2. public class WechatPayAdapter {
  3.     // 处理微信支付请求的方法
  4.     public PaymentResult process(PaymentRequest request) {
  5.         // 构建微信支付所需的参数
  6.         Map<String, String> params = buildWechatPayParams(request);
  7.         // 调用微信支付接口
  8.         String result = wechatPayService.pay(params);
  9.         // 处理支付结果
  10.         return handleWechatPayResult(result);
  11.     }
  12.     // 构建微信支付参数的方法
  13.     private Map<String, String> buildWechatPayParams(PaymentRequest request) {
  14.         // 根据PaymentRequest构建微信支付所需的参数
  15.         Map<String, String> params = new HashMap<>();
  16.         params.put("appId", request.getAppId());
  17.         params.put("timeStamp", request.getTimeStamp());
  18.         // 其他参数构建
  19.         return params;
  20.     }
  21.     // 处理微信支付结果的方法
  22.     private PaymentResult handleWechatPayResult(String result) {
  23.         // 解析微信支付结果,封装成统一的PaymentResult返回
  24.         PaymentResult paymentResult = new PaymentResult();
  25.         if ("success".equals(result)) {
  26.             paymentResult.setStatus(PaymentStatus.SUCCESS);
  27.         } else {
  28.             paymentResult.setStatus(PaymentStatus.FAILURE);
  29.         }
  30.         return paymentResult;
  31.     }
  32. }
复制代码
在这个WechatPayAdapter类中,process方法首先通过buildWechatPayParams方法构建微信付出所需的参数,然后调用微信付出服务wechatPayService.pay来发起付出请求,最后通过handleWechatPayResult方法处置处罚付出结果,并将其封装成统一的PaymentResult对象返回 。
这样,PaymentFacade类无需关心微信付出的具体实现细节,只需要调用WechatPayAdapter的process方法即可完成微信付出操作,实现了不同付出渠道的解耦和封装 。
 (三)功能测试

这里我们在单元测试中,通过 Mock 对象替换实际依靠,我们可以更专注地测试PaymentFacade类的业务逻辑。以测试PaymentFacade的pay方法为例,使用 Mock 框架(如 Mockito)创建WechatPayAdapter和AlipayAdapter的 Mock 对象,并注入到PaymentFacade中,然后对pay方法举行测试,即可验证其逻辑的正确性。
  1. import org.junit.jupiter.api.Test;
  2. import org.mockito.Mock;
  3. import org.mockito.Mockito;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import org.springframework.boot.test.mock.mockito.MockBean;
  7. import static org.junit.jupiter.api.Assertions.assertEquals;
  8. @SpringBootTest
  9. public class PaymentFacadeTest {
  10.     @MockBean
  11.     private WechatPayAdapter wechatPay;
  12.     @MockBean
  13.     private AlipayAdapter alipay;
  14.     @Autowired
  15.     private PaymentFacade paymentFacade;
  16.     @Test
  17.     public void testPayWithWechat() {
  18.         PaymentRequest request = new PaymentRequest();
  19.         request.setChannel(PaymentChannel.WECHAT);
  20.         PaymentResult mockResult = new PaymentResult();
  21.         Mockito.when(wechatPay.process(request)).thenReturn(mockResult);
  22.         PaymentResult result = paymentFacade.pay(request);
  23.         assertEquals(mockResult, result);
  24.     }
  25.     @Test
  26.     public void testPayWithAlipay() {
  27.         PaymentRequest request = new PaymentRequest();
  28.         request.setChannel(PaymentChannel.ALIPAY);
  29.         PaymentResult mockResult = new PaymentResult();
  30.         Mockito.when(alipay.process(request)).thenReturn(mockResult);
  31.         PaymentResult result = paymentFacade.pay(request);
  32.         assertEquals(mockResult, result);
  33.     }
  34. }
复制代码
五、总结

外观模式作为一种强大的布局型设计模式,在软件开辟中具有不可忽视的核心价值。它通过提供统一接口,将复杂的子系统封装起来,极大地降低了客户端与子系统交互的难度。就像我们使用智能手机时,无需相识手机内部复杂的硬件和软件架构,只需通过简单的操作界面(外观接口)就能完成各种功能,如打电话、发短信、浏览网页等 。
从依靠解耦的角度来看,外观模式使得客户端与子系统之间的依靠关系变得松散。客户端只依靠于外观类,而不依靠于具体的子系统实现。这意味着当子系统发生变化时,只要外观类的接口不变,客户端代码就无需修改,进步了系统的可维护性和扩展性 。
此外,外观模式还提拔了系统的架构整齐度。它将复杂的子系统交互逻辑封装在外观类中,使得系统的布局更加清晰、简洁。各个子系统专注于自身的功能实现,而外观类则负责协调子系统之间的协作,这种分工明白的架构使得系统更易于明白和维护 。
但是外观模式并非实用于全部场景。在需频繁深度定制子系统功能时,因它提供统一简化接口,限制对具体子系统功能的灵活调用,所以大概不实用,比如金融系统付出功能需高度定制化处置处罚的环境。当子系统间耦合过深难以封装时,外观模式也难发挥作用,因其前提是封装子系统复杂性,像遗留系统子系统耦合紧密,需先重构降低耦合度,再思量用外观模式。
所以希望大家对于设计模式,都要做到依事而定~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

郭卫东

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表