Practical Patterns: Refactoring with CoR Pattern

This is a 3rd post in the series of Design Patterns. Since I would be refactoring an existing solution it would help to refer to my previous posts to understand the sample code base. You may begin with Bridge and move to Decorator pattern to see how the application has evolved.

Using a single if else or a small switch case has never been difficult. But if the options or decisions to make continues to grow, then you would soon end up with an unruly code base. You would very quickly lose loose coupling and with violation of Single Reponsibility Principle, there would be a huge risk of breaking existing code or introducing defects with newer requirements.

Chain of Responsibility (CoR) is very popular frameworks which employ it to process events or requests. One can write a single atomic unit of process and then chain them together to form a complex processing pipelines. OWIN is one such specification through which you can implement complex frameworks by chaining individual OWIN components together.

CoR provides a chance for more than one object to process the request. Every object in the chain can process the request or once the request has been processed by a suitable handler, it can return. The sender or the client needs to only know the initial handler or the base class to launch the request.

There is a special case which requires to be handled in CoR. Since each handler is chained like nodes in a linked list, the end of chain condition requires to be handled. We’ll review the options shortly.

Let us work with adding a new requirement to the existing application. A client requires to go through an approval process for Corporate accounts. Depending on the amount’s value a manager or director can approve the WithDrawal. Let us see how the solution looks without implementing it through the pattern. Let us pick up from where we left from our Decorator implementation.
Introduce a new Employee class which would provide us with a possible approver. The Approve method would determine if this employee can approve or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Employee
{
public Employee(string name, decimal approvalLimit)
{
Name = name;
ApprovalLimit = approvalLimit;
}

public string Name { get; }

private decimal ApprovalLimit { get; }

public bool Approve(decimal amount)
{
if(amount < ApprovalLimit)
{
return true;
}
return false;
}
}

In the Withdraw method, we now check through the list of approvers to determine if we can continue with the withdrawal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Corporate : ITransaction
{
private decimal _balance = 0;
private decimal _overdraft = 0;
const decimal overdraft_factor = 0.1m;
// list of approvers
List<Employee> approvers;
public Corporate(decimal balance)
{
_balance = balance;
Overdraft = _balance;
Console.WriteLine("Opened corporate account with balance {0}", balance);
Console.WriteLine("Overdraft stands at {0}", _overdraft);

approvers = new List<Employee>
{
new Employee("Micky Manager", 1000),
new Employee("Donald Director", 5000)
};
}

// Omitted Deposit for brevity

public void Withdraw(decimal amount)
{
bool amountProcessed = false;
foreach (Employee approver in approvers)
{
amountProcessed = approver.Approve(amount);
if (amountProcessed)
break;
}
if(!amountProcessed)
{
Console.WriteLine("Amount {0} did not get approved", amount);
return;
}
if (amount < Overdraft)
{
_balance -= amount;
Overdraft = _balance;
Console.WriteLine("Widthdrawn: {0} | New balance: {1} | Overdraft: {2}", amount, _balance, Overdraft);
return;
}
Console.WriteLine("Could not withdraw. Current overdraft limit {0}", Overdraft);
}
}

At this point there is no change in the client application.

1
2
3
IAccount account = new CreditAccount("C2102", AccountType.Corporate);
account.Deposit(1000);
account.Withdraw(1000);

In the Withdraw method we now iterate through the list of approvers. Why is this a cause of concern? This means that the algorithm for knowing how the approval process works is known to the caller. In this simple case it is iterating through a list of employees. It goes from one approver to the next until someone in the list approves or the list ends. CoR takes all these reponsibilities away from the caller, while also reducing coupling and granting more flexibility.
To move this to CoR implementation, we introduce a handler. The handler takes the responsibility of the action as well as promoting or delegating to the next handler if the current instance is unable to process.

Though it is not imperative to have an interface (I normally have just an abstract class), in this instance we would go with one.

1
2
3
4
5
interface IWithdrawHandler
{
bool Approve(decimal amount);
void SetNext(IWithdrawHandler next);
}

Implementing the interface we can get a common handler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class WithdrawHandler : IWithdrawHandler
{
private readonly Employee _approver;
private IWithdrawHandler _next;
public WithdrawHandler(Employee approver)
{
_approver = approver;
}
public bool Approve(decimal amount)
{
if (_approver.Approve(amount))
return true;
else
return _next.Approve(amount);
}

public void SetNext(IWithdrawHandler next)
{
_next = next;
}
}

The Corporate class can take in instance of IWithdrawHandler in its constructor.
In AccountBase where we were creating the instance of Corporate class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Corporate : ITransaction
{
private decimal _balance = 0;
private decimal _overdraft = 0;
const decimal overdraft_factor = 0.1m;

private readonly IWithdrawHandler _handler;
//List<Employee> approvers;
public Corporate(decimal balance, IWithdrawHandler handler)
{
_balance = balance;
Overdraft = _balance;
Console.WriteLine("Opened corporate account with balance {0}", balance);
Console.WriteLine("Overdraft stands at {0}", _overdraft);
_handler = handler;
}

public void Withdraw(decimal amount)
{
bool amountProcessed = _handler.Approve(amount);

if(!amountProcessed)
{
Console.WriteLine("No approvers for amount {0}", amount);
return;
}
if (amount < Overdraft)
{
_balance -= amount;
Overdraft = _balance;
Console.WriteLine("Widthdrawn: {0} | New balance: {1} | Overdraft: {2}", amount, _balance, Overdraft);
return;
}
Console.WriteLine("Could not withdraw. Current overdraft limit {0}", Overdraft);
}
}

abstract class AccountBase : IAccount
{
protected ITransaction transactionImp;
public AccountBase(string name, AccountType type)
{
Name = name;
switch (type)
{
case AccountType.Personal:
transactionImp = new AuditTransactionDecorator(new Personal(1000));
break;
case AccountType.Corporate:
IWithdrawHandler micky = new WithdrawHandler(new Employee("Micky Manager", 500));
IWithdrawHandler donald = new WithdrawHandler(new Employee("Donald Director", 1000));
IWithdrawHandler scrooge = new WithdrawHandler(new Employee("Scrooge Owner", 1500));
micky.SetNext(donald);
donald.SetNext(scrooge);
transactionImp = new AuditTransactionDecorator(
new NotifyTransactionDecorator(
new Corporate(5000, micky)));
break;
default:
throw new NotImplementedException("Unknown account type");
}
}
//...
//...
//...
}

There still is no change in the calling application, but only change is how the instance of Corporate is being created. When this is handled by DI or a factory, the change would be even less intrusive. One requirement of CoR is the handling of “end of chain” condition. What happens when we reach the last handler (scrooge here) and is unable to approve? In this case NullReferenceException is thrown. This might be an acceptable behaviour, though you might want to throw a custom exception. The client in this case should be prepared to take a corrective action. Another way is to have the last handler special such that it knows not to try and promote to next handler, but always returns. Or you could have a special handler which is meant to handler ‘end of chain’ event. This is akin to a Null Object where it has a default implementation.