《架构整洁之道》读后感之:从代码结构开始,打造优秀的软件架构

251 阅读6分钟

我正在参加「掘金·启航计划」

按层封装的代码结构

《架构整洁之道》提到了按层封装的代码结构,这是一种将代码按照功能进行水平分层的方式。

例如,web代码为一层,业务逻辑为一层,持久化是另外一层。这种方法将功能相似的代码进行分组,每一层只能对相邻的下层有依赖关系。在Java中,分层的概念通常是用package来表示的。按层封装的优点在于软件项目初期会很合适,因为它不会太过复杂。

然而,它的缺点在于无法展现具体的业务领域信息,只能通过规范和约定来进行约束,容易被打破,因为方法都是public的,可以被跨层调用。当所有类都设置为public时,包仅仅是一种组织形式(类似文件夹的分组方式),而不是封装方式。

示例:UML类图(Class Diagram):

UML类图.png

图展示了按层封装的代码类之间的关系以及它们的属性和方法

UML顺序图(Sequence Diagram):

image.png

图展示了按层封装代码结构中各层之间的交互过程

按功能封装

另一个重要的概念是按功能封装,这是一种垂直切分的方式,按照相关功能、业务概念或者聚合根来切分。这种方法将所有类型都放在一个相同的包中,以业务概念来命名,使得相关代码更容易找到。

但是,有一点需要注意,OrdersController作为整个包的入口,其他类都可以设置为包范围内的protected,代码库中的其他代码都必须通过控制器才能访问订单信息——这可能是好处,也可能是坏处。

代码示例

例子中,我们构建了一个简单的订单管理系统,将订单相关的功能封装在一个独立的包中。包括订单控制器(OrderController)、订单服务(OrderService)和订单存储库(OrderRepository)。

  1. 订单控制器(OrderController):
package com.example.order;

import com.example.model.Order;

// OrderController类处理HTTP请求,并调用OrderService来处理业务逻辑
public class OrderController {

    private OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    // 处理创建订单的HTTP请求
    public void createOrder(int customerId, String item, int quantity) {
        orderService.createOrder(customerId, item, quantity);
    }

    // 处理获取订单详情的HTTP请求
    public Order getOrder(int orderId) {
        return orderService.getOrder(orderId);
    }
}
  1. 订单服务(OrderService):
package com.example.order;

import com.example.model.Order;

// OrderService类负责处理业务逻辑,如创建订单和获取订单信息
class OrderService {

    private OrderRepository orderRepository;

    OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    // 创建订单
    void createOrder(int customerId, String item, int quantity) {
        Order order = new Order(customerId, item, quantity);
        orderRepository.save(order);
    }

    // 获取订单信息
    Order getOrder(int orderId) {
        return orderRepository.findById(orderId);
    }
}
  1. 订单存储库(OrderRepository):
package com.example.order;

import com.example.model.Order;

import java.util.HashMap;
import java.util.Map;

// OrderRepository类负责与数据库交互,如保存和查询订单信息
class OrderRepository {

    // 使用简单的Map模拟数据库存储
    private Map<Integer, Order> storage = new HashMap<>();

    // 保存订单信息
    void save(Order order) {
        storage.put(order.getId(), order);
    }

    // 通过ID查找订单信息
    Order findById(int orderId) {
        return storage.get(orderId);
    }
}

在这个示例中,我们将订单相关的功能按照垂直方向进行封装,将所有类型都放在一个相同的包中,以业务概念来命名。这种结构使得相关代码更容易找到,职责划分清晰,有助于代码的维护和扩展。同时,OrderService和OrderRepository类的访问修饰符设置为包范围内的(默认访问级别),确保只能通过OrderController来访问订单信息

端口和适配器

《架构整洁之道》还提到了端口和适配器这一概念,这是一种将业务领域代码和具体实现细节(框架、数据库等)隔离的架构。它可以区分代码中的内部代码(领域)和外部代码(基础设施),使得内部区域包含了所有的领域概念,而外部区域包含了和外界交互的部分(例如UI、数据库、第三方集成)。

在这种架构中,只有外部代码能依赖内部代码,注意这里的依赖关系是由外向内的。 这样的设计有助于保持业务逻辑的核心部分与实现细节的解耦,有利于系统的扩展和维护。

示例:UML类图(Class Diagram):

UML类图.png
  1. UserRepositoryPort(接口):这是一个接口,定义了与用户存储相关的操作。它为内部领域代码提供了一个抽象的依赖接口,使得领域代码与实际的存储实现(例如数据库)解耦。
  2. UserController:这是一个控制器类,负责处理客户端发出的HTTP请求。它将处理用户创建和查询请求的逻辑委托给UserService。
  3. UserService:这是一个服务类,负责处理用户相关的业务逻辑,例如创建用户和查询用户信息。它依赖于> UserRepositoryPort接口来与用户存储进行交互。
  4. UserRepositoryAdapter:这是一个适配器类,实现了UserRepositoryPort接口。它将领域代码中的抽象存储操作转换为具体的实现,使得领域代码与具体的存储实现(UserRepository)解耦。
  5. UserRepository:这是一个存储库类,负责与数据库交互(在这个简化示例中,我们用一个简单的Map模拟了数据库存储)。它负责保存和查询用户信息。
  6. User:这是一个简单的实体类,表示一个用户。它包含了用户的属性(如ID、姓名和电子邮件)和相应的getter方法。

UML顺序图(Sequence Diagram):

UML类图.png

  1. 客户端发出创建用户的请求,请求参数包括姓名和电子邮件。这个请求被发送到UserController。
  2. UserController将创建用户的逻辑委托给UserService。
  3. UserService根据提供的参数创建一个新的User对象,并调用UserRepositoryPort的save方法将新创建的用户对象保存到存储系统中。
  4. UserRepositoryPort将保存用户的请求转发给UserRepositoryAdapter。
  5. UserRepositoryAdapter将请求转发给具体的UserRepository,执行实际的保存操作。
  6. 类似地,在查询用户信息的过程中,UserController将请求委托给UserService,UserService通过UserRepositoryPort、UserRepositoryAdapter和UserRepository进行查询,并将查询结果返回给客户端。

按组件封装

这种方法将个粗粒度组件相关的所有类放入Java包中,将“业务逻辑”与“持久化代码”合并在一起,称为“组件”。组件是部署单元,是系统中能够部署的最小单位,对应在Java中就是jar文件。例如,独立的OrderService会将所有订单相关的东西封装起来。按组件封装的优点在于能利用编译器来约束,相比文档更容易发现结构错误,可以利用编译器来维护架构设原则。

image.png

最后

每种架构方法都有其优缺点,我们需要根据项目的具体需求和团队的技术水平来权衡选择。