问题 DDD实体利用服务


我有一个应用程序,我正在尝试用至少一个名义上的DDD类型域模型构建,并且正在与某个部分进行斗争。

我的实体有一些业务逻辑,它使用我目前在某些域服务中的一些财务计算和速率计算,以及我在一个值对象中放置的一些常量值。

我正在努力解决如何让实体使用域服务中的逻辑,或者这些服务中的逻辑是否属于那里。这是我到目前为止:

public class Ticket
{
    public Ticket(int id, ConstantRates constantRates, FinancialCalculationService f, RateCalculationService r)
    {
        Id = id;
        ConstantRates = constantRates;
        FinancialCalculator = f;
        RateCalculator = r;
    }

    private FinancialCalculationService FinancialCalculator { get; set; }

    private RateCalculationService RateCalculator { get; set; }

    private ConstantRates ConstantRates { get; set; }

    public int Id { get; private set; }

    public double ProjectedCosts { get; set; }

    public double ProjectedBenefits { get; set; }

    public double CalculateFinancialGain()
    {
        var discountRate = RateCalculator.CalculateDiscountRate(ConstantRates.Rate1, ConstantRates.Rate2,
                                                                ConstantRates.Rate3);

        return FinancialCalculator.CalculateNetPresentValue(discountRate,
                                                            new[] {ProjectedCosts*-1, ProjectedBenefits});
    }
}


public class ConstantRates
{
    public double Rate1 { get; set; }
    public double Rate2 { get; set; }
    public double Rate3 { get; set; }
}

public class RateCalculationService
{
    public double CalculateDiscountRate(double rate1, double rate2, double rate3 )
    {
        //do some jibba jabba
        return 8.0;
    }
}

public class FinancialCalculationService
{
    public double CalculateNetPresentValue(double rate, params double[] values)
    {
        return Microsoft.VisualBasic.Financial.NPV(rate, ref values);
    }

}

我觉得某些计算逻辑确实属于那些域服务,但不是真的喜欢我必须从我的存储库手动注入这些依赖项。是否有另一种方式可以建模?我不喜欢那个吗?

读过蓝皮书但之前没有真正构建过这种风格的东西,我正在寻找指导。

编辑

谢谢大家的反馈!基于我所听到的,听起来我的模型看起来应该更像下面的内容。这看起来更好?

public class Ticket
{
    public Ticket(int id)
    {
        Id = id;
    }

    private ConstantRates ConstantRates { get; set; }

    public int Id { get; private set; }

    public double ProjectedCosts { get; set; }

    public double ProjectedBenefits { get; set; }

    public double FinancialGain { get; set; }
}



public class ConstantRates
{
    public double Rate1 { get; set; }
    public double Rate2 { get; set; }
    public double Rate3 { get; set; }
}

public class FinancialGainCalculationService
{
    public FinancialGainCalculationService(RateCalculationService rateCalculator, 
        FinancialCalculationService financialCalculator,
        ConstantRateFactory rateFactory)
    {
        RateCalculator = rateCalculator;
        FinancialCalculator = financialCalculator;
        RateFactory = rateFactory;
    }

    private RateCalculationService RateCalculator { get; set; }
    private FinancialCalculationService FinancialCalculator { get; set; }
    private ConstantRateFactory RateFactory { get; set; }

    public void CalculateFinancialGainFor(Ticket ticket)
    {
        var constantRates = RateFactory.Create();
        var discountRate = RateCalculator.CalculateDiscountRate(constantRates.Rate1, constantRates.Rate2,
                                                                constantRates.Rate3);

        ticket.FinancialGain = FinancialCalculator.CalculateNetPresentValue(discountRate,
                                                            new[] {ticket.ProjectedCosts*-1, ticket.ProjectedBenefits});
    }
}

public class ConstantRateFactory
{
    public ConstantRates Create()
    {
        return new ConstantRates();
    }
}

public class RateCalculationService
{
    public double CalculateDiscountRate(double rate1, double rate2, double rate3 )
    {
        //do some jibba jabba
        return 8.0;
    }
}

public class FinancialCalculationService
{
    public double CalculateNetPresentValue(double rate, params double[] values)
    {
        return Microsoft.VisualBasic.Financial.NPV(rate, ref values);
    }

}

域模型在这一点上最终变得相当贫乏,但是当我添加功能时,它可能会有更多功能。

编辑2

好的,我得到了更多的反馈,也许我的“计算”服务更像是战略对象,我的实体可以依赖它。这是对实体的更多逻辑的另一种看法,并利用这些策略对象。对此的想法?直接在实体中实例化这些助手的任何问题?我认为我不想在我的测试中嘲笑那些,但OTOH我也无法在不测试这些策略对象的情况下测试CalculateFinancialGain方法。

public class Ticket
{
    public Ticket(int id, ConstantRates constantRates)
    {
        Id = id;
        ConstantRates = constantRates;
    }

    private ConstantRates ConstantRates { get; set; }

    public int Id { get; private set; }

    public double ProjectedCosts { get; set; }

    public double ProjectedBenefits { get; set; }

    public double CalculateFinancialGain()
    {
        var rateCalculator = new RateCalculator();
        var financeCalculator = new FinanceCalculator();
        var discountRate = rateCalculator.CalculateDiscountRate(ConstantRates.Rate1, ConstantRates.Rate2,
                                                                ConstantRates.Rate3);

        return financeCalculator.CalculateNetPresentValue(discountRate,
                                                            ProjectedCosts*-1, 
                                                            ProjectedBenefits); 
    }
}

public class ConstantRates
{
    public double Rate1 { get; set; }
    public double Rate2 { get; set; }
    public double Rate3 { get; set; }
}

public class RateCalculator
{
    public double CalculateDiscountRate(double rate1, double rate2, double rate3 )
    {
        //do some jibba jabba
        return 8.0;
    }
}

public class FinanceCalculator
{
    public double CalculateNetPresentValue(double rate, params double[] values)
    {
        return Microsoft.VisualBasic.Financial.NPV(rate, ref values);
    }

}

12660
2018-03-04 16:26


起源

你能描述一下“Ticket”与域名的关系吗? - Steve Horn
我认为门票是我在这里的聚合根。它基本上是应用程序的增强/缺陷请求。该应用程序正在尝试根据预计的成本/收益计算特定维护票证的“价值”。 - Craig Vermeer
你在使用IoC模式吗?我建议将这些计算器移动到可以注入的属性。将使测试负载更容易。 - E Rolnicki
是的,我正在使用IoC容器。现在我考虑一下,我可以轻松地将这些注入我的实体。我也将我的存储库用作工厂,但我认为我现在需要将它们分开。根据我的理解,如果我使用的ORM会更难做(注入依赖项,那就是),但我不在这里,所以应该很容易。 - Craig Vermeer


答案:


让您的服务接受 Ticket 实体作为参数。服务应该是无状态的,同一服务应该能够为任意数量的实体提供服务。

在你的情况下,我会拉 FinancialCalculatorService 和 RateCalculatorService 超出您的实体并使每个服务上的方法接受 Ticket 实体作为参数。

花一点时间阅读 皮克。 105 的 Eric Evans的领域驱动设计


6
2018-03-04 16:51



谢谢 - 自从我读完这本书已经有一年了,所以很多细节都被遗忘了。所以,听起来某件事是否是服务取决于它是否适合普遍存在的语言。在这种情况下,RateCalculatorService肯定适合该域的账单。 FinancialCalculator,不是那么多。 - Craig Vermeer
我刚刚在过去的一周内从封面到封面阅读了这本书,所以它在我脑海里一直很新鲜=) - Jon Erickson
在你的情况下,你所说的是有道理的,你会有一个'FinancialCalculator'作为一项服务,根据你通过询问'Ticket'得到的'Rate'来计算'财务收益'(和门票只是使用'RateCalculator'来封装计算该速率的逻辑)。它仍然感觉好像'RateCalculator'可以是一个服务,所以你不必每次重建一个票务实体时实例化一个,但我会根据你的域无处不在的语言做更有意义的事情。 - Jon Erickson


鉴于我们所看到的课程,我不认为他们是真的 服务 在里面  蓝皮书的感觉,我会保留计算器 Ticket

也不 FinancialCalculatorService 要么 RateCalculationService 依赖于域实体 - 它们都在原始值上运行。应用程序不必担心如何计算票证可能带来的经济收益,因此将这些信息封装在票证本身中是很有价值的。

如果他们真的不依赖于域实体,请考虑将它们视为“独立类”​​而不是“服务”(再次,在蓝皮书术语中)。这当然是合适的 Ticket 依赖战略对象(FinancialCalculator 和 RateCalculator)它们本身不具有外来依赖性,并且不会自己修改域实体的状态。

编辑2的更新。我认为其中之一 优点 使计算器分开的类是你可以独立测试它们 Ticket。严格来说,门票不负责执行这些计算,他们负责对这些合作课程进行正确的调用。所以我倾向于让它们像你在最初的例子中那样注入/模拟。


3
2018-03-04 17:48



将它们注入我的实体可以追溯到我最初的一些痛苦,因为这是我需要在我的存储库中做的一些额外工作。我想我可以看出为什么这是一个很多辩论的主题:-) - Craig Vermeer


我会说服务使用实体,而不是相反。

另一件事,在您的域名上不确定,但您确定票证是实体而不是价值对象吗?


2
2018-03-04 16:34



我对实体的理解是它在域的上下文中具有特定的身份。在这种情况下,票证上的Id基本上是案例编号。所以,我是这么认为的? :-)你会说不同吗? Re:使用实体服务,然后你会使用FinancialGainCalculationService对其进行建模,使用Ticket和其他两个服务来获得财务增益数字吗?如果商业逻辑在服务中,那么这种导致贫血领域模型的不是吗?谢谢! - Craig Vermeer
所以票证ID在您的商务语言中用作参考?我不认为实体应该能够执行改变自己状态的工作......我相信这项工作属于服务级别,然后可以将这些更改调和回仓库。 - E Rolnicki
让我在经典的Order模型中澄清一下。作为聚合的订单可以向其自身添加产品详细信息行,但是我不相信订单应该能够放置自己,这会将新状态保存回聚合的存储库...我相信这种状态的保存应该发生在服务级别 - E Rolnicki
另一个问题:你的计算器是否依赖于存储库?如果没有,他们可能不是服务。 DDD不仅仅是实体,价值对象和服务的世界。可以有其他'帮助'对象移动事物。 - E Rolnicki
@E Rolnicki - 虽然同意并非一切都必须是Entities,ValueObjects和Services,如果手头的对象不访问存储库,它可能不是我觉得难以遵循的服务。据我所知,对服务进行分类的主要原因是它没有状态或标识,而是执行操作或抽象复杂的任务。 - jpierson


你实际上已经提出了一个问题,即已经进行了相当多的讨论。在曲目的两边都有信徒,所以你需要自己决定最有意义的东西。

就个人而言,我没有我的实体使用服务,因为它围绕“如何干净地将服务提供给我的实体?”创建了大量工作。题。

在我看来,像CalculateFinancialGains()更像是一个服务级别的调用。这确实导致Ticket非常贫血,但我认为它有其他行为?如果不是那可能是一种气味......


2
2018-03-04 16:43





这个问题实际上是“清洁代码”一书中讨论的一个例子(第96-97页)。根本问题是是否使用程序方法或面向对象方法。希望我不违反重复这里的几个部分,但这是鲍勃·马丁所说的指导:

过程代码(使用数据结构的代码)使得在不改变现有数据结构的情况下添加新函数变得容易。另一方面,OO代码使得在不改变现有功能的情况下添加新类变得容易。

赞美也是如此:

程序代码使得添加新数据结构变得困难,因为所有功能都必须改变。 OO代码使得添加新函数变得很困难,因为所有类都必须更改。

我理解DDD“值类型”将是Bob Martin称之为数据结构的东西。

希望这会有所帮助,而不只是增加噪音:)


1
2018-03-04 17:14