Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/oop/2.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Oop 独立于依赖项的单元测试值对象_Oop_Unit Testing_Dependency Injection_Tdd_Domain Driven Design - Fatal编程技术网

Oop 独立于依赖项的单元测试值对象

Oop 独立于依赖项的单元测试值对象,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

TL;DR
如何在不存根或注入依赖项的情况下,独立于依赖项测试值对象


在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)
My
Quantity
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”类型的生命周期有很大关系,因为我们对特定实例感兴趣。但是,当我们需要注入特定的