I have an application that I'm trying to build with at least a n开发者_运维知识库ominally 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);
}
}
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
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.
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?
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...
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 :)
精彩评论