Reputation: 1848
Problem summary
We have 2 bounded contexts which represent the same data entities, but offer different functionality to operate on said entities. Some of the more complicated calculations are starting to surface in both contexts identically. Do we copy paste and write unit tests? Do we map and extract a common calculation policy object?
The primary objective is maintainability. Anything else is secondary.
Details
I've had a look at this question which touches on my problem, but I want to get some input on a specific design choice.
We have 2 bounded contexts that handles investments. Let's say these have the following classes:
Context - Investing
Context - Reporting
The investing context does actual investing. I.e. shifts funds between bank accounts and investment products and so on.
The reporting context is read-only and exposes some methods to calculate some investment details.
It so happens that a new rule to calculate how much an investor is still allowed to invest in a product contains logic that will be used exactly the same way to report on how much an investor may still invest in a product.
Here is sample pseudo code for those who understand better when looking at code:
namespace Investing
{
public class Investment
{
private List<InvestmentIsntruction> Instructions {get;private set;}
public decimal GetRemainingContributionAllowed()
{
return SOME_PREDEFINED_LIMIT - Instructions.Sum(x => x.Amount);
}
}
}
namespace Reporting
{
public class Investment
{
private List<InvestmentIsntruction> Instructions {get;private set;}
public decimal GetRemainingContributionAllowed()
{
return SOME_PREDEFINED_LIMIT - Instructions.Sum(x => x.Amount);
}
}
}
The formula for calculating the limit here is simplified. Supposing that the actual formula is much more complicated, how do we effectively share this logic? We are not too picky about the solution. We would like a solution that can be motivated properly from a design point of view.
Solutions so far
1) Just copy paste the logic and write unit tests to ensure that the results are always the same. The reasoning would be that the contexts are different after all. Perhaps in the future the reporting formula may differ for example, and the tie between the contexts would be easy to break (adjust unit test).
2) Extract a common calculator object with the following interface:
decimal GetRemainingAllowance(IEnumerable<IInvestmentIntsruction> instructions, decimal predefinedLimit);
Implement IInvestmentInstruction interface on each context's classes. The motivation here would of course be a unified place to put a calculation. What we don't like is the interface on the InvestmentInstruction class. If we have more overlapping calculations later, there might be dozens of interfaces like this to implement. For example:
public class InvestmentInstruction : ILimitCalculationInstruction, IOtherCalcuationInstruction, IYetSomethingElse{}
Each interface exposing something useful to some common overlapping calculation.
3) This is our preferred solution so far - Extract a completely independent calculator object with it's own interface as in solution 2. Then map domain objects to a common model when they need to be used by this calculator. For example:
public class Calculator
{
public decimal GetRemainingAllowance(IEnumerable<IInvestmentIntsruction> instructions, decimal predefinedLimit){...}
}
public class DedicatedCalcuatorInvestmentIsntructionModel : IInvestmentInstruction {}
namespace Investing
{
public class Investment
{
private List<InvestmentIsntruction> Instructions {get;private set;}
public decimal GetRemainingContributionAllowed(Calculator calculator)
{
var mappedInstructions = this.Instructions.Select(x => new DedicatedCalcuatorInvestmentIsntructionModel(){//Assign properties} );
return calculator.GetRemainingAllowance(mappedInstructions, SOME_PREDEFINED_LIMIT);
}
}
}
namespace Reporting
{
public class Investment
{
private List<InvestmentIsntruction> Instructions {get;private set;}
public decimal GetRemainingContributionAllowed(Calculator calculator)
{
var mappedInstructions = this.Instructions.Select(x => new DedicatedCalcuatorInvestmentIsntructionModel(){//Assign properties} );
return calculator.GetRemainingAllowance(mappedInstructions, SOME_PREDEFINED_LIMIT);
}
}
}
...
This seems to solve the duplication issue in a decent manner without too tightly coupling the various contexts to the calculation. It's also the solution that feels the most like overengineering the issue.
Question
Which of our solutions would you recommend if any? What other ways are there to solve this problem?
Please let me know if I've failed to provide some critical info. I'm pretty deep into the problem so it's easy to forget which parts are not self-explanatory.
Upvotes: 0
Views: 578
Reputation: 13256
Another option is to denormalize the required GetRemainingContributionAllowed
value and store that. Storing that value could be done whenever the relevant changes are made to Investment
. An alternative is to have your report generation be somewhat of a process where the first step is to have the domain calculate the value and then storing that value. The reporting bit would then only read it.
Reporting should not be performing complex business functionality. Simple arithmetic and the like is OK but if you are duplicating what your domain is doing perhaps keep that in the domain.
Upvotes: 2