Talk:ExtendingInterfaces
From APIDesign
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.
/** * 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 = theBalance; 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 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 CheckingAccountFactory. */ public abstract class CheckingAccountFactoryAbstract implements CheckingAccountFactory { /** * {@inheritDoc} */ @Override public BankAccount createBankAccount() { return 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 standard implementation of the CheckingAccountFactory. */ public class CheckingAccountFactoryStandard extends CheckingAccountFactoryAbstract { /** * {@inheritDoc} */ @Override public CheckingAccount createCheckingAccount() { return new CheckingAccountStandard(this); } } // 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 SavingsAccountFactory. */ public abstract class SavingsAccountFactoryAbstract implements SavingsAccountFactory { /** * {@inheritDoc} */ @Override public BankAccount createBankAccount() { return 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; } } /** * The standard implementation of the SavingsAccountFactory. */ public class SavingsAccountFactoryStandard extends SavingsAccountFactoryAbstract { /** * {@inheritDoc} */ @Override public SavingsAccount createSavingsAccount() { return new SavingsAccountStandard(this); } } /** * 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; } }
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.
/** * 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) { } }
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.
public class Client { /** * Implementation to add interest payment to an account. */ // If AddInterest 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 interest rate for an account. This is a parameterized closure. */ // If SetInterestRate needs access to Client instance data, then remove // the "static" qualifier. private static class SetInterestRate extends DoBankAccountAdapter { /** * My interest rate parameter. */ private final int interestRate; /** * {@inheritDoc} */ @Override public void doBankAccount(SavingsAccount theSavingsAccount) { theSavingsAccount.setInterestRate(interestRate); } /** * Construct an instance. * * @param theInterestRate The interest rate parameter. */ private SetInterestRate(int theInterestRate) { super(); interestRate = theInterestRate; } } /** * 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 } /** * Set the interest rate on the specified account. * * @param theAccount The account reference. * @param theInterestRate The interest rate parameter. */ public void setInterestRate(BankAccount theBankAccount, int theInterestRate) { DoBankAccount closure; closure = new SetInterestRate(theInterestRate); closure.process(theBankAccount); // ignores non-savings account } }
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
Thanks Jeffrey, I guess your text deserves a separate page. Would ExtendingInterfaceHierarchy be appropriate name? Feel free to create it, or I will do it in a week or so.
Btw. I am not really comfortable with getting compile time error, as that is not very backward compatible and also does not prevent an error when such code is compiled against version with fewer account types and executed with newer one with more types. I'd rather used extensible visitor as described in Chapter 18.
--JaroslavTulach 13:32, 20 August 2009 (UTC)