Design your API for easy extension with double dispatch
New page
2009 August 19
You may have read my prior remarks concerning factory instances. That topic only touched upon the larger topic of double dispatch. The key to compatibly extending interfaces is using polymorphism and double dispatch.
The following examples are overly-simplified to focus on the main point of using double dispatch to extend API interfaces. The API is logically
separated from its implementation by using "Functions" interfaces and
classes that are inherited by the API interfaces and classes.
In this example, there are two API sub-types, CheckingAccount and SavingsAccount, with a parent type of BankAccount. The client maintains a reference to a BankAccount instance. When the client needs special case handling of a particular sub-type, the client passes the BankAccount reference to a double dispatch instance created by the client. The double dispatch instance distinguishes the correct sub-type and invokes the appropriate method for the actual sub-type without using "instanceof" or reflection.
<source lang='java'>
/**
* The top-level functional declaration. All bank accounts share
* these functional behaviors.
*/
public interface BankAccountFunctions
{
/**
* Get the current account balance.
*
* @return The current account balance.
*/
public abstract long getBalance();
/**
* Set the current account balance.
*
* @param theBalance The new account balance.
*
* @return The previous account balance.
*/
public abstract long setBalance(long theBalance);
}
/**
* Common implementation of all sub-types.
*/
public abstract class BankAccountFunctionsAbstract
implements BankAccountFunctions
{
/**
* The current account balance.
*/
private long balance;
/**
* {@inheritDoc}
*/
@Override
public long getBalance()
{
return balance;
}
/**
* {@inheritDoc}
*/
@Override
public long setBalance(long theBalance)
{
long result;
result = balance;
balance = result;
return result;
}
}
/**
* An enumeration of all types of bank accounts. Add new
* types at the end of the definition to maintain the
* ordinals of the prior types.
*/
public enum BankAccountEnum
{
CHECKING_ACCOUNT,
SAVINGS_ACCOUNT; // add new sub-types after the last
}
/**
* The top-level polymorphic API. The client can use this
* type anywhere in the application. The double dispatch
* helper class can translate this reference type into the
* actual subtype reference without using "instanceof".
*/
public interface BankAccount
extends BankAccountFunctions
{
/**
* Get the factory instance that created this instance.
*
* @return The factory instance that created this instance.
*/
public abstract BankAccountFactory getBankAccountFactory();
/**
* Get the BankAccountEnum for the actual implementation.
*
* @return The BankAccountEnum for the actual implementation.
*/
public abstract BankAccountEnum getBankAccountEnum();
/**
* This method is the double dispatch interface.
*
* @param theDoBankAccount The DoBankAccount reference.
*/
public abstract invoke(DoBankAccount theDoBankAccount);
}
/**
* The factory API for BankAccount.
*/
public interface BankAccountFactory
{
/**
* Create a new instance of BankAccount.
*
* @return A new instance of BankAccount.
*/
public abstract BankAccount createBankAccount();
}
// The first sub-type is CheckingAccount.
/**
* The functions specific to a checking account.
*/
public interface CheckingAccountFunctions
extends BankAccountFunctions
{
/**
* Determine whether this account has overdraft protection.
*
* @return {@code true} This account has overdraft protection.
*/
public abstract boolean hasOverdraft();
/**
* Set whether this account has overdraft protection.
*
* @param theOverdraft The overdraft protection value.
*
* @return {@code true} The previous overdraft value.
*/
public abstract boolean setOverdraft(boolean theOverdraft);
}
/**
* The implementation of the checking account functions.
*/
public abstract class CheckingAccountFunctionsAbstract
extends BankAccountFunctionsAbstract
implements CheckingAccountFunctions
{
/**
* The overdraft setting.
*/
private boolean overdraft;
/**
* {@inheritDoc}
*/
@Override
public boolean hasOverdraft()
{
return overdraft;
}
/**
* {@inheritDoc}
*/
@Override
public boolean setOverdraft(boolean theOverdraft)
{
boolean result;
result = overdraft;
overdraft = theOverdraft;
return result;
}
}
/**
* The API sub-type for checking account.
*/
public interface CheckingAccount
extends BankAccount,
CheckingAccountFunctions
{
/**
* Get the factory instance that created this instance.
*
* @return The factory instance that created this instance.
*/
public abstract CheckingAccountFactory getCheckingAccountFactory();
}
/**
* The factory API for CheckingAccount.
*/
public interface CheckingAccountFactory
extends BankAccountFactory
{
/**
* Create a new instance of CheckingAccount.
*
* @return A new instance of CheckingAccount.
*/
public abstract CheckingAccount createCheckingAccount();
}
/**
* The basic implementation of the API for the checking account.
*/
public abstract class CheckingAccountAbstract
extends CheckingAccountFunctionsAbstract
implements CheckingAccount
{
/**
* {@inheritDoc}
*/
@Override
public final BankAccountFactory getBankAccountFactory()
{
return getCheckingAccountFactory();
}
/**
* {@inheritDoc}
*/
@Override
public final BankAccountEnum getBankAccountEnum()
{
return BankAccountEnum.CHECKING_ACCOUNT;
}
/**
* {@inheritDoc}
*/
@Override
public final invoke(DoBankAccount theDoBankAccount)
{
theDoBankAccount.doBankAccount(this);
}
}
/**
* The standard implementation of the API for the checking account.
*/
public class CheckingAccountStandard
extends CheckingAccountAbstract
{
/**
* The CheckingAccountFactory that created this instance.
*/
private final CheckingAccountFactory checkingAccountFactory;
/**
* {@inheritDoc}
*/
@Override
public final CheckingAccountFactory getCheckingAccountFactory()
{
return checkingAccountFactory;
}
/**
* Construct an instance.
*
* @param theCheckingAccountFactory The CheckingAccountFactory instance.
*/
public CheckingAccountStandard(CheckingAccountFactory theCheckingAccountFactory)
{
super();
checkingAccountFactory = theCheckingAccountFactory;
}
}
// The second sub-type is SavingsAccount.
/**
* The functions specific to a savings account.
*/
public interface SavingsAccountFunctions
extends BankAccountFunctions
{
/**
* Get the annual interest rate applied to this account.
*
* @return the annual interest rate applied to this account.
*/
public abstract int getInterestRate();
/**
* Get the annual interest rate applied to this account.
*
* @param theInterestRate The interest rate in basis points.
*
* @return The previous interest rate value.
*/
public abstract int setInterestRate(int theInterestRate);
}
/**
* The implementation of the savings account functions.
*/
public abstract class SavingsAccountFunctionsAbstract
extends BankAccountFunctionsAbstract
implements SavingsAccountFunctions
{
/**
* The interest rate in basis points.
*/
private int interestRate;
/**
* {@inheritDoc}
*/
@Override
public int getInterestRate()
{
return interestRate;
}
/**
* {@inheritDoc}
*/
@Override
public int setInterestRate(int theInterestRate)
{
int result;
result = interestRate;
interestRate = theInterestRate;
return result;
}
}
/**
* The API sub-type for savings account.
*/
public interface SavingsAccount
extends BankAccount,
SavingsAccountFunctions
{
/**
* Get the factory instance that created this instance.
*
* @return The factory instance that created this instance.
*/
public abstract SavingsAccountFactory getSavingsAccountFactory();
}
/**
* The factory API for SavingsAccount.
*/
public interface SavingsAccountFactory
extends BankAccountFactory
{
/**
* Create a new instance of SavingsAccount.
*
* @return A new instance of SavingsAccount.
*/
public abstract SavingsAccount createSavingsAccount();
}
/**
* The basic implementation of the API for the savings account.
*/
public abstract class SavingsAccountAbstract
extends SavingsAccountFunctionsAbstract
implements SavingsAccount
{
/**
* {@inheritDoc}
*/
@Override
public final BankAccountFactory getBankAccountFactory()
{
return getSavingsAccountFactory();
}
/**
* {@inheritDoc}
*/
@Override
public final BankAccountEnum getBankAccountEnum()
{
return BankAccountEnum.SAVINGS_ACCOUNT;
}
/**
* {@inheritDoc}
*/
@Override
public final invoke(DoBankAccount theDoBankAccount)
{
theDoBankAccount.doBankAccount(this);
}
}
/**
* The standard implementation of the API for the savings account.
*/
public class SavingsAccountStandard
extends SavingsAccountAbstract
{
/**
* The SavingsAccountFactory that created this instance.
*/
private final SavingsAccountFactory savingsAccountFactory;
/**
* {@inheritDoc}
*/
@Override
public final SavingsAccountFactory getSavingsAccountFactory()
{
return savingsAccountFactory;
}
/**
* Construct an instance.
*
* @param theSavingsAccountFactory The SavingsAccountFactory instance.
*/
public SavingsAccountStandard(SavingsAccountFactory theSavingsAccountFactory)
{
super();
savingsAccountFactory = theSavingsAccountFactory;
}
}
/**
* A wrapper Factories API for obtaining a factory instance.
*/
public interface BankAccountFactories
{
/**
* Get the BankAccountFactory for the specified type.
*
* @param theBankAccountEnum The BankAccountEnum for the type.
*
* @return The BankAccountFactory for the specified type.
*/
public abstract BankAccountFactory getBankAccountFactory(BankAccountEnum theBankAccountEnum);
/**
* A convenience method to create an account of the specified type.
*
* @param theBankAccountEnum The BankAccountEnum for the type.
*
* @return A new instance of BankAccount of the specified type.
*/
public abstract BankAccount createBankAccount(BankAccountEnum theBankAccountEnum);
}
/**
* The base implementation for the Factories API. The concrete
* implementation is stored elsewhere in the application. The
* factory instances are singletons stored in an array that is
* indexed by the BankAccountEnum.
*/
public abstract class BankAccountFactoriesAbstract
implements BankAccountFactories
{
/**
* {@inheritDoc}
*/
@Override
public BankAccount createBankAccount(BankAccountEnum theBankAccountEnum)
{
BankAccount result;
BankAccountFactory factory;
factory = getBankAccountFactory(theBankAccountEnum);
result = factory.createBankAccount();
return result;
}
}
</source>
In the example, the top-level API reference type is BankAccount. There are two sub-types, CheckingAccount and SavingsAccount, that have actual implementations. The usual way to distinguish between the sub-types is to use the "instanceof" built-in function. However, the double dispatch technique is far more powerful and extensible. The top-level BankAccount is initially designed to support double dispatch with its "invoke" method. Each distinct sub-type provides its own implementation of the "invoke" method, which is the key for double dispatch.
The service provider maintains the DoBankAccount interface, DoBankAccountAbstract class, and the DoBankAccountAdapter class. Each time the service provider adds a new API sub-type, it will update these definitions and republish them. Existing client code that uses the DoBankAccount interface or the DoBankAccountAdapter class will be unaffected. If the client code uses the DoBankAccountAbstract class, then it will get a compile-time error when it fails to implement an appropriate "doBankAccount" method.
<source lang='java'>
/**
* The double dispatch API for BankAccount.
*/
public interface DoBankAccount
{
/**
* Process the specified BankAccount according to its true type.
*
* @param theBankAccount The BankAccount reference.
*/
public abstract void process(BankAccount theBankAccount);
/**
* Process the CheckingAccount.
*
* @param theCheckingAccount The CheckingAccount reference.
*/
public abstract void doBankAccount(CheckingAccount theCheckingAccount);
/**
* Process the SavingsAccount.
*
* @param theSavingsAccount The SavingsAccount reference.
*/
public abstract void doBankAccount(SavingsAccount theSavingsAccount);
}
/**
* The base implementation for the double dispatch API.
*/
public abstract class DoBankAccountAbstract
implements DoBankAccount
{
/**
* {@inheritDoc}
*/
@Override
public void process(BankAccount theBankAccount)
{
theBankAccount.invoke(this); // double dispatch to sub-type
}
}
/**
* The adapter implementation to provide empty methods for
* unneeded sub-types. This is used for isolating special
* case handling of a sub-type.
*/
public class DoBankAccountAdapter
extends DoBankAccountAbstract
{
/**
* {@inheritDoc}
*/
@Override
public void doBankAccount(CheckingAccount theCheckingAccount)
{
}
/**
* {@inheritDoc}
*/
@Override
public void doBankAccount(SavingsAccount theSavingsAccount)
{
}
}
</source>
---------------------------
To add a new sub-type named "Fubar", define new FubarFunctions interface, FubarFunctionsAbstract class, Fubar interface, FubarAbstract class, and FubarStandard class. The definitions are almost a copy-and-paste of an existing sub-type with modifications for the FubarFunctions interface and FubarFunctionsAbstract class for the specific behavior of the API sub-type.
The BankAccountEnum class is updated with a FUBAR constant after the last constant, along with the DoBankAccount interface, and updating the DoBankAccountAdapter class by adding a "doBankAccount" method signature for the Fubar sub-type.
---------------------------
<source lang='java'>
public class Client
{
/**
* Implementation to add interest payment to an account.
*/
// If this needs access to Client instance data, then remove
// the "static" qualifier.
private static class AddInterest
extends DoBankAccountAdapter
{
/**
* {@inheritDoc}
*/
@Override
public void doBankAccount(SavingsAccount theSavingsAccount)
{
int interestRate;
long interestPaid;
long balance;
balance = theSavingsAccount.getBalance();
interestRate = theSavingsAccount.getInterestRate();
interestPaid = interestRate * balance / 10000L;
balance += interestPaid;
theSavingsAccount.setBalance(balance);
}
}
/**
* My handler for AddInterest.
*/
// If AddInterest needs access to Client instance data, then
// remove the "static" qualifier.
private static final DoBankAccount addInterest = new AddInterest();
/**
* Implementation to set overdraft protection. This is a parameterized closure.
*/
// If SetOverdraft needs access to Client instance data, then
// remove the "static" qualifier.
private static class SetOverdraft
extends DoBankAccountAdapter
{
/**
* My overdraft parameter.
*/
private final boolean overdraft;
/**
* {@inheritDoc}
*/
@Override
public void doBankAccount(CheckingAccount theCheckingAccount)
{
theCheckingAccount.setOverdraft(overdraft);
}
/**
* Construct an instance.
*
* @param theOverdraft The overdraft parameter.
*/
private SetOverdraft(boolean theOverdraft)
{
super();
overdraft = theOverdraft;
}
}
/**
* My closure to enable overdraft protection.
*/
// If SetOverdraft needs access to Client instance data, then
// remove the "static" qualifier.
private static final DoBankAccount overdraftEnable = new SetOverdraft(true);
/**
* My closure to disable overdraft protection.
*/
// If SetOverdraft needs access to Client instance data, then
// remove the "static" qualifier.
private static final DoBankAccount overdraftDisable = new SetOverdraft(false);
/**
* Add up the account balances.
*
* @param theAccounts The array of accounts to sum.
*
* @return The sum of the account balances.
*/
public long sum(BankAccount[] theAccounts)
{
long result;
int kk;
result = 0L;
kk = theAccounts.length;
while(0 < kk--)
{
result += theAccounts[kk].getBalance();
}
return result;
}
/**
* Add interest payment to the account balance.
*
* @param theAccount The account reference.
*/
public void addInterest(BankAccount theBankAccount)
{
addInterest.process(theBankAccount); // ignores non-savings account
}
/**
* Set overdraft protection on the specified account.
*
* @param theAccount The account reference.
* @param theOverdraft The overdraft parameter.
*/
public void setOverdraft(BankAccount theBankAccount, boolean theOverdraft)
{
DoBankAccount closure;
closure = theOverdraft ? overdraftEnable : overdraftDisable;
closure.process(theBankAccount); // ignores non-checking account
}
}
</source>
Each time the client has special case processing for one or more of the sub-types, the client defines an inner class that extends DoBankAccountAbstract or DoBankAccountAdapter, and then implements the appropriate "doBankAccount" method(s). The "doBankAccount" method receives the BankAccount reference as the actual sub-type in the parameter. The inner class can then perform whatever special processing is needed for that particular sub-type.
The client can use the DoBankAccountAdapter class to effectively ignore sub-types that it doesn't care about. This allows for future extension of BankAccount by adding a new sub-type in a compile-time and run-time compatible way.
If the client requires knowing all sub-types, then extending only the DoBankAccountAbstract for an inner class will cause a compile-time error for a "missing" sub-type. The inner class will fail to compile, because it doesn't implement all of the abstract "doBankAccount" methods.
This API design is very extensible and backward compatible with existing client code. It doesn't use "instanceof" or reflection. It can catch client code (compile-time error) that depends on knowing all sub-types by using the DoBankAccountAbstract class as a base for the client's inner classes. It can ignore some sub-types and focus on one or more specific sub-types by extending the DoBankAccountAdapter class. Finally, the basic structure of the interfaces, classes, the inheritance hierarchy, and the separation of function from API, lends itself to easy extension of the API sub-types through automated code generators, either stand-alone or provided by an IDE.
Two cents worth. Your mileage may vary.
Jeffrey D. Smith