https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
我对此模式有疑问。数据库位于外层,但实际上如何工作?例如,如果我有一个仅管理此实体的微服务:
person{
id,
name,
age
}
其中一个用例是管理人员。 “管理人员”正在保存/检索/ ..人员(=> CRUD操作),但是要做到这一点,用例需要与数据库对话。但这将违反依赖性规则
使该体系结构起作用的首要规则是“依赖关系规则”。该规则表明,源代码依赖性只能指向内部。
如果我收到一个GET /person/{id}
请求,我的微服务应该这样处理吗?
但是使用依赖倒置会违反
内圈中的任何事物都不能完全了解外圈中的事物。特别是,外圈中声明的名称不得由内圈中的代码提及。其中包括功能,类。变量或任何其他命名的软件实体。
跨越边界。 该图的右下方是一个示例 我们如何越过圆圈边界。它显示了控制器和 演示者在下一层与用例进行通信。注意 控制流程。它开始于控制器,然后通过 用例,然后在演示者中执行。另请注意 源代码依赖性。他们每个人都向内指向 用例。
我们通常使用Dependency解决这种明显的矛盾 反演原理。以Java之类的语言为例, 安排接口和继承关系,以使源 代码依赖关系恰恰反对了控制流 越过边界。
例如,考虑用例需要调用演示者。 但是,此调用不能直接进行,因为这会违反 依赖规则:外圈中没有人可以提及名字 内圈。因此,我们有用例调用接口(此处显示为 内圈有用例输出端口),并在 外圈实现它。
使用相同的技术来跨越 建筑。我们利用动态多态性来创建 与控制流相反的源代码依赖性,因此我们 无论流向何方,都可以符合“依赖规则” 控制权正在进入。
用例层应声明由数据库包(框架和驱动程序层)实现的存储库接口
如果服务器收到一个GET /persons/1
请求,则PersonRest将创建一个PersonRepository并将此存储库+ ID传递给ManagePerson :: getPerson函数,getPerson不知道PersonRepository但知道它实现的接口,因此不会违反有规则吗?
ManagePerson :: getPerson会使用该存储库来查找实体,并将Person实体返回给PersonRest :: get,这会将Json Objekt返回给客户端吗?
可悲的是,英语不是我的母语,所以我希望你们能让我知道我是否理解正确的模式,并可能回答我的一些问题。
请提前
答案 0 :(得分:5)
数据库位于外层,但实际上如何工作?
您在网关层创建一个技术独立的接口,并在db层中实现它。例如
public interface OrderRepository {
public List<Order> findByCustomer(Customer customer);
}
实现位于db层
public class HibernateOrderRepository implements OrderRepository {
...
}
在运行时,将内层与外层实现一起注入。但是您没有源代码依赖性。
您可以通过扫描导入语句来查看。
其中一个用例是管理人员。 “管理人员”正在保存/检索/ ..人员(=> CRUD操作),但是要做到这一点,用例需要与数据库对话。但这将违反依赖性规则
否,这不会违反依赖关系规则,因为用例定义了所需的接口。数据库只是实现它。
如果使用maven管理应用程序依赖项,您将看到db jar模块取决于用例,反之亦然。但是最好将这些用例接口提取到自己的模块中。
然后模块依赖性将如下所示
+-----+ +---------------+ +-----------+
| db | --> | use-cases-api | <-- | use cases |
+-----+ +---------------+ +-----------+
那是依赖关系的倒置,否则看起来像这样
+-----+ +-----------+
| db | <-- | use cases |
+-----+ +-----------+
是的,这将是违法的,因为Web层访问db层。更好的方法是Web层访问控制器层,控制器层访问用例层,依此类推。
要保持依赖关系反转,您必须使用上面显示的接口将各层解耦。
因此,如果要将数据传递到内层,则必须在内层引入一个接口,该接口定义获取所需数据并在外层实现数据的方法。
在控制器层中,您将指定一个这样的接口
public interface ControllerParams {
public Long getPersonId();
}
在网络层中,您可以像这样实现服务
@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
。
因此,如果您遵循依赖关系反转规则和干净的体系结构,则永远不会在内层中看到外层类的import语句。
干净架构的目的是使主要业务类别不依赖于任何技术或环境。由于依存关系从外层指向内层,因此外层发生变化的唯一原因是由于内层发生了变化。或者,如果您交换外层的实现技术。例如。休息-> SOAP
那我们为什么要努力呢?
罗伯特·C·马丁(Robert C. Martin)在第5章“面向对象编程”中对此进行了介绍。在依赖倒置一节的最后,他说:
使用这种方法,在以OO语言编写的系统中工作的软件架构师可以完全控制系统中所有源代码依赖项的方向。 Thay不受约束使这些依赖关系与控制流保持一致。无论调用哪个模块和调用哪个模块,软件架构师都可以将源代码依赖性指向任一目录。
那就是力量!
我想开发人员经常对控制流和源代码依赖性感到困惑。控制流通常保持不变,但是源代码依赖关系相反。这使我们有机会创建插件体系结构。每个接口都是插入点。因此可以互换,例如由于技术或测试原因。
编辑
网关层=接口OrderRepository => OrderRepository-Interface是否应该位于UseCases内,因为我需要在该级别上使用crud操作?
我认为可以将OrderRepository
移入用例层。另一个选择是使用用例的输入和输出端口。用例的输入端口可能具有类似存储库的方法,例如findOrderById
,并将其调整为OrderRepository
。为了保持持久性,它可以使用您在输出端口中定义的方法。
public interface UseCaseInputPort {
public Order findOrderById(Long id);
}
public interface UseCaseOutputPort {
public void save(Order order);
}
仅使用OrderRepository
的区别在于用例端口仅包含特定于用例的存储库方法。因此,它们仅在用例更改时更改。因此,他们只承担一个责任,因此您尊重接口隔离原则。
答案 1 :(得分:2)
关键元素是依赖倒置。内层都不应该依赖于外层。因此,例如,如果用例层需要调用数据库存储库,则必须在用例层内部定义存储库接口(只是一个接口,没有任何实现),并将其实现放置在接口适配器层中。