Reputation: 2303
Imagine that we have an Aggregate that has a life cycle, such that it can change its behavior during its lifetime. During the first part of its life, it can do some things and during the second part, it can do other things.
I´d like to hear opinions on how should we restrict what the aggregate can do on each phase. To make it a little more tangible, lets take an financial trade as an aggreagate example.
The trade clearly has 3 distinct phases, which I´ll call Typed, Validated and Submitted
My first thought is to pollute the aggregate with InvalidOperationExceptions
, which I really don´t like:
public class Trade
{
private enum State { Typed, Validated, Submited }
private State _state = State.Typed;
public Guid Id { get; }
public Contract Contract { get; }
public decimal Price { get; }
public Trade (Guid id, Contract contract, decimal price) { ... }
private string _validationReason = null;
private AccountingInformation _accInfo = null;
public void Validate(string reason) {
if (_state != State.Typed)
throw new InvalidOperationException (..)
...
_validationReason = reason;
_state = State.Validated;
}
public string GetValidationReason() {
if (_state == State.Typed)
throw new InvalidOperationException (..)
return _validationReason;
}
public void SubmitToLedger(AccountingInformation info) {
if ((_state != State.Validated))
throw new InvalidOperationException (..)
...
}
public AccountingInfo GetAccountingInfo() { .. }
}
I can do something like a Maybe pattern, to avoid the exceptions on the Get...
methods. But that would not work for the behavior methods (Validate
, SubmitToLedger
, etc)
Oddly, if I were to be working on a functional language (such as F#), I would probably create a different type for each state.
type TypedTrade = { Id : Guid; Contract: Contract; Price : decimal }
type ValidatedTrade = { Id : Guid;
Contract: Contract;
Price : decimal;
ValidationReason : string}
type SubmittedTrade = { Id : Guid;
Contract: Contract;
Price : decimal;
ValidationReason : string;
AccInfo : AccountingInfo }
// TypedTrade -> string -> ValidatedTrade
let validateTrade typedTrade reason =
...
{ Id = typedTrade.Id; Contract = typedTrade.Contract;
Price = typedTrade.Price; Reason = reason }
// ValidatedTrade -> AccountingInfo -> SubmittedTrade
let submitTrade validatedTrade accInfo =
...
{ Id = validatedTrade.Id;
Contract = validatedTrade.Contract;
Price = validatedTrade.Price;
Reason = validatedTrad.Reason;
AccInfo = accInfo }
And the problem would gracefully go away. But to do that in OO, I would have to make my aggregate immutable and maybe create some kind o hierarchy (in which I would have to hide base methods !? ouch!).
I just wanted an opinion on what you guys do on these situations, and if there is a better way.
Upvotes: 0
Views: 84
Reputation: 14064
You could have one class per state instead of a single class. See this post by Greg Young : http://codebetter.com/gregyoung/2010/03/09/state-pattern-misuse/
The usual problem with the State pattern is the friction with persistence concerns and especially ORMs. It's up to you to decide if the better robustness and type safety is worth the trouble.
Upvotes: 1
Reputation: 1044
I like the idea of having different types for each state. Its a clean design in my opinion. From a logical view a newly created trade is definitly something different than a submitted trade.
public Interface ITrade
{
Guid Id { get; }
Contract Contract { get; }
decimal Price { get; }
}
public class Trade : ITrade
{
public Trade(Guid id, Contract contract, decimal price)
{
Id = id;
Contract = contract;
Price = price;
}
Guid Id { get; }
Contract Contract { get; }
decimal Price { get; }
public ValidatedTrade Validate(string reason)
{
return new ValidatedTrade(this, reason);
}
}
public class ValidatedTrade : ITrade
{
private ITrade trade;
private string validationReason;
public ValidatedTrade(Trade trade, string validationReason)
{
this.trade = trade;
this.validationReason = validationReason;
}
Guid Id { get { return trade.Id; } }
Contract Contract { get { return trade.Contract ; } }
decimal Price { get { return trade.Price ; } }
public string GetValidationReason()
{
return validationReason;
}
public SubmittedTrade SubmitToLedger(AccountingInfo accountingInfo)
{
return new SubmittedTrade(this, accountingInfo);
}
}
public class SubmittedTrade : ITrade
{
private ITrade trade;
private AccountingInfo accountingInfo;
public SubmittedTrade(ValidatedTrade trade, AccountingInfo accountingInfo)
{
this.trade = trade;
this.accountingInfo = accountingInfo;
}
Guid Id { get { return trade.Id; } }
Contract Contract { get { return trade.Contract ; } }
decimal Price { get { return trade.Price ; } }
public AccountingInfo GetAccountingInfo() { .. }
}
Upvotes: 2