用例驱动实现DDD的方法 - codex


根据UML Distilled(第 9 章),用例是由一个共同的用户目标联系在一起的一组场景(banq:特定角色的各种操作场景上下文)。在这种情况下,我们处理的是一种系统用例,它表示用户角色与系统的之间的交互。
要表达单个交互操作的概念,请在命名用例时以动词开头(例如“列出动作”、“提款”、“删除我的数据”、“支付购物车”)。在实践中,用例只是一个函数(一个查询或一个命令),它应该是纯粹的——确定性的并且没有副作用。(banq注:类似事件风暴中的领域事件中命令动作,注意动作是有限制的上下文BC)
用例应该是最高级别和最可见的架构实体。用例是中心。总是!数据库和框架都是细节!没有数据库
 
用例是您的应用程序的工作单元(即其架构原子设备)。他们应该是一等公民,以便在浏览其代码库时应用程序的目标立即变得清晰。尽管这与代码组织有关,但最终会促进领域驱动架构的良好实践。
正如房屋或图书馆的计划对这些建筑物的用例大肆宣传一样,软件应用程序的架构也应该对应用程序的用例大肆宣传 - 尖叫架构
用例驱动的方法在后端应用程序中更有意义,因为这是业务逻辑应该存在的地方。
 
让我们以通过 Web API 提供服务的后端应用程序为例:
在领域驱动架构中,Web API 是由 Web 请求处理程序组成的主要适配器(适配器是应用程序入口点,不包含业务逻辑)。
在用例驱动的方法中,每个 Web 处理程序只是一个用例入口点——它们具有一对一的关系。因此,每个 web 处理程序都应该有它的实现文件,包括验证、解析、(反)序列化、调用域、错误处理、响应准备、API 文档(例如OpenAPI)、常量等。没有其他地方可以寻找。一切尽在一处。

class CreateUserHandler(
    private val createUser: CreateUser,
    private val generateUserId: () -> UserId,
) : Handler {

    override fun handle(ctx: Context) {
        val createUserResult = createUser(
            ctx.bodyAsClass(UserRepresenter::class.java)
                .toUser(generateUserId())
        )
        ctx.status(
            when (createUserResult) {
                NewUser -> HttpStatus.CREATED_201
                UserAlreadyExists -> HttpStatus.CONFLICT_409
            }
        )
    }

    private class UserRepresenter(val email: String, val name: String, val password: String) {
        fun toUser(id: UserId) = User(
            email = email.toEmail(), name = name, password = password.toPassword(), id = id
        )
    }
}

 
领域
域是业务操作发生的地方。充满域操作的“服务”/“集线器”文件是一个坏主意。相反,为每个用例创建一个文件并相应地命名它。具有支持用例的单个公共功能的文件并不是一件坏事。事实上,这是一个很好的想法,因为它是模块化的和有凝聚力的。它应该是自包含的,并且仅依靠辅助适配器来满足其需求(例如存储库)。也就是说,除了实际的用例功能(包括语义验证、编排等),在同一文件中还包括其请求/响应模型、错误/异常和任何私有帮助程序。
  
测试
为每个用例创建一个测试文件。每个测试文件都会练习一个用例的所有场景。每个测试只需要注入用例的实际依赖项,而不是一堆。这些因素有助于“将测试作为文档”——自动化测试的一个目标——因为它们有助于将用例描述为用户/客户。
 
功能模块化
用例驱动的方法促进了用例之间的解耦。由于它有助于发现它们之间的共享代码,因此您犯该错误的可能性较小。
按用例组织代码库是识别应用程序存在的原因。用例是应用程序的工作单元。一旦开始利用它,您就可以识别特征、子域和有界上下文。如果有意义,这有助于创建微服务和/或按团队拆分工作。
。。。。
点击标题见详细原文