干净的架构设计模式

19

enter image description here

https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

我对这种模式有一些疑问。数据库在外层,但在实际中会如何工作?例如,如果我有一个仅管理此实体的微服务:

person{
  id,
  name,
  age
}

其中一个用例是管理人员。管理人员是保存/检索/...人员(=>CRUD操作),但要做到这一点,Use Case需要与数据库进行通信。但这将违反依赖规则。

使此体系结构工作的最重要的规则是依赖性规则。该规则指出源代码依赖关系只能指向内部。

  1. 这会是一个有效的用例吗?
  2. 如果它在外层,我该如何访问数据库?(依赖反转?)

如果我收到GET /person/{id}请求,我的微服务应该如何处理?

enter image description here

但使用依赖反转会违反

内圈中的任何内容都不能了解外圈中的任何东西。特别是,在内圈中声明的某个名称不得被内圈中的代码提及。其中包括函数、类、变量或任何其他命名的软件实体。


跨越边界。 在图表的右下方是一个示例 我们如何穿过圆形边界。它显示控制器和 现在的演示文稿与下一层中的用例通信。注意 控制流。它开始于控制器,通过 使用案例,然后最终执行演示文稿。还要注意 源代码依赖关系。每个指针都指向内部   案例。

我们通常通过使用依赖反转原则来解决这种表面上的矛盾。   例如,在Java这样的语言中,我们会   安排接口和继承关系,以便源代码依赖关系抵制控制流在正好相反的点处穿过边界。

例如,考虑到用例需要调用演示文稿。   但是,此调用不能直接进行,因为那将违反依赖规则:不能提及外部圆中的任何名称。   因此,我们让用例在内部圈中调用一个接口(此处显示为Use Case Output Port),并由外部圈中的演示者实现它。

在架构中跨越所有边界时使用相同的技术。   我们利用动态多态性创建源代码依赖关系,以抵制控制流,以便无论控制流的方向如何,我们都可以符合依赖规则。

Use Case层应该声明一个Repository接口,由DB包(框架和驱动程序层)实现吗?

enter image description here

如果服务器收到一个GET /persons/1请求,PersonRest会创建一个PersonRepository并将该Repository和ID传递给ManagePerson::getPerson函数。getPerson不知道PersonRepository但知道它实现的接口,所以它不违反任何规则,对吗?ManagePerson::getPerson将使用该Repository查找实体,并返回一个Person Entity给PersonRest::get,后者将向客户端返回一个Json对象,对吗?

个人认为,清晰架构思想过于复杂,我更喜欢洋葱架构,我已经使用这种架构创建了一个示例项目 - Adam Siemion
2个回答

12

整合数据库

数据库在外层,但在现实中如何工作呢?

你需要在用例层创建一个技术无关接口,并在网关层实现它。我想这就是为什么那一层被称为接口适配器,因为你在此处调整内部层次定义的接口。例如:

public interface OrderRepository {
    public List<Order> findByCustomer(Customer customer);
}

实现在网关层

public class HibernateOrderRepository implements OrderRepository {
      ...
}

在运行时,您将实现实例传递给用例的构造函数。由于该用例仅依赖于接口(如上例中的OrderRepository),因此您不必对网关实现进行源代码依赖。

通过扫描您的导入语句,您可以看到这一点。

其中一个用例是管理人员。管理人员是保存/检索/..人员(=> CRUD操作),但为了做到这一点,Usecase需要与数据库通信。但这将违反依赖规则

不,那不会违反依赖规则,因为用例定义了它们需要的接口。数据库只是实现它。

如果使用maven管理应用程序依赖项,则会看到数据库jar模块依赖于用例而不是相反。

+-----+      +-----------+
|  db | -->  | use cases |
+-----+      +-----------+

将这些用例接口提取为一个独立的模块可能更好。这可以防止db模块依赖于用例模块的依赖项。

那么模块依赖关系就会变成这样

+-----+      +---------------+     +-----------+
|  db | -->  | use-cases-api | <-- | use cases |
+-----+      +---------------+     +-----------+

两个选项都是依赖关系的反转,否则依赖关系会像这样:

+-----+         +-----------+
|  db | <--X--  | use cases |
+-----+         +-----------+

整合Web层

如果我收到了一个GET /person/{id}请求,我的微服务应该这样处理吗? enter image description here

不应该这样做,因为Web层访问了DB层。更好的方法是Web层访问控制器层,控制器层访问用例层,用例层访问存储库,可以是数据库存储库,也可以是任意外部系统。

为了保持依赖反转,您必须使用接口将层解耦,如上所示。

因此,如果您想将数据传递到内部层,则必须在内部层引入定义获取所需数据的方法的接口,并在外部层中实现它。换句话说,您需要将外部层适配到内部层。我想这就是为什么Bob大叔称此层为接口适配器的原因。

Interface Adapters Layer

在控制器层中,您将指定一个接口,如下所示

public interface ControllerParams {
    public Long getPersonId();
}

在Web层中,您可以像这样实现您的服务

@Path("/person")
public PersonRestService {

    // Maybe injected using @Autowired if you are using spring
    private SomeController someController;

    @Get
    @Path("{id}")
    public void getPerson(PathParam("id") String id){
       try {
           Long personId = Long.valueOf(id);

           someController.someMethod(new ControllerParams(){
                public Long getPersonId(){
                    return personId;
                }
           });
       } catch (NumberFormatException e) {
           // handle it
       }
    }
}

乍一看似乎是样板代码,但请记住,您可以让rest框架将请求反序列化为Java对象。而这个对象可能实现了ControllerParams

如果您始终遵循依赖反转规则和清洁架构,您将永远不会在内部层中看到外部层类的导入语句。

那么我们为什么要做这个努力呢?

清洁架构的目的是主要业务类不依赖任何技术或环境。由于依赖关系从外部到内部层,外部层发生变化的唯一原因是因为内部层发生变化或者如果您要更换外部层的实现技术。例如Rest -> SOAP。

罗伯特·C·马丁在第5章面向对象编程的结尾部分在依赖反转的部分告诉我们:

采用这种方法,使用面向对象语言编写系统的软件架构师可以对系统中所有源代码依赖项的方向拥有绝对掌控权。 他们不受约束地将这些依赖项与控制流对齐。 无论哪个模块进行调用,哪个模块被调用,软件架构师都可以将源代码依赖性指向任何方向。

这就是力量!

控制流和源代码依赖关系

我猜开发人员经常会对控制流和源代码依赖之间的区别感到困惑。

控制流描述运行时调用的顺序。它引入的依赖关系称为运行时依赖项。

源代码依赖关系是指源代码中出现的类型所依赖的关系。在像Java这样的语言中,需要导入类型。这就是为什么导入语句几乎包括所有源代码依赖项的原因。我说几乎所有,因为同一包中的类型不需要导入。

依赖反转意味着源代码依赖关系与控制流相反。依赖反转为我们提供了创建插件体系结构的机会。每个接口都是插入点,可以出于领域、技术或测试原因交换。

编辑

网关层 = 接口OrderRepository => OrderRepository-Interface不应该在UseCases中吗?因为我需要在那个层级上使用CRUD操作?

是的,OrderRepository接口应该在用例层中定义。我们经常犯的一个错误是认为接口属于实现者。但是,接口属于客户端。客户端告诉接口它想要什么,但保持开放如何完成。

还要考虑应用接口隔离原则并定义特定于用例的接口,例如PlaceOrderUseCaseRepository接口,而不仅仅是每个用例都使用的OrderRepository

您应该这样做的原因是防止通过公共接口耦合用例,并遵守单一责任原则。专门用于一个用例的存

public interface PlaceOrderRepository {
     public void storeOrder(Order order);
}

还有一个使用案例的界面可能是这样的:

public interface CancelOrderRepository {
     public void removeOrder(Order order);
}

谢谢@RenéLink的回答 :) 网关层 = 接口OrderRepository => OrderRepository接口不应该在UseCases内部吗?因为我需要在那个层次上使用crud操作。 - Barney Stinson

2
关键的元素是依赖反转。内层不应依赖于外层。因此,例如 Use Case 层需要调用数据库存储库,则必须在 Use Case 层内定义一个存储库接口(仅是一个接口,没有任何实现),并将其实现放在接口适配器层中。

你好,首先感谢你的回答!在框架和驱动层中的数据库包里我该放什么? - Barney Stinson
1
@BarneyStinson 例如数据库存储库接口的实现。 - Adam Siemion
网关(接口适配器层)和DB(框架和驱动程序)=>如果我使用Java Spring,我应该将存储库放在DB中,因为它是一个框架,如果我不使用框架而是使用JDBC,例如,我把它放在接口适配器层中!?我真的不理解DB和网关之间的区别。 - Barney Stinson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接