领域服务

了解领域服务

  • 领域中的服务表示一个无状态的操作
  • 用于实现特定于某个领域的任务
  • 当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务

为了避免贫血模型,在封装领域逻辑时,考虑设计要素的顺序为:
Value Object -> Entity -> Domain Service

什么情况下需要领域服务

  • 执行一个显著的业务操作过程
  • 对领域对象进行转换
  • 以多个领域对象作为输入进行计算,结果产生一个值对象

那些逻辑放在领域服务中

  • 领域行为需要多个领域实体参与协作
  • 领域行为与状态无关
  • 领域行为需要与外部资源(尤其是DB)协作

建模领域服务

  • 需要确定创建的领域服务是否需要一个独立的接口
  • 给领域服务的实现类命名
  • 为领域服务创建一个迷你层
  • 最后测试领域服务

参考文献

  1. 《实现领域驱动设计》 — Vaughn Vernon
  2. 何时定义领域服务 — 张逸

聚合:如何聚合

聚合

  • 定义:将实体和值对象在一致性边界之内组成聚合
  • 聚合:体现了一套不变的业务规则 判断聚合关系的方法:如果整体不存在时,部分是否存在。如果不存在,就是聚合;反之如果整体不存在,部分仍然会存在,则他们之间不是聚合关系。

如何聚合

  • 找到事务一致性,即要求具有立即性和原子性,同时具备最终一致性
  • 在创建聚合时,主要关注的是一致性边界,而不是创建对象树
  • 尽可能的设计小聚合,减少维护一致性和不变性的成本
  • 大聚合可能会限制系统的性能和可伸缩性
  • 小的极端是,一个聚合只拥有一个全局标识和单个属性
  • 好的做法是,使用根实体(Root Entity)来表示聚合,其中只包含最小数量的属性和值类型属性
  • 聚合的属性:
    • 保留那些必须与其它属性保持一致的属性(当其中一个被改变,修改,其它的也会随之改变)
    • 聚合的属性优选值对象:测试简单、维护方便
  • 当一个用例需要修改多个聚合时,需要考虑是否需要重新建模聚合(因为一个用例的变化关注点应该是单一的)

实现

  • 通过唯一标识引用其他聚合
  • 引用聚合和被引用聚合不可以在同一个事务中进行修改(同时修改说明耦合,设计聚合时的一致性边界划定有问题)
  • 如果上面一条规则导致大聚合,那么可能需要引入最终一致性(即,各聚合在最终达到一致事务状态)
  • 那么如何解决对象之间交互完成任务的情况呢?
    • 通过标识引用使多个聚合协同工作
    • 建模对象导航性:可以在调用聚和行为之前,使用资源库或领域服务来获取所需要的对象
    • 避免在聚合中使用资源库或领域服务(处理复杂依赖关系中,在聚合方法中使用领域方法却是最好的解决方法)
  • 在边界之外使用最终一致性
    • 当在一个聚合上执行命令方法时,如果需要在其他聚合上执行额外的业务规则,那么使用最终一致性
    • 需要领域专家确认,对于修改不同聚合实例之间的时间延迟,他们是否可以接受,最终一致性方法:
      • 一个聚合的命令方法所发布的领域事件及时的发送给异步订阅放
      • 在接收到事件之后,每个订阅方都会获取自己的聚合实例,然后在该聚合上完成想要的操作
      • 每个订阅放都在单独的事务中进行操作,满足了一次事务只修改一个聚合实例的原则
  • 在一个事务中更新多个聚合实例的理由(妥协的理由):
    • 方便用户界面
    • 缺乏技术机制
    • 全局事务
    • 查询性能

参考文献

  1. 《实现领域驱动设计》 — Vaughn Vernon

DCI架构

以前的架构

  • 面向对象编程先驱的目标是在代码中捕获最终用户的心理模型
  • 用户在界面上所做的任何工作都会操作代码中的对象,如果程序提供有关用户操作如何影响程序状态的实时反馈,
    则可以减少用户错误和意外。一个好的GUI服务应该提供这种可以让用户感知到实际情况的交互表示。
  • MVC的目标就是提供一种从最终用户大脑到计算机“大脑”直接连接的错觉
  • MVC是为了将信息的表示与用户的交互分开。
  • 信息是最终用户心智模型的关键要素。
  • 设计良好的程序应该要捕获数据模型中的信息模型,如果做到这一点,用户就会觉得计算机内存是他内存的延伸。
  • 也就是说,最终用户实际上实在操作记忆中他们头脑中想象的对象 broadcasting
  • 这种直接操纵错觉是关于计算机是什么以及它们如何为人们服务的客观视角的核心。
  • 视图在屏幕上显示模型。不同的视图可能以完全不同的方式支持相同的数据,即相同的模型。
  • 控制器创建视图,并协调视图和模型。它通常作为按键、定位设备、其它事件接收,输入用户手势的角色。 broadcasting

遇到的问题

  • 面向对象的设计过程,在多个对象进行交互时,这个交互过程没有被捕获到。
  • 这些协作与交互也具有结构,构成了最终用户心智模型的一部分,但是代码中没有内聚这些交互。

思考数据

  • 数据(Data)代表用户对他们世界中事物的心理模型(对象)
  • 以前的经验法则:名词是对象,动词是方法。
  • 假设一个 savingAccount对象,从这个账户中取款,就是这个账户减少余额的动作
  • 其中减少余额容易理解为这个savingAccount的行为,但是取款这个数据目的就被丢失了
  • 事实上取款是整个系统的行为,并不同与减少余额这个savingAccount对象的行为
  • 以前面向对象的解决方法就是通过继承,即 取款继承和扩展了savingAccount类,保持了基类的稳定性,实现了
    开闭原则:对扩展开放,对修改关闭。
  • 但是过度的继承,导致难以追溯业务行为;以致对程序员造成沉重负担。

思考角色

  • 这里说的角色(role),一种新的行为概念,它也存在于用户的脑海中
  • 以转账为例,程序员识别的算法:
    源账户开始交易
    源账户验证可用资金(请注意,这必须在交易内部完成,以避免干预提款!)
    源账户减少自身余额
    源帐户请求目标帐户增加其余额
    源账户更新其日志以指出这是一次转账(而不是,例如,简单的提款)
    源帐户请求目标帐户更新其日志
    源账户结束交易
    源账户通知账户持有人转账成功
    

    代码实现的行为如下:

    template <class ConcreteAccountType>
    class TransferMoneySourceAccount: public MoneySource
    {
    private:
     ConcreteDerived *const self() {
        return static_cast<ConcreteDerived*>(this);
     }
     void transferTo(Currency amount) {
        // This code is reviewable and
        // meaningfully testable with stubs!
        beginTransaction();
        if (self()->availableBalance() < amount) {
          endTransaction();
          throw InsufficientFunds();
        } else {
          self()->decreaseBalance(amount);
          recipient()->increaseBalance (amount);
          self()->updateLog("Transfer Out", DateTime(), amount);
          recipient()->updateLog("Transfer In", DateTime(), amount);
        }
        gui->displayScreen(SUCCESS_DEPOSIT_SCREEN);
        endTransaction();
      }
    }
    
  • 这种实现方式比把行为分布在多个类中更容易理解。更像用户心中想的那样。
  • 本质上讲,角色(role)体现了通用的抽象算法
  • 用户识别单个对象及其域的存在,但每个对象还必须实现来自用户交互模型的行为。这些行为通过它在给定用例中扮演的角色将其与其它对象联系在一起。
    • 例如:银行及其账户的系统是什么数据模型,以及在账户间进行资金转义算法模型中系统是做什么的。
  • 下图中 broadcasting
  • 右侧,我们将最终用户角色抽象捕获为接口
  • 顶部,我们发现从右侧角色抽象的克隆开始的角色,但其填充了方法。
  • 左侧,类
  • 角色和类都存在于最终用户的脑海中,两者在运行时融合为一个对象。
  • 与场景用例相关的更多动态操作来自对象所扮演的角色
  • 从用例场景中截取的操作集合称为角色。
  • 希望如下图:将角色的逻辑注入到对象中,以便他们与对象在实例化时从其类接收的方法一样多地成为对象的一部分。 broadcasting
  • 如果语言支持,可以更聪明些在运行时向每个对象中注入足够多的逻辑

协调工作的角色: 上下文和交互

  • 以转账为例:为了实施转账用例,就需要储蓄账户扮演源账户的角色,投资账户扮演目标账户的角色。
  • 每个角色“方法”都将在它被粘到的对象的上下文中执行
  • 可以使用例如:委托、注入等技巧为了对象提供必要的智能以扮演他们必须扮演的角色 broadcasting
    • 在MVC中,controller将model对象放在用户操作用例中执行,这些对象此时都是在内存中
    • 以转账为例:最终用户考虑一个过程或算法来根据所涉及的角色进行汇款。 我们需要挑选出可以运行该算法的代码,然后我们要做得就是将正确的对象与正确的角色对齐并让代码运行。
    • 如上图所示,算法和角色到对象的映射都属于一个Context目的
    • 典型实现中,每个用例都有一个上下文对象,每个上下文都包含一个标识符,用于该用例中所涉及的每个角色 Context所要做得就是将角色标识符绑定到正确的对象,然后我们只需要启动上下文的“入口”–角色的触发器 方法,代码就可以运行了。
    • Context可以视为一个表,它将角色成员函数映射到一个对象方法

DCI特性

  • 使用role来捕获参与用例需求的主要用户概念,角色是最终用户认知模型的一流自己,我们希望在代码中反映它们
  • 使用对象来捕获来自经验和隐性知识的深层领域概念,作为勉强智能的数据
  • 软件展现开闭原则而仅基于继承的开闭原则导致信息隐藏不佳,DCI风格保持了领域类和角色的完整性
  • 类对修改是封闭的,通过角色的注入对扩展开放
  • DCI非常适合敏捷软件开发,它运行程
  • 序员直接与最终用户的心智模型连接

参考文档

  1. DCI 架构:面向对象编程的新愿景

Go fake for os.exec

Golang os.exec

我们进行Linux后端服务编程时,经常会进行系统操作。Golang对系统命令执行的支持是   通过os.exec库实现的。  
那么在测试时,我们如何在UT当中,对调用os.exec的地方进行单元测试呢?
这个库的mock并不好处理,在设置exec.Command之后执行它的run方法,所以即使想对它   进行打桩也找不到可以替换的接口和mock对象。

这个包自己的解决办法

在exec这个包的测试文件exec_test.go中,可以看到有这样一段代码: ```Go func helperCommand(t *testing.T, s ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd } ``` 上面这个函数在测试用例执行时执行的,并且在函数中执行一个命令为os.Args[0]的系统命令。   os.Args[0]就是这个Go代码在执行go test的时候的执行程序。 通过上面这个代码,go test执行到这个函数时,就会以```"-test.run=TestHelperProcess -- "```   为参数执行go test执行程序。go test的可执行文件,执行机制是执行 test.run指定的测试用例。   那么现在就相当与指定执行```TestHelperProcess```这个用例了。   详细解释见:[测试 os/exec.Command](https://npf.io/2015/06/testing-exec-command/)

应用实例

  1. 第一步,需要把产品代码中对os.exec的调用做个封装:var execCommand = exec.Command
    然后代码中使用的是execCommand,这样做是便于我们下面进行Fake
  2. 测试Fake的写法如下,可以和实际的测试用例不在同一个包中:
    func fakeExecCommand(command string, args...string) *exec.Cmd {
        cs := []string{"-test.run=TestHelperProcess", "--", command}
        cs = append(cs, args...)
        cmd := exec.Command(os.Args[0], cs...)
        cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
        return cmd
     }
    
  3. 最终执行对象是一个测试用例,需要跟被测测试用例在一个包下面:
    func TestHelperProcess(t *testing.T){
         if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
             return
         }
         cmdName = os.Args[3] // 0--是 go test 执行程序 1--是 TestHelperProcess 2-- 是“--”
         fmt.Fprintf(os.Stdout, "command name is %s\n ")
         os.Exit(0)
     }
    
  4. 在用例中进行替换使用就可以了
    execCommand = fakeExecCommand
    defer func(){ execCommand = exec.Command }()
    

这个方法相对优雅,而且可以进行针对不同命令进扩展,以及统计等。

参考文档

  1. 测试 os/exec.Command
  2. Unit Testing Exec Command in Golang

—  原创作品许可 — 署名-非商业性使用-禁止演绎 3.0 未本地化版本 — CC BY-NC-ND 3.0   —