A pretty common situation when developing software is having a common algorithm with a set of differing specifics. We want to require the different implementations to have a common pattern in order to ensure that they’re following the same algorithm and also to make the code easier to understand. Once you understand the overall pattern, you can more easily understand each implementation.
The template method pattern is designed for these kinds of situations. Your overall algorithm design is represented by an abstract class. This has a series of abstract methods that represent customized steps in the algorithm, while any common code can be kept in this class. Each variant of the algorithm is implemented by a concrete class that over‐
rides the abstract methods and provides the relevant implementation.
Let’s think through a scenario in order to make this clearer. As a bank, we’re going to be giving out loans to members of the public, companies, and employees. These cate‐
gories have a fairly similar loan application process—you check the identity, credit his‐
tory, and income history. You get this information from different sources and apply different criteria. For example, you might check the identity of a person by looking at an existing bill to her house, but companies have an official registrar such as the SEC in the US or Companies House in the UK.
We can start to model this in code with an abstract LoanApplication class that controls the algorithmic structure and holds common code for reporting the findings of the loan application. There are then concrete subclasses for each of our different categories of
Lambda-Enabled Design Patterns | 119
applicant: CompanyLoanApplication, PersonalLoanApplication, and EmployeeLoa nApplication. Example 8-21 shows what our LoanApplication class would look like.
Example 8-21. The process of applying for a loan using the template method pattern
public abstract class LoanApplication {
public void checkLoanApplication() throws ApplicationDenied { checkIdentity();
checkCreditHistory();
checkIncomeHistory();
reportFindings();
}
protected abstract void checkIdentity() throws ApplicationDenied;
protected abstract void checkIncomeHistory() throws ApplicationDenied;
protected abstract void checkCreditHistory() throws ApplicationDenied;
private void reportFindings() {
CompanyLoanApplication implements checkIdentity by looking up information in a company registration database, such as Companies House. checkIncomeHistory would involve assessing existing profit and loss statements and balance sheets for the firm.
checkCreditHistory would look into existing bad and outstanding debts.
PersonalLoanApplication implements checkIdentity by analyzing the paper state‐
ments that the client has been required to provide in order to check that the client’s address exists. checkIncomeHistory involves assessing pay slips and checking whether the person is still employed. checkCreditHistory delegates to an external credit pay‐
ment provider.
EmployeeLoanApplication is just PersonalLoanApplication with no employment history checking. Conveniently, our bank already checks all its employees’ income his‐
tories when hiring them (Example 8-22).
Example 8-22. A special case of an an employee applying for a loan
public class EmployeeLoanApplication extends PersonalLoanApplication { @Override
protected void checkIncomeHistory() { // They work for us!
} }
With lambda expressions and method references, we can think about the template method pattern in a different light and also implement it differently. What the template
method pattern is really trying to do is compose a sequence of method calls in a certain order. If we represent the functions as functional interfaces and then use lambda ex‐
pressions or method references to implement those interfaces, we can gain a huge amount of flexibility over using inheritance to build up our algorithm. Let’s look at how we would implement our LoanApplication algorithm this way, in Example 8-23!
Example 8-23. The special case of an employee applying for a loan
public class LoanApplication { private final Criteria identity;
private final Criteria creditHistory;
private final Criteria incomeHistory;
public LoanApplication(Criteria identity, Criteria creditHistory, Criteria incomeHistory) { this.identity = identity;
this.creditHistory = creditHistory;
this.incomeHistory = incomeHistory;
}
public void checkLoanApplication() throws ApplicationDenied { identity.check();
creditHistory.check();
incomeHistory.check();
reportFindings();
}
private void reportFindings() {
As you can see, instead of having a series of abstract methods we’ve got fields called identity, creditHistory, and incomeHistory. Each of these fields implements our Criteria functional interface. The Criteria interface checks a criterion and throws a domain exception if there’s an error in passing the criterion. We could have chosen to return a domain class from the check method in order to denote failure or success, but continuing with an exception follows the broader pattern set out in the original imple‐
mentation (see Example 8-24).
Example 8-24. A Criteria functional interface that throws an exception if our applica‐
tion fails
public interface Criteria {
public void check() throws ApplicationDenied;
}
Lambda-Enabled Design Patterns | 121
The advantage of choosing this approach over the inheritance-based pattern is that instead of tying the implementation of this algorithm into the LoanApplication hier‐
archy, we can be much more flexible about where to delegate the functionality to. For example, we may decide that our Company class should be responsible for all criteria checking. The Company class would then have a series of signatures like Example 8-25.
Example 8-25. The criteria methods on a Company
public void checkIdentity() throws ApplicationDenied;
public void checkProfitAndLoss() throws ApplicationDenied;
public void checkHistoricalDebt() throws ApplicationDenied;
Now all our CompanyLoanApplication class needs to do is pass in method references to those existing methods, as shown in Example 8-26.
Example 8-26. Our CompanyLoanApplication specifies which methods provide each criterion
public class CompanyLoanApplication extends LoanApplication { public CompanyLoanApplication(Company company) {
super(company::checkIdentity, company::checkHistoricalDebt, company::checkProfitAndLoss);
} }
A motivating reason to delegate the behavior to our Company class is that looking up information about company identity differs between countries. In the UK, Companies House provides a canonical location for registering company information, but in the US this differs from state to state.
Using functional interfaces to implement the criteria doesn’t preclude us from placing implementation within the subclasses, either. We can explicitly use a lambda expression to place implementation within these classes, or use a method reference to the current class.
We also don’t need to enforce inheritance between EmployeeLoanApplication and PersonalLoanApplication to be able to reuse the functionality of EmployeeLoanAppli cation in PersonalLoanApplication. We can pass in references to the same methods.
Whether they do genuinely subclass each other should really be determined by whether loans to employees are a special case of loans to people or a different type of loan. So, using this approach could allow us to model the underlying problem domain more closely.