清洁架构设计模式

Clean Architecture Design Pattern

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

我对这个模式有一些疑问。数据库位于外层,但它在现实中如何工作?例如,如果我有一个只管理这个实体的微服务:

person{
  id,
  name,
  age
}

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

The overriding rule that makes this architecture work is The Dependency Rule. This rule says that source code dependencies can only point inwards.

  1. 这甚至是一个有效的用例吗?
  2. 外层的数据库怎么访问? (依赖反转?)

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

但是使用依赖倒置会违反

Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle. That includes, functions, classes. variables, or any other named software entity.


Crossing boundaries. At the lower right of the diagram is an example of how we cross the circle boundaries. It shows the Controllers and Presenters communicating with the Use Cases in the next layer. Note the flow of control. It begins in the controller, moves through the use case, and then winds up executing in the presenter. Note also the source code dependencies. Each one of them points inwards towards the use cases.

We usually resolve this apparent contradiction by using the Dependency Inversion Principle. In a language like Java, for example, we would arrange interfaces and inheritance relationships such that the source code dependencies oppose the flow of control at just the right points across the boundary.

For example, consider that the use case needs to call the presenter. However, this call must not be direct because that would violate The Dependency Rule: No name in an outer circle can be mentioned by an inner circle. So we have the use case call an interface (Shown here as Use Case Output Port) in the inner circle, and have the presenter in the outer circle implement it.

The same technique is used to cross all the boundaries in the architectures. We take advantage of dynamic polymorphism to create source code dependencies that oppose the flow of control so that we can conform to The Dependency Rule no matter what direction the flow of control is going in.

Use Case 层是否应该声明一个将由 DB Package(Frameworks & Drivers Layer)实现的 Repository Interface

如果服务器收到 GET /persons/1 请求,PersonRest 将创建一个 PersonRepository 并将此 Repository + ID 传递给 ManagePerson::getPerson 函数,getPerson 不知道 PersonRepository 但知道它实现的接口所以它没有违反任何规则吧? ManagePerson::getPerson 会使用该存储库来查找实体,并且 return 个人实体会 PersonRest::get 这会 return 一个 Json 客户的对象对吗?

遗憾的是,英语不是我的母语,所以如果我理解的模式正确,我希望你们能告诉我,也许可以回答我的一些问题。

提前联系

关键要素是依赖倒置。 None 的内层应该依赖于外层。因此,例如,如果用例层需要调用数据库存储库,那么您必须在用例层中定义一个存储库接口(只是一个接口,没有任何实现)并将其实现放在接口适配器层中。

The Database is at outter Layer but how would that work in reality?

您在用例层创建一个技术独立接口,并在网关层实现它。我想这就是为什么该层被称为接口适配器的原因,因为您在这里适配定义在一个内层中的接口。例如

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

实现在网关层

public class HibernateOrderRepository implements OrderRepository {
      ...
}

在运行时,您将实现实例传递给用例的构造函数。由于用例只依赖于接口,OrderRepository 在上面的例子中,你没有对网关实现的源代码依赖性。

您可以通过扫描导入语句来查看。

And one of the use cases would be manage Persons. Manage Persons is saving / retrieving / .. Persons (=> CRUD operations), but to do this the Usecase needs to talk to a database. But that would be a violation of the Dependency rule

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

如果您使用 Maven 管理您的应用程序依赖项,您将看到 db jar 模块依赖于用例,反之亦然。但如果将这些用例接口提取到自己的模块中会更好。

那么模块依赖关系将如下所示

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

这是依赖关系的反转,否则看起来像这样

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

If i get a GET /person/{id} Request should my Microservices process it like this?

是的,那是违规的,因为 web 层访问 db 层。更好的做法是web层访问controller层,controller层访问use case层等等。

要保持​​依赖倒置,您必须使用我上面显示的接口解耦层。

所以如果你想向内层传递数据,你必须在内层引入一个接口,定义方法来获取它需要的数据并在外层实现。

在控制器层中,您将指定这样的接口

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
       }
    }
}

乍一看好像是样板代码。但请记住,您可以让其余框架将请求反序列化为 java 对象。而这个对象可能会实现 ControllerParams 而不是。

如果您因此遵循依赖倒置规则和干净的架构,您将永远不会在内层中看到外层 class 的导入语句。

干净架构的目的是让主要业务class不依赖于任何技术和环境。由于依赖关系从外层指向内层,因此外层更改的唯一原因是内层更改。或者如果你交换外层的实现技术。例如。休息 -> SOAP

那么我们为什么要这样做呢?

Robert C. Martin 在第 5 章面向对象编程中讲述了它。在依赖倒置部分的末尾,他说:

With this approach, software architects working in systems written in OO languages have absolute control over the direction of all source code dependencies in the system. Thay are not constrained to align those dependencies with the flow of control. No matter which module does the calling and which module is called, the software architect can point the source code dependency in either direction.

That is power!

我想开发人员经常对控制流和源代码依赖性感到困惑。控制流通常保持不变,但源代码依赖关系是相反的。这使我们有机会创建插件架构。每个接口都是一个插入点。因此可以交换,例如出于技术或测试原因。

编辑

gateway layer = interface OrderRepository => shouldnt the OrderRepository-Interface be inside of UseCases because i need to use the crud operations on that level?

是的,OrderRepository 接口应该在用例层定义。还要考虑应用接口隔离原则并定义一个 MyCuseCaseRepository 接口,而不仅仅是每个用例使用的 OrderRepository

你应该这样做的原因是为了防止用例通过公共接口耦合,并遵守单一责任原则。因为专用于一个用例的存储库接口只有一个更改原因。

编辑

应用接口隔离原则并提供专用于用例的自己的存储库接口也是一个好主意。这将有助于将用例彼此分离。如果所有用例都使用同一个Repository接口,那么这个接口就把所有用例的所有方法都聚集起来了。您可以通过更改此接口的方法轻松打破一个用例。

所以我通常应用接口隔离原则并创建以用例命名的存储库接口。例如

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

另一个用例的界面可能如下所示:

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