views:

191

answers:

5

I have an application that I'm trying to build with at least a nominally DDD-type domain model, and am struggling with a certain piece.

My entity has some business logic that uses some financial calculations and rate calculations that I currently have inside some domain services, as well as some constant values I'm putting in a value object.

I'm struggling with how to have the entity use the logic inside the domain services, or whether the logic inside those services even belongs there. This is what I have so far:

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);
    }

}

I feel like some of that calculation logic does belong in those domain services, but don't really like that I'll have to manually inject those dependencies from my Repository. Is there an alternate way that this should be modeled? Am I wrong in not liking that?

Having read the Blue Book but not really built anything in this style before, I'm looking for guidance.

EDIT

Thanks all for the feedback! Based on what I'm hearing, it sounds like my model should look more like the following. This look better?

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);
    }

}

The domain model ends up being fairly anemic at this point, but as I add features maybe it'll have more to it.

EDIT 2

Okay, I got some more feedback that perhaps my 'calculation' services are more like strategy objects that it's okay for my Entity to depend on. Here's another take at it with more of the logic back in the Entity, and making use of those strategy objects. Thoughts on this? Any issues with instantiating those helpers directly in the Entity? I don't think I'll want to mock those out in my tests, but OTOH I can't test the CalculateFinancialGain method without testing those strategy objects, either.

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);
    }

}
+1  A: 

i would say services use entities, not the other way around.

another thing, not sure on your domain, but are you certain ticket is an entity and not a value object?

E Rolnicki
My understanding of an Entity was that it has a specific identity in the context of the domain. In this case, the Id on the Ticket is basically case number. So, I think so? :-) Would you say differently?Re: Service using entities, would you then model it with a FinancialGainCalculationService, that used a Ticket and the other two services to get that Financial Gain number? Doesn't that kind of lead to an anemic domain model, if the biz logic is in the service?Thanks!
Craig Vermeer
so the ticket id is used in your business language as a reference?i do not think that entities should be able to perform work that changes their own state...i believe this work belongs in a service level which can then reconcile those changes back into the repo.
E Rolnicki
let me clarify, in the classic Order model.an Order, being an aggregate, can add product detail lines to itself, however I do not believe an order should be able to Place itself, which would save the new state back into that aggregate's repository...i believe this saving of state should occur in a service level
E Rolnicki
To the question re: Ticket ID being in the business language, yes that's the case.
Craig Vermeer
another question: do your calculators rely on repositories? if not, they probably are not services. DDD is not a world of only entities, value objects, and services. there can be other 'helper' objects that move things along.
E Rolnicki
@E RE: relying on repositories: No, not directly. There are some constant rates whose value object is accessed by a factory (that will get the rates from configuration) and that feeds into the Rate Calculator, but that's it. I'll take another stab at it, moving the logic back into the entity
Craig Vermeer
+1  A: 

You've actually struck on a question that there has been quite a bit of discussion on. There are believers on both sides of the tracks so you need to decide for yourself what makes the most sense.

Personally I don't have my entities use services as it creates a whole lot of work around the "How do I cleanly get services into my entities?" question.

It looks to me like CalculateFinancialGains() is more of a service level call. This does lead to Ticket being very anemic but I assume it has other behavior? And if it doesn't that's probably a smell...

ShaneC
+3  A: 

Have your service accept the Ticket entity as a parameter. Services should be stateless and the same service should be able to provide its services to any number of entities.

In your situation I would pull the FinancialCalculatorService and RateCalculatorService out of your entity and make the methods on each service accept the Ticket entity as a parameter.

Take a second and read pg. 105 of Domain-Driven Design by Eric Evans

Jon Erickson
Thanks -- it's been about a year since I read the book, so many of the finer points are forgotten. So, it sounds like whether something is an Service or not depends on whether it fits into the Ubiquitous language. In that case, the RateCalculatorService definitely fits that bill for this domain. FinancialCalculator, not as much.
Craig Vermeer
I just finished reading the book from cover to cover within the past week so it has been fresh in my mind =)
Jon Erickson
In your case what you said makes some sense, you would have a 'FinancialCalculator' that acts as a service to calculate a 'Financial Gain' based upon a 'Rate' that you get by asking the 'Ticket' for (and the ticket just uses a 'RateCalculator' in order to encapsulate the logic of figuring out that rate). It still feels as though the 'RateCalculator' can be a service so that you don't have to instantiate one every time you reconstitute a ticket entity, but I would do what makes more sense in accordance to your domain's ubiquitous language.
Jon Erickson
+1  A: 

This question is actually an example of a discussion that is in the book "Clean Code" (pp 96-97). The underlying question is whether or not to use a procedural approach or a object oriented approach. Hope I'm not in violation repeating a couple parts here, but here is what Bob Martin states for guidance:

Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. OO code, on the other hand, makes it easy to add new classes without changing existing functions.

The compliment is also true:

Procedural code makes it hard to add new data structures because all the functions must change. OO code makes it hard to add new functions because all the classes must change.

My understanding that a DDD "Value type" would be what Bob Martin calls a data structure.

Hope this helps and doesn't just add to the noise :)

Steve Horn
+1  A: 

Given what we've seen of the classes, I don't think they're really services in the blue book sense, and I would keep the calculators in Ticket.

Neither FinancialCalculatorService or RateCalculationService has dependencies on domain entities - they both operate on primitive values. Applications shouldn't have to worry about how to calculate the financial gain that would result from a ticket, so it's valuable to encapsulate that information inside the ticket itself.

If they really don't have dependencies on domain entities, consider thinking of them as 'standalone classes' rather than 'services' (once again, in blue book terminology). It's certainly appropriate for Ticket depend on strategy objects (FinancialCalculator and RateCalculator) that do not themselves have exotic dependencies and do not themselves modify the state of domain entities.

Update for Edit 2. I think one of the advantages of making the calculators separate classes is that you can test them independently of Ticket. Strictly speaking, tickets aren't responsible for performing those calculations, they're responsible for making the right calls to those collaborating classes. So I'd be inclined to make them inject-able / mock-able as they were in your initial example.

Jeff Sternal
And injecting them into my entity goes back to some of my initial pain , because that's some extra work I'll need to do in my repository. I guess I can see why this is a subject of so much debate :-)
Craig Vermeer