I am trying to refactor a large application to use more DDD principles as and when I get chance. I am concentrating on the very simple areas first. The app currently has a very simple Domain Service (one of many) as it stands (see option 1). Please see the code below:
Option 1 - Live
public IEnumerable<System.Collections.Generic.KeyValuePair<decimal, int>> CalculateDenominations(decimal cost, ICurrency currency)
{
var target = cost;
foreach (var denomination in currency.AvailableDenominations)
{
var numberRequired = target / denomination;
if (numberRequired >= 1)
{
int quantity = (int)Math.Floor(numberRequired);
yield return new KeyValuePair<decimal, int>(denomination, quantity);
target = target - (quantity * denomination);
}
}
}
Option 2
public sealed class DenominationsRequired
{
private readonly decimal _cost;
private readonly ICurrency _currency;
public DenominationsRequired(decimal cost, ICurrency currency)
{
if (currency == null)
throw new ArgumentNullException("Currency cannot be null", "ICurrency");
if (cost < 0)
throw new ArgumentException("Cost cannot be less than zero", "Cost");
if (decimal.Round(cost, 2) != cost)
throw new ArgumentException(string.Concat("Cost has too many decimal places. It should only have: ", currency.DecimalPlaces), "Cost");
_cost = cost;
_currency = currency;
}
public decimal Cost
{
get { return _cost; }
}
public ICurrency Currency
{
get { return _currency; }
}
public IEnumerable<System.Collections.Generic.KeyValuePair<decimal, int>> CalculateDenominations()
{
var target = _cost;
foreach (var denomination in _currency.AvailableDenominations)
{
var numberRequired = target / denomination;
if (numberRequired >= 1)
{
int quantity = (int)Math.Floor(numberRequired);
yield return new KeyValuePair<decimal, int>(denomination, quantity);
target = target - (quantity * denomination);
}
}
}
}
I am thinking about refactoring the Domain Service (option 1 - live) into a Value Object (option 2) so that I can add validation. I would be grateful for critical comments about this approach. Is a ValueObject (option 2) "better" than a domain service (option 1) for this? I believe it is acceptable to put caclculations in Value Objects as long as they do not mutate state.
I am specifically interested to know:
Is a Value Object "better" than a Domain Service for this specific requirement?
Is it wise to have a property called:
Currency.DecimalPlaces
(indicates how many decimal places the currency allows e.g. 2 for pounds and 2 for dollars - some currencies allow 3 or 4 decimal places), which is used in theDenominationsRequired
constructor for validation?Should I keep option 1 or go with option 2? If option 1, then how do I approach validation?
denomination
keys insideCalculateDenominations
guaranteed to be unique? If so, you should change that method to return aDictionary<decimal, int>
instead. That also implies you can't use an iterator block. Personally I would just make that method a property as well, since it is pure and the rest of your class is immutable (assuming theICurrency
doesn't mutate). \$\endgroup\$IEnumerable
. As I've also said in one of my previous comments: instead of returning key-value-pairs you should return tuples with strong property names. \$\endgroup\$Dictionary
. By returningIEnumerable
, the caller of this method no longer has a runtime guarantee that the keys/denominations are unique. \$\endgroup\$