门面模式

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

Facade 模式是经典的 四人帮 结构模式家族的一部分。我们已经了解了结构模式系列中的其他模式 – Adapter Bridge Composite Decorator



为子系统中的一组接口提供统一的接口。 Facade 定义了一个更高级的接口,使子系统更易于使用。

-- 设计模式:可重用面向对象软件的要素

当我们创建一个系统时,我们将其划分为子系统以降低复杂性。我们遵循 单一职责原则 为子系统类分配特定的职责。但是,子系统之间通常存在依赖关系。此外,客户端单独与子系统类交互以满足业务需求可能会导致显着的复杂性。

考虑电子商务商店的订单履行流程。当用户下订单购买产品时,以下服务完成该过程:

  • 库存服务 :检查在 Oracle 上运行的仓库数据库的产品可用性。
  • 支付服务 :与支付网关连接以处理订单支付。
  • 运输服务 :连接外部物流网络服务,将产品从仓库运送到用户地址。

应用程序的控制器与订单的上述服务进行交互。当用户与 UI 交互下订单时,请求被映射到控制器,后者又与服务交互以完成请求,然后将完成状态通知用户。在真实的电子商务商店应用程序中,控制器通常是底层框架的专用组件,例如 Spring MVC 控制器。

我们的电子商务商店还支持移动客户端。用户可以下载客户端应用程序并从他们的设备下订单。传统桌面客户端还可以与商店通信,作为对希望通过客户服务助理通过电话下订单的用户的持续支持。这就是不同的客户如何与电子商务商店的订单履行流程进行交互。

如上图所示,客户端需要与子系统类实现的服务进行多次交互,为此,客户端需要了解子系统类的内部结构。这意味着,我们的客户与子系统类紧密耦合——这从根本上违反了 SOLID 设计原则 。想象一下,如果底层数据存储需要更改为 NoSQL 数据库,或者当前的支付网关被另一个支付网关取代,会产生怎样的影响。如果在服务层中引入新的 InvoicingService 或更新现有的 ShippingService 以使组织内部的物流部分,情况可能会变得更糟。由于这种紧密耦合,服务层中的任何更改都将传播到客户端层。这使得更改既耗时又容易出错。

我们不需要让客户端与子系统紧密耦合,而是需要一个使子系统更易于使用的接口。在我们的示例中,我们的客户只想下订单。他们真的不需要关心处理库存、运输或付款。 Facade 模式是一种为客户端与子系统交互提供简单方法的方法。通过外观,现在我们可以在不影响客户端代码的情况下更改子系统类。简而言之,我们使客户端与子系统类松耦合。

有了外观,这就是不同的客户与订单履行流程交互的方式。

正如您在上图中看到的,通过引入外观,客户端现在与外观进行交互以完成订单,而不是单独的子系统服务。外观从客户端透明地处理与子系统服务的底层交互。

因此,我们可以将 Facade 模式的参与者分类为:

  • Facade :将客户端请求委托给适当的子系统类。
  • 子系统类 :实现子系统功能。子系统类由外观使用,但反之则不然。我们稍后会在这篇文章中谈到它。
  • 客户端 :请求外观执行一些操作。

应用外观模式

要将外观模式应用于我们的订单履行示例,让我们从域类开始 Product

产品.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


我让 Product 类保持简单,只有两个字段,一个用于初始化它们的构造函数和默认构造函数。

接下来我们将编写子系统服务类。

库存服务.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


支付服务.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


ShippingService.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}

子系统类代表订单履行流程的不同服务。需要注意的一件事是子系统类没有对外观的引用。这些类不知道任何 Facade,并且被设计为独立工作,即使 Facade 不存在。请记住—— 子系统类由外观使用,但反之则不然

出于示例的目的,我将服务类保持在最低限度。这仅用于说明目的。一个真正的电子商务示例会复杂得多。

我们可以有一个没有任何接口的具体外观类——模式不要求一个。然而,我们将提供一个接口来遵循——“ 依赖于抽象。 Do not depend on concretions ”总结了 依赖倒置原则 。通过这样做,我们可以让客户端针对此接口进行编程,以通过外观与服务进行交互。将我们的代码写入一个接口也松散了类之间的耦合。

OrderServiceFacade.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


我们将在 OrderServiceFacadeImpl 类中实现该接口。

OrderServiceFacadeImpl.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


在外观中,我们实现了整合所有子系统交互的 placeOrder() 方法。在这个方法中,我们调用了服务上的方法来执行完成订单的操作。

接下来我们将编写控制器类——门面的客户端。

OrderFulfillmentController.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


我们编写的 OrderFulfillmentController 客户端类非常简单。客户端控制器调用外观的 placeOrder() 方法并将结果存储在 boolean 中。

我经常看到初级程序员把控制器类搞得一团糟。在MVC设计模式中,controller绝对没有直接与数据库层交互的业务。在控制器类中直接使用 JDBC 数据源的情况太常见了。这明显违反了 单一职责原则 。控制器只有一个目的,那就是响应网络请求。它不是调用数据库,不是使用 Hibernate,不是管理数据库事务。

因为我们的控制器在生活中只有一个功能,所以很容易测试。

OrderFulfillmentControllerTest.java


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


测试的输出是这样的。


 package guru.springframework.gof.facade.domain;
public class Product {
    public int productId;
    public String name;
    public Product(){}
    public Product(int productId, String name){
        this.productId=productId;
        this.name=name;
    }
}


结论

在 GoF 模式中,我发现 Facade 模式是最容易理解和应用的模式之一。其实,在我知道它之前,我已经在直观地应用它了。一旦理解了 Facade 模式,就会越来越多地使用它。

程序员经常将 Facade 模式与 Adapter 模式混淆。请记住,Facade 通常是关于降低与子系统接口的复杂性,而 Adapter 更适合将现有接口调整为客户期望使用的另一个接口。

使用 Spring 开发的企业应用程序 中,外观通常用于整合应用程序为其用户提供的所有业务服务。在 Spring 应用程序中,您将经常开发业务和服务外观,作为应用程序业务逻辑和服务层的网关。对于持久性,您将编写 DAO,这是一种外观,但特定于数据层。虽然,我有意让这个示例保持通用,但您应该能够在 IoC 和 依赖注入 的上下文中看到它如何与 Spring 一起很好地工作。