第一步是将整个代码库分成组件和领域模型。组件包含大部分业务逻辑,领域模型包含数据、状态和领域模型逻辑。如果您在建筑层面将它们分开会有所帮助——组件依赖于模型,但反之则不然。
什么是组件和模型,它们的属性是什么?
避免外部状态可能是领域模型逻辑最典型的限制。现在,您可以看到最后一张图片并看到该模型对任何其他工件没有任何依赖性。因此,那里的逻辑不能调用任何其他服务,当然也不能在数据库中存储任何东西(或它本身)。最好不要间接传递外部状态(例如,通过将域模型逻辑的参数值传递到模型中)。在那里传递的每个参数都会增加内存消耗,并且这些模型可以大量存在。此外,参数值和域数据有些混合。
第二步是将组件分成控制器、服务和存储库层。我们还应该以这种方式在构建级别上将它们分开:Controller -> Service -> Repository。
将代码内部解耦为层对于代码演进非常有用。我们将它们分为三层:
对于层之间的通信,我们可以使用数据传输对象(DTO)。为了简化,我们可以使用主要属于服务层的领域模型。如果我们想清楚地分开(并严格遵循单一职责原则),我们也可以为 Controller 和 Repository 层创建额外的 DTO。
正如我们所说,这些层应该在构建级别上分开(例如,通过 Maven 工件)。确保没有不需要的依赖项泄漏到错误的层(例如,REST API 泄漏到服务层,甚至数据库库泄漏到控制器层)。此外,确保控制器和存储库与业务逻辑无关,反之亦然。使用这个经验法则:总是问自己控制器/存储库是否可以按原样替换为新控制器/存储库,并且不需要重新实现从业务逻辑到新控制器/存储库的任何内容。
现在事情变得有点复杂,所以包应该被命名。最佳实践似乎是工件的名称应该从包名称的一部分创建,以便能够轻松地从 Stacktrace 中找到工件(例如,工件的名称:来自 eu.manta.controller.user 的 manta-controller)。我们可以避免包名称前缀,因为在大多数情况下,它是显而易见的,而后缀(在层名称之后)因为它包含工件。
第三步是在层之间创建契约——端口——并反转服务层和存储层之间的依赖方向。在构建级别,依赖项如下所示:Controller -> InputPort <- Service -> OutputPort <- Repository。这种架构模式也被称为六边形架构,由 Alistar Cockburn 创建。
这是分层架构的下一步。解决了Service层依赖Repository这个明显的问题,所以Repository是不可替换的。此外,我们这里有两个新工件:输入端口和输出端口。它们包含定义层间契约的接口。如果我们认为它们有用或者我们想要遵守单一职责原则,它们也可以包含 DTO。