Oop 独立于依赖项的单元测试值对象
TL;DROop 独立于依赖项的单元测试值对象,oop,unit-testing,dependency-injection,tdd,domain-driven-design,Oop,Unit Testing,Dependency Injection,Tdd,Domain Driven Design,TL;DR 如何在不存根或注入依赖项的情况下,独立于依赖项测试值对象 在Misko Hevery的博文中,他主张如下(引自博文): 可注入类可以在其构造函数中请求其他可注入项(有时我将可注入项称为服务对象,但 这个词太夸张了。可注射的永远不能在其构造函数中要求非可注射的(可更新的) Newables可以在其构造函数中请求其他Newables,但不能请求injectable(有时我将Newables称为Value对象,但是 再一次,这个术语被重载了) 现在,如果我有一个Quantityval
如何在不存根或注入依赖项的情况下,独立于依赖项测试值对象
在Misko Hevery的博文中,他主张如下(引自博文):
- 可注入类可以在其构造函数中请求其他可注入项(有时我将可注入项称为服务对象,但 这个词太夸张了。可注射的永远不能在其构造函数中要求非可注射的(可更新的)
- Newables可以在其构造函数中请求其他Newables,但不能请求injectable(有时我将Newables称为Value对象,但是 再一次,这个术语被重载了)
Quantity
value对象,如下所示:
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;
}
}
type QuantityFactory(validator : IQuantityValidator) =
member this.Create value : Quantity =
validator.Validate value
value
public void Validate(IQuantityValidator validator)
MyQuantity
value对象的正确构造依赖于至少两个验证器。通常,我会通过构造函数注入这些验证器,以便在测试期间存根它们
然而,根据Misko的说法,一个newable不应该在它的构造函数中要求注入。坦率地说,一个看起来像这样的数量对象
$quantity=新数量(1,$intValidator,$gtValidator)代码>看起来很尴尬
使用依赖项注入框架来创建一个值对象甚至更加尴尬。然而,现在我的依赖项在Quantity
构造函数中是硬编码的,如果业务逻辑发生变化,我没有办法修改它们
您如何正确地设计值对象,以测试和遵守可注入项和可更新项之间的分离
注:
这只是一个非常简单的例子。我的真实对象中有严重的逻辑,可能也会使用其他依赖项
我使用了一个PHP示例来进行说明。请用其他语言回答
避免依赖于非值类型的值类型。还要避免执行验证和抛出异常的构造函数。在您的示例中,我有一个验证和创建数量的工厂类型。值对象应该只包含基本值(整数、字符串、布尔标志、其他值对象等)
通常,最好让值对象本身保护其不变量。在您提供的Quantity示例中,它可以通过检查传入值而不依赖外部依赖性轻松做到这一点。然而,我意识到你在写作
这只是一个非常简单的例子。我的真实对象中有严重的逻辑,可能也会使用其他依赖项
因此,虽然我将根据Quantity示例概述一个解决方案,但请记住它看起来过于复杂,因为这里的验证逻辑非常简单
既然你也写
我使用了一个PHP示例来进行说明。请用其他语言回答
我将用F#来回答
如果您有外部验证依赖项,但仍希望将数量保留为值对象,则需要将验证逻辑与值对象解耦
一种方法是定义一个用于验证的接口:
type IQuantityValidator =
abstract Validate : decimal -> unit
在本例中,我在OP示例中使用了Validate
方法,该方法在验证失败时抛出异常。这意味着如果Validate
方法没有抛出异常,一切都很好。这就是方法返回unit
的原因
(如果我没有决定在OP上设置此接口的模式,我宁愿使用;如果是这样,我宁愿将Validate
方法声明为decimal->bool
)
IQuantityValidator
界面允许您引入:
此组合只需迭代其他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."))
当您使用例如9m
或42m
调用myValidator
时,它将返回而不出错,但如果您使用例如9.8m
、0m
或-1m
调用它,它将抛出相应的异常
如果您想构建比十进制更复杂的东西,可以引入工厂,并使用适当的验证器组成工厂
由于数量在这里非常简单,我们可以将其定义为decimal
上的类型别名:
type Quantity = decimal
工厂可能是这样的:
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;
}
}
type QuantityFactory(validator : IQuantityValidator) =
member this.Create value : Quantity =
validator.Validate value
value
public void Validate(IQuantityValidator validator)
现在,您可以使用所选的验证器组合一个QuantityFactory
实例:
let factory = QuantityFactory(myValidator)
这将允许您提供decimal
值作为输入,并获取(验证)数量
值作为输出
这些呼叫成功:
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
现在,鉴于示例域的简单性质,所有这些都非常复杂,但随着问题域变得更加复杂,您的场景也可以应用于实体。在某些情况下,实体需要某些依赖项才能执行某些行为。据我所知,最常用的机制是双重分派
我将使用C#作为示例
在您的情况下,您可以有如下内容:
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;
}
}
type QuantityFactory(validator : IQuantityValidator) =
member this.Create value : Quantity =
validator.Validate value
value
public void Validate(IQuantityValidator validator)
正如其他答案所指出的,值对象通常非常简单,可以在构造函数中执行其不变检查。电子邮件值对象就是一个很好的例子,因为电子邮件具有非常特定的结构
稍微复杂一点的可能是一个订单行
,我们需要完全假设地确定它是否应纳税:
public bool IsTaxable(ITaxableService service)
在您引用的文章中,我认为“newable”与我们在DI容器中发现的“transient”类型的生命周期有很大关系,因为我们对特定实例感兴趣。但是,当我们需要注入特定的