Practical Patterns: Refactoring with Bridge Design Pattern

You would have heard this before - the only thing constant is change. In software how do we prepare ourselves for change? Can we afford to over engineer in anticipation of change or do we create a big ball of mud as we continue to accommodate change?

Only in a perfect world would we be able to realise everything at one time and place. We need to be smart about how we design our software so that we can add/change features to it during its life. You Aren’t Gonna Need It (YAGNI) and SOLID would provide us with a good start so that refactoring need not be a pain. Refactoring is all in the mindset and often teams shy away from this practise.

In this start of my series, I’ll attempt to demonstrate on how we can use design patterns to refactor your existing code. Refactoring is a great approach even when you are working on a new piece of code and not just for modifying or enhancing existing software. It keeps your design light and simple.

Let us look at an overly simplified example of an account which facilitates in conducting a basic transaction. Let us also assume that this came in as your initial requirement and the client is implemented.Initial solution

IAccount is the abstraction which the clients would use.

1
2
3
4
5
6
7
interface IAccount
{
void Deposit(decimal amount);
void Withdraw(decimal amount);
decimal Balance { get; }
string Name { get; }
}

SavingAccount provides concrete implementation for IAccount.

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
class SavingAccount : IAccount
{
decimal _balance = 1000;

public SavingAccountLegacy(string name)
{
Name = name;
Console.WriteLine("Opened personal account with balance {0}", _balance);
}
public void Deposit(decimal amount)
{
_balance += amount;
Console.WriteLine("Deposited: {0} | New balance: {1}", amount, _balance);
}

public void Withdraw(decimal amount)
{
if (amount < _balance)
{
_balance -= amount;
Console.WriteLine("Widthdrawn: {0} | New balance: {1}", amount, _balance);
return;
}
Console.WriteLine("Could not withdraw. Current balance is {0}", _balance);
}

public decimal Balance
{
get { return _balance; }
}

public string Name
{
get;
private set;
}
}

The client is tightly bound to IAccount.

1
2
3
4
5
6
7
8
9
10
class Program
{
static void Main(string[] args)
{
IAccount account = new SavingAccount("A0001");
account.Withdraw(800);
account.Deposit(100);
account.Withdraw(400);
}
}

When you run the app you would see an output which indicates a set of transactions. These are from SavingAccount which portray some behaviour.

1
2
3
4
5
6
/*
Opened personal account with balance 1000
Widthdrawn: 800 | New balance: 200
Deposited: 100 | New balance: 300
Could not withdraw. Current balance is 300
*/

Down the line the inevitable happens and we have new requirements. There is a need to handle processing of transactions depending on whether the accounts are - personal or corporate. When you start seeing changes, it is time to sit up and consider whether it is good time to refactor your code. You anyway have to handle the requirement and there is a cue that you might see more change coming this way. Refactor now than struggle later.

One could just derive again from the existing class and provide the 2 new types. The client code is already bound tightly to the abstraction IAccount. Suppose a credit account is required which varies from savings and also requires to have the two flavours(personal & corporate) supported. As you meet new account types or variation thereof, there would be an explosion of classes due to inheritance. You could prevent this by having conditional checks in the existing classes for each account or transaction. But you would only make your code dirty and end up with technical debt. Here you should favour composition over inheritance while also retaining the abstraction.
Solution with inheritance

Introducing Bridge pattern here would allow you to separate the abstraction from its implementation. Let us dive into refactoring this code while leveraging this pattern.

Like the Bridge pattern states let us introduce another level of abstraction for the implementations. Make note that this abstraction is not exposed or known to the client and we’ll shortly see why this is important. You would continue to inherit IAccount to provide different account types, but their implementations would be abstracted away through ITransaction.

The new refactored diagram.Refactored with Bridge Pattern

The client still goes through the earlier interface IAccount which ensures that none of the refactoring impacts the client. The implementers of IAccount would have a concrete instance of ITransaction. This should be not be exposed to the client or be assigned by the client. This way the client is never bound to ITransaction and we can port the client to any underlying concrete implementation without recompiling it. Only the bridge itself and its implementer needs to be compiled. You can now add new type CreditAccount (varying abstraction), but still use the same implementations. Since you have decoupled your abstraction (IAccount) from the implementation (ITransaction), both can vary independently.

Abstraction for implementations.

1
2
3
4
5
6
interface ITransaction
{
void Deposit(decimal amount);
void Withdraw(decimal amount);
decimal GetBalance();
}

Note that this interface or base class can be different from the abstraction. They can provide a more finer interface.

Two flavours for an account implementation.

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
67
68
69
70
71
72
73
74
75
76
77
78
79
class Personal : ITransaction
{
private decimal _balance = 0;
public Personal(decimal balance)
{
_balance = balance;
Console.WriteLine("Opened personal account with balance {0}", balance);
}
public void Deposit(decimal amount)
{
_balance += amount;
Console.WriteLine("Deposited: {0} | New balance: {1}", amount, _balance);
}

public decimal GetBalance()
{
return _balance;
}

public void Withdraw(decimal amount)
{
if (amount < _balance)
{
_balance -= amount;
Console.WriteLine("Widthdrawn: {0} | New balance: {1}", amount, _balance);
return;
}
Console.WriteLine("Could not withdraw. Current balance is {0}", _balance);
}
}

class Corporate : ITransaction
{
private decimal _balance = 0;
private decimal _overdraft = 0;
const decimal overdraft_factor = 0.1m;
public Corporate(decimal balance)
{
_balance = balance;
Overdraft = _balance;
Console.WriteLine("Opened corporate account with balance {0}", balance);
Console.WriteLine("Overdraft stands at {0}", _overdraft);
}
public void Deposit(decimal amount)
{
_balance += amount;
Overdraft = _balance;
Console.WriteLine("Deposited: {0} | New balance: {1} | Overdraft: {2}",
amount, _balance, Overdraft);
}

public decimal GetBalance()
{
return _balance;
}

public void Withdraw(decimal amount)
{
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);
}

private decimal Overdraft
{
get
{ return _overdraft; }
set
{
_overdraft = Math.Abs(value) + (Math.Abs(value) * overdraft_factor);
}
}
}

Base class which selects the required implementer. This is not absolutely necessary, if we have setup a DI container, we could configure it to inject the concrete implementer directly in to the constructor of the abstraction. But the whole point is to not configure the concrete implementer by the client. Also one could have a factory method to return the required instance.

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
abstract class AccountBase : IAccount
{
// this is protected and hidden from the client
protected ITransaction transactionImp;
public AccountBase(string name, AccountType type)
{
Name = name;
switch (type)
{
case AccountType.Personal:
transactionImp = new Personal(1000);
break;
case AccountType.Corporate:
transactionImp = new Corporate(5000);
break;
default:
throw new NotImplementedException("Unknown account type");
}
}

public abstract void Deposit(decimal amount);
public abstract void Withdraw(decimal amount);
public abstract decimal Balance { get; }
public string Name { get; private set; }

}

enum AccountType
{
Personal = 0,
Corporate
}

Refactored SavingAccount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SavingAccount : AccountBase
{
public SavingAccount(string name, AccountType type) : base(name, type)
{
}

public override void Deposit(decimal amount)
{
transactionImp.Deposit(amount);
}

public override void Withdraw(decimal amount)
{
transactionImp.Withdraw(amount);
}

public override decimal Balance
{
get { return transactionImp.GetBalance(); }
}

}

Finally, the client. Note it still uses the same interface. The client has not changed. You would need a way to decide which implementation to choose.

1
2
3
4
5
6
7
8
9
10
class Program
{
static void Main(string[] args)
{
IAccount account = new SavingAccount("A0001", AccountType.Personal);
account.Withdraw(800);
account.Deposit(100);
account.Withdraw(400);
}
}

Run the client and you see the same output which you had before refactoring.

1
2
3
4
5
6
/*
Opened personal account with balance 1000
Widthdrawn: 800 | New balance: 200
Deposited: 100 | New balance: 300
Could not withdraw. Current balance is 300
*/

But when you have a new interface, you can continue to use the same implementations.

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
class CreditAccount : AccountBase
{
const decimal charge = 0.03m;

public CreditAccount(string name, AccountType type) : base(name, type)
{
}

public override void Deposit(decimal amount)
{
transactionImp.Deposit(amount);
}

public override void Withdraw(decimal amount)
{
amount = amount + (amount * charge);
Console.WriteLine("Credit withdraw surcharge levied: {0}", amount* charge);
transactionImp.Withdraw(amount);
}

public override decimal Balance
{
get { return transactionImp.GetBalance(); }
}
}

The client which uses the interface IAccount never changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Program
{
static void Main(string[] args)
{
IAccount account = new SavingAccount("A0001", AccountType.Corporate);
account.Withdraw(800);
account.Deposit(100);
account.Withdraw(400);

Console.WriteLine("-----------------------------");
account = new CreditAccount("C2102", AccountType.Personal);
account.Withdraw(1000);
account.Deposit(1000);
account.Withdraw(5000);
account.Withdraw(200);

}
}

There is a lot of confusion between Bridge pattern and Strategy pattern. Both their implementations look similar, their UML representations look similar. You would have heard this before, but their intent is different. Now what the heck does that mean?
Strategy pattern provides a way to alter behaviour of the class(context) at runtime. The client decides which algorithm it needs to use based on some decision. The client uses the context which can take any instance of the strategy interface. The client is aware of both the context and the interface. The below client code blurs the distinction between Strategy and Bridge.

1
2
3
4
5
6
7
8
9
10
11
class Program
{
static void Main(string[] args)
{
IAccount account = new SavingAccount("A0001");
// client injects implementer instance to a property of type ITransaction.
account.Transaction = new Personal(1000);

account.Withdraw(800);
}
}

In Bridge pattern, the client is only aware of the abstraction and not the implementation. That was the reason you saw the instance being created internally. This pattern is used to keep your code maintainable structurally. If the client was injecting an instance of implementer, then it would not be a Bridge pattern since the client is changing the behaviour. Also Bridge provides two separate lines of abstractions which allow us to change either on a tangent.

Next Practical Patterns: Refactoring with Decorator Design Pattern