Just a quick run by posting from some discussions yesterday. I see many people using the State Pattern in their domains. More often than not though they are making a mistake that can be seen by analyzing the OO aspects of their code. Let’s go through a quick example of a Loan Application.
When its “In process” you can edit data on it or submit for approval.
When its “awaiting approval” you can no longer edit details data but you can approve or deny the application.
When its “completed” it has become immutable and can no longer be edited but may have some getters on it for viewing of historical data
I went through and made a quick little example of this
public interface ILoanApplicationState
{
void Approve();
void Deny();
InterestRate GetApprovedInterestRate();
InterestRate CalculateLikelyInterestRate();
void SubmitForApproval();
void ChangeDetails(LoanDetails details);
}
public class LoanApplicationState : ILoanApplicationState
{
private LoanApplication parent;
public LoanApplicationState(LoanApplication Parent)
{
parent = Parent;
}
public virtual void Approve() {}
public virtual void Deny(){}
public virtual InterestRate GetApprovedInterestRate() {}
public virtual InterestRate CalculateLikelyInterestRate() {}
public virtual void SubmitForApproval(){}
public virtual void ChangeDetails(LoanDetails details) {}
}
public class InProcessState : LoanApplicationState
{
public InProcessState(LoanApplication Parent) : base(Parent) { }
public override void Approve()
{
throw new InvalidOperationException("Cannot approve an inprocess application");
}
public override void Deny()
{
throw new InvalidOperationException("Cannot deny an inprocess application");
}
public override InterestRate GetApprovedInterestRate()
{
throw new InvalidOperationException("No approved interest rate exists on an in process application");
}
public override InterestRate CalculateLikelyInterestRate()
{
return InterestRate.Default;
}
public override void SubmitForApproval()
{
//change parent state to submitted
}
public override void ChangeDetails(LoanDetails details)
{
//change parent details
}
}
public class AwaitingApprovalState : LoanApplicationState
{
public InProcessState(LoanApplication Parent) : base(Parent) { }
public override void Approve()
{
//change parent state to approved and issue some behavior
}
public override void Deny()
{
//change parent state to denied and issue some behavior
}
public override InterestRate GetApprovedInterestRate()
{
throw new InvalidOperationException("No approved interest rate exists on an awaiting approval application");
}
public override InterestRate CalculateLikelyInterestRate()
{
return InterestRate.Default;
}
public override void SubmitForApproval()
{
throw new InvalidOperationException("Already awaiting approval");
}
public override void ChangeDetails(LoanDetails details)
{
throw new InvalidOperationException("No approved interest rate exists on an in process application");
}
}
public class CompletedApplicationState : LoanApplicationState
{
public InProcessState(LoanApplication Parent) : base(Parent) { }
public override void Approve()
{
throw new InvalidOperationException("Application has already been completed");
}
public override void Deny()
{
throw new InvalidOperationException("Application has already been completed");
}
public override InterestRate GetApprovedInterestRate()
{
//return parents approved interest rate
}
public override InterestRate CalculateLikelyInterestRate()
{
//return parents approved interest rate
}
public override void SubmitForApproval()
{
throw new InvalidOperationException("Already completed");
}
public override void ChangeDetails(LoanDetails details)
{
throw new InvalidOperationException("Application is already completed");
}
}
OK enough yucky code you guys get the idea (the state pattern is also very verbose). The idea here is we plug in these state objects to our object and they handle the behavior changes from the original object.
My contention here is that you should not use the State Pattern like this. Where is the polymorphism? What if we think about the gross violation of LSP that we just created? This does however happen way too often.
The solution is that we really have three classes here InProcessApplication, AwaitingApprovalApplication, and CompletedApplication.
public class InProcessApplication {
InProcessApplication SubmitForApproval(ILoanProcessingService processor) { … }
}
this will force us to end up with smaller and easier to understand classes… and I mean hey, its not like polymorphism would have worked before anyways. It also makes our Ubiquitous Language more concise.
There is a particular code smell here that will help us distinguish whether we want the State Pattern or separate classes. Do some of the data/behavior only make sense in certain states? If so use three separate classes. Going along with this, if we find “throws” in our state implementors we should realize through LSP that we are doing something bad.
Posted
Tue, Mar 9 2010 1:37 AM
by
Greg