https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
我对这种模式有些疑问。数据库在外层,但在现实中如何工作呢?例如,如果我有一个管理这个实体的微服务:
null
person{
id,
name,
age
}
null
其中一个用例是管理人员。管理人员正在保存/检索/。。人员(=>CRUD操作),但是要做到这一点,Usecase需要与数据库对话。但那将违反依赖规则
使这个体系结构工作的首要规则是依赖规则。此规则表示源代码依赖项只能指向内部。
如果我收到一个get/person/{id}
请求,我的微服务是否应该这样处理它?
但是使用依赖关系反转将违反
内圈里的人根本不可能知道外圈里的事。特别是,在外圆中声明的某物的名称不得被内圆中的代码提及。这包括,功能,类。变量,或任何其他命名的软件实体。
跨越边界。在图的右下方是一个我们如何跨越圆圈边界的例子。它显示了与下一层中的用例进行通信的控制器和演示者。注意控制流。它在控制器中开始,在用例中移动,然后最终在演示器中执行。还要注意源代码依赖关系。它们中的每一个都指向用例。
我们通常通过使用依赖倒置原理来解决这种表面上的矛盾。例如,在像Java这样的语言中,我们会安排接口和继承关系,使源代码依赖关系在边界的正确位置与控制流相对抗。
例如,考虑用例需要调用演示者。但是,这种调用必须不是直接的,因为那样会违反依赖规则:外圈中的任何名字都不能被内圈提及。所以我们让内圈中的用例调用一个接口(这里显示为用例输出端口),让外圈中的演示者实现它。
相同的技术用于跨越体系结构中的所有边界。我们利用动态多态性来创建与控制流相反的源代码依赖关系,以便无论控制流走向什么方向,我们都能符合依赖关系规则。
用例层是否应该声明一个将由DB包(框架和驱动程序层)实现的存储库接口
如果服务器收到get/persons/1
请求,PersonRest将创建一个PersonRepository,并将这个Repository+ID传递给ManagePerson::getPerson函数,getPerson不知道PersonRepository,但知道它实现的接口,因此它不会违反任何规则,对吗?ManagePerson::GetPerson将使用该存储库查找实体,并将一个Person实体返回给PersonRest::Get,后者将向客户端返回一个Json Objekt,对吗?
很遗憾,英语不是我的母语,所以我希望你们能让我知道我是否正确地理解了这个模式,也许能回答我的一些问题。
提前Ty
数据库在外层,但在现实中如何工作呢?
您在用例层创建一个与技术无关的接口,并在网关层实现它。我猜这就是为什么那一层被称为接口适配器,因为您可以在这里适配定义在内层中的接口。例如。
public interface OrderRepository {
public List<Order> findByCustomer(Customer customer);
}
实现在网关层
public class HibernateOrderRepository implements OrderRepository {
...
}
在运行时,您将实现实例传递给用例的构造函数。由于用例只依赖于接口(在上面的示例中为orderrepository
),因此您不依赖于网关实现的源代码。
您可以通过扫描您的导入语句来看到这一点。
其中一个用例是管理人员。管理人员正在保存/检索/。。人员(=>CRUD操作),但是要做到这一点,Usecase需要与数据库对话。但那将违反依赖规则
不,那不会违反依赖规则,因为用例定义了它们需要的接口。db只是实现它。
如果您使用maven管理应用程序依赖关系,您将看到db jar模块依赖于用例,而不是相反。但是将这些用例接口提取到自己的模块中会更好。
则模块依赖关系如下所示
+-----+ +---------------+ +-----------+
| db | --> | use-cases-api | <-- | use cases |
+-----+ +---------------+ +-----------+
这是依赖关系的反转,否则会像下面这样
+-----+ +-----------+
| db | <-- | use cases |
+-----+ +-----------+
是的,那将是一个违反,因为web层访问db层。更好的方法是web层访问控制器层,控制器层访问用例层等等。
为了保持依赖关系反转,您必须使用接口来解耦各层,就像我上面展示的那样。
因此,如果您想要将数据传递到内层,您必须在内层引入一个接口,该接口定义获取所需数据的方法,并在外层实现它。
在控制器层中,您将指定如下接口
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
。
因此,如果您遵循依赖关系反转规则和干净的体系结构,您将永远不会在内层中看到外层类的导入语句。
清洁架构的目的是主要业务类不依赖于任何技术或环境。由于依赖关系从外层指向内层,所以外层更改的唯一原因是因为内层的更改。或者交换外层的实现技术。例如。休息->;肥皂
那么我们为什么要做这个努力呢?
RobertC.Martin在第5章面向对象编程中讲述了这一点。在“依赖关系反转”一节的最后,他说:
使用这种方法,在用OO语言编写的系统中工作的软件架构师对系统中所有源代码依赖项的方向拥有绝对的控制权。他们不受约束地将这些依赖关系与控制流对齐。无论哪个模块进行调用,哪个模块被调用,软件架构师都可以将源代码依赖指向任何一个方向。
那就是力量!
我猜开发人员经常对控制流和源代码依赖关系感到困惑。控制流通常保持不变,但源代码依赖关系是反向的。这给了我们创建插件架构的机会。每个接口都是一个要插入的点。因此可以进行交换,例如出于技术或测试的原因。
编辑
网关层=接口订单存储库=>;OrderRepository-Interface不应该在UseCases中吗,因为我需要在那个级别上使用crud操作?
是的,OrderRepository接口应该在用例层中定义。还要考虑应用接口隔离原则,定义一个MyCuseCaseRepository
接口,而不是每个用例都使用的OrderRepository
。
您应该这样做的原因是为了防止用例通过公共接口耦合,并兑现单一责任原则。因为专用于一个用例的存储库接口只有一个更改的理由。
编辑
应用接口隔离原则并提供专用于用例的自己的存储库接口也是一个好主意。这将有助于将用例彼此解耦。如果所有用例都使用相同的存储库接口,那么这个接口就累加了所有用例的所有方法。您可以通过更改此接口的一个方法轻松地中断一个用例。
所以我通常应用接口隔离原则,创建以用例命名的存储库接口。例如。
public interface PlaceOrderRepository {
public void storeOrder(Order order);
}
另一个用例的接口可能如下所示:
public interface CancelOrderRepository {
public void removeOrder(Order order);
}
关键要素是依赖反转。任何内层都不应依赖于外层。因此,例如,如果用例层需要调用数据库存储库,那么您必须在用例层中定义一个存储库接口(只是一个接口,没有任何实现),并将其实现放在接口适配器层中。