TL;DR
如何在不 stub 或注入(inject)它们的情况下独立于其依赖项来测试值对象?
在 Misko Hevery 的博文中 To “new” or not to “new”…他主张以下内容(引自博客文章):
- An Injectable class can ask for other Injectables in its constructor.(Sometimes I refer to Injectables as Service Objects, but that term is overloaded.). Injectable can never ask for a non-Injectable (Newable) in its constructor.
- Newables can ask for other Newables in their constructor, but not for Injectables (Sometimes I refer to Newables as Value Object, but again, the term is overloaded)
现在,如果我有
Quantity
像这样的值对象:class Quantity{
$quantity=0;
public function __construct($quantity){
$intValidator = new Zend_Validate_Int();
if(!$intValidator->isValid($quantity)){
throw new Exception("Quantity must be an integer.");
}
$gtValidator = new Zend_Validate_GreaterThan(0);
if(!$gtvalidator->isValid($quantity)){
throw new Exception("Quantity must be greater than zero.");
}
$this->quantity=$quantity;
}
}
我的
Quantity
值对象的正确构造至少依赖于 2 个验证器。通常我会通过构造函数注入(inject)这些验证器,这样我就可以 stub 他们在测试期间。然而,根据 Misko 的说法,newable 不应该在其构造函数中要求注入(inject)。坦率地说
Quantity
看起来像这样的对象$quantity=new Quantity(1,$intValidator,$gtValidator);
看起来真的很尴尬。使用依赖注入(inject)框架来创建值对象更加尴尬。但是现在我的依赖项被硬编码在
Quantity
中。构造函数,如果业务逻辑发生变化,我无法更改它们。您如何正确设计值(value)对象以测试和遵守注入(inject)剂和新剂之间的分离?
笔记:
最佳答案
值对象应该只包含原始值(整数、字符串、 bool 标志、其他值对象等)。
通常,最好让值对象本身 保护其不变量 .在您提供的 Quantity 示例中,它可以通过检查传入值轻松做到这一点,而无需依赖外部依赖项。但是,我意识到你写
This is just a very very simplified example. My real object my have serious logic in it that may use other dependencies as well.
因此,虽然我将基于 Quantity 示例概述一个解决方案,但请记住,它看起来过于复杂,因为这里的验证逻辑非常简单。
既然你也写
I used a PHP example just for illustration. Answers in other languages are appreciated.
我将在 F# 中回答。
如果您有外部验证依赖项,但仍希望将 Quantity 保留为值对象,则需要 解耦 来自值对象的验证逻辑。
一种方法是定义一个验证接口(interface):
type IQuantityValidator =
abstract Validate : decimal -> unit
在这种情况下,我设计了
Validate
OP 示例中的方法,该方法在验证失败时引发异常。这意味着如果 Validate
方法不会抛出异常,一切都很好。这就是该方法返回 unit
的原因。 .(如果我没有决定在 OP 上设计这个接口(interface),我会更喜欢使用 Specification pattern;如果是这样,我会改为将
Validate
方法声明为 decimal -> bool
。)IQuantityValidator
界面使您能够引入 Composite :type CompositeQuantityValidator(validators : IQuantityValidator list) =
interface IQuantityValidator with
member this.Validate value =
validators
|> List.iter (fun validator -> validator.Validate value)
这个复合只是简单地遍历其他
IQuantityValidator
实例并调用它们的Validate
方法。这使您能够组合任意复杂的验证器图。一个叶子验证器可以是:
type IntegerValidator() =
interface IQuantityValidator with
member this.Validate value =
if value % 1m <> 0m
then
raise(
ArgumentOutOfRangeException(
"value",
"Quantity must be an integer."))
另一种可能是:
type GreaterThanValidator(boundary) =
interface IQuantityValidator with
member this.Validate value =
if value <= boundary
then
raise(
ArgumentOutOfRangeException(
"value",
"Quantity must be greater than zero."))
注意
GreaterThanValidator
类通过其构造函数获取依赖项。在这种情况下,boundary
只是一个 decimal
,所以它是 Primitive Dependency ,但它也可能是多态依赖(A.K.A 服务)。您现在可以从这些构建 block 组成您自己的验证器:
let myValidator =
CompositeQuantityValidator([IntegerValidator(); GreaterThanValidator(0m)])
当您调用
myValidator
与例如9m
或 42m
,它返回没有错误,但如果你用例如调用它9.8m
, 0m
或 -1m
它抛出适当的异常。如果你想构建比
decimal
更复杂的东西,您可以引入工厂,并使用适当的验证器组成工厂。由于 Quantity 在这里非常简单,我们可以将其定义为
decimal
上的类型别名。 :type Quantity = decimal
工厂可能如下所示:
type QuantityFactory(validator : IQuantityValidator) =
member this.Create value : Quantity =
validator.Validate value
value
您现在可以撰写
QuantityFactory
使用您选择的验证器的实例:let factory = QuantityFactory(myValidator)
这将使您提供
decimal
值作为输入,并获取(验证)Quantity
值作为输出。这些调用成功:
let x = factory.Create 9m
let y = factory.Create 42m
虽然这些抛出适当的异常:
let a = factory.Create 9.8m
let b = factory.Create 0m
let c = factory.Create -1m
现在,所有这些都是 非常复杂鉴于示例域的简单性质,但随着问题域变得越来越复杂,complex is better than complicated .
关于oop - 与依赖项隔离的单元测试值对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22263646/