Monday, July 11, 2011

Resharper series - Create Template Methods

Template method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure. In your code if two or more methods in subclasses perform similar steps in the same order with different logic or steps you can make use of the extract template method refactoring steps to generalize the methods by extracting their steps into methods with identical signatures and pull up these to form the Template Method.

For e.g., the below given code samples simulate the withdrawal functionality on different bank accounts.
public class SavingsAccount
{
    private readonly User _user;
    private decimal _balance;

    public SavingsAccount(User user)
    {
        _user = user;
    }

    public string GetUserName()
    {
        return _user.GetName();
    }

    public void SetBalance(decimal balance)
    {
        _balance = balance;
    }

    public void HasSufficientBalanceForTransfer(decimal amountToWithdraw)
    {
        if(amountToWithdraw > _balance)
            throw new InsufficientBalancesException();
    }

    public decimal GetServiceChargeForWithdrawal()
    {
        return 0M;
    }

    public void WithdrawAmount(decimal amountToWithdraw)
    {
        _balance -= amountToWithdraw + GetServiceChargeForWithdrawal();
    }

    public decimal GetBalance()
    {
        return _balance;
    }
}

public class CurrentAccount
{
    private readonly User _user;
    private decimal _balance;

    public CurrentAccount(User user)
    {
        _user = user;
    }

    public string GetUserName()
    {
        return _user.GetName();
    }

    public void SetBalance(decimal balance)
    {
        _balance = balance;
    }

    public void HasSufficientBalanceForTransfer(decimal amountToWithdraw)
    {
        if(amountToWithdraw > _balance)
            throw new InsufficientBalancesException();
    }

    public decimal GetServiceChargeForWithdrawal()
    {
        return 0M;
    }

    public void WithdrawAmount(decimal amountToWithdraw)
    {
        _balance -= amountToWithdraw + GetServiceChargeForWithdrawal();
    }

    public decimal GetBalance()
    {
        return _balance;
    }
}

The test cases on the current account for the withdrawal function

[TestMethod]
[ExpectedException(typeof(InsufficientBalancesException))]
public void ifUserTriesToWithdrawMoreMoneyThanAvailableBalanceShouldThrowException()
{
    var currentAccount = _bank.CreateSavingsAccount(_user);
    const decimal balance = 100M;
    currentAccount.SetBalance(balance);

    const decimal amountToWithdraw = 500M;
    currentAccount.HasSufficientBalanceForTransfer(amountToWithdraw);
}

[TestMethod]
public void currentAccountDoesNotDeductServiceChargeOnWithdrawal()
{
    var currentAccount = _bank.CreateSavingsAccount(_user);
    const decimal balance = 100M;
    currentAccount.SetBalance(balance);

    var serviceCharge = currentAccount.GetServiceChargeForWithdrawal();
    Assert.IsTrue(serviceCharge == 0M);
}

[TestMethod]
public void ifUserTriesToWithdrawAmountAvailableInAccountShouldProccedWithTransaction()
{
    var currentAccount = _bank.CreateSavingsAccount(_user);
    const decimal balance = 100M;
    currentAccount.SetBalance(balance);

    const decimal amountToWithdraw = 50M;

    currentAccount.WithdrawAmount(amountToWithdraw);
    Assert.AreEqual(currentAccount.GetBalance(), 50M);
}

As you can see from the test code, the withdraw functionality follows a series of steps to complete the actual withdrawal of money from the account like checking available balances, calculating the service charge etc. We can try to apply the template method by generalizing these actions and pulling up the template method to an abstract Account class.
First we apply the Extract superclass refactoring to create a superclass that holds the template method.



After applying the refactoring our Current and Savings account classes now looks like.
public class CurrentAccount : Account
{
    public CurrentAccount(User user)
    {
        _user = user;
    }

    public void HasSufficientBalanceForTransfer(decimal amountToWithdraw)
    {
        if(amountToWithdraw > _balance)
            throw new InsufficientBalancesException();
    }

    public decimal GetServiceChargeForWithdrawal()
    {
        return 0M;
    }

    public void WithdrawAmount(decimal amountToWithdraw)
    {
        _balance -= amountToWithdraw + GetServiceChargeForWithdrawal();
    }
}

public class SavingsAccount : Account
{
    public SavingsAccount(User user)
    {
        _user = user;
    }

    public void HasSufficientBalanceForTransfer(decimal amountToWithdraw)
    {
        if(amountToWithdraw > _balance)
            throw new InsufficientBalancesException();
    }

    public decimal GetServiceChargeForWithdrawal()
    {
        return 25M;
    }

    public void WithdrawAmount(decimal amountToWithdraw)
    {
        _balance -= amountToWithdraw + GetServiceChargeForWithdrawal();
    }
}

Next we apply the pull members up refactoring to pull the generalized methods to the super class and then implement the abstract methods in the respective child classes.



The final output looks like.
public abstract class Account
{
    protected User _user;
    protected decimal _balance;

    public string GetUserName()
    {
        return _user.GetName();
    }

    public void SetBalance(decimal balance)
    {
        _balance = balance;
    }

    public decimal GetBalance()
    {
        return _balance;
    }

    public abstract void HasSufficientBalanceForTransfer(decimal amountToWithdraw);
    public abstract decimal GetServiceChargeForWithdrawal();

    public void WithdrawAmount(decimal amountToWithdraw)
    {
        HasSufficientBalanceForTransfer(amountToWithdraw);
        var serviceCharge = GetServiceChargeForWithdrawal();
        _balance -= amountToWithdraw + serviceCharge;
    }
}

public class CurrentAccount : Account
{
    public CurrentAccount(User user)
    {
        _user = user;
    }

    public override void HasSufficientBalanceForTransfer(decimal amountToWithdraw)
    {
        if(amountToWithdraw > _balance)
            throw new InsufficientBalancesException();
    }

    public override decimal GetServiceChargeForWithdrawal()
    {
        return 0M;
    }
}

public class SavingsAccount : Account
{
    public SavingsAccount(User user)
    {
        _user = user;
    }

    public override void HasSufficientBalanceForTransfer(decimal amountToWithdraw)
    {
        if(amountToWithdraw > _balance)
            throw new InsufficientBalancesException();
    }

    public override decimal GetServiceChargeForWithdrawal()
    {
        return 25M;
    }
}

No comments: