The Abstract Factory Pattern
The Abstract Factory
Contextual Forces
Motivation
Create an interface for creating sets of dependant or related instances that implement a set of abstract types. The Abstract Factory coordinates the instantiation of sets of objects that have varying implementations in such a way that only legitimate combinations of instances are possible, and hides these concrete instances behind a set of abstractions.
Encapsulation
Issues hidden from the consuming (Client) objects include:
- The number of sets of instances supported by the system
- Which set is currently in use
- The concrete types that are instantiated at any point
- The issue upon which the sets vary
Procedural Analog
A fairly common need for something like the Abstract Factory arises when supporting multiple operating systems in a single application. To accomplish this, one would need to select the right set of behaviors: a disk driver, a mouse driver, a graphics driver, and so forth, for the operating system that the application is currently being installed for.
In a non object-oriented system, one could accomplish this via conditional compilation, switching in the proper library for the operating system in question:
#IFDEF Linux include Linux_Drv.lib #ENDIF #IFDEF Windows include Windows_Drv.lib #ENDIF
If the libraries in question contained routines that were named the same, and worked the same as other libraries for other operating systems, such that the consuming application code could reference them without regard to the particular operating system in use in a given case, then it would simplify the code overall, and allow for a smoother transition between operating systems, and in supporting new operating systems in the future.
The Abstract Factory performs a similar role, but using objects, abstractions, and instantiation.
Non-Software Analog
I have two toolkits in my main toolbox.
One contains a set of wrenches (box-end wrenches, socket wrenches, closed-end wrenches) that are designed to work on a car that comes from overseas, and thus has metric measurements (centimeters, millimenters, etc...)
The other contains an identical set (box-end, socket, closed-end) that are designed instead for an engine with "English" measurements (inches, quarter-inches, and so forth)
I do not have engines that I work on which have some bolts that are metric and others which are English, and therefore I only use one set of wrenches or the other. The two toolkits encapsulate the difference: I choose the toolkit that applies to a given engine, and I know that all the wrenches I pull from it will be appropriate, making my maintenance process much simpler and allowing me to concentrate on the tune-up or other process I am trying to accomplish.
Implementation Forces
Example
Let's assume we are doing business in two countries, the United States and Canada. Because of this our application must be able to calculate taxes for both countries, as we as calculate freight charges based on the services we use in each case. Also, we must determine that an address is properly formatted given the country we're dealing with any any point in time. To support this, we have the abstractions in place CalcTax, CalcFreight, and AddressVer, which have implementing classes for each country in each case.
The Client would be designed to take an implementation of AbstractFactory in its constructor, then use it to make all the helper objects that it needs. Note that since the client is designed to work only with a single factory, and since there is no factory version that produces, say, a USTax object and a CanadaFreight object, it is impossible for the client to obtain this illegitimate combination of helper objects.
public class Client{ private CalcTax myCalcTax; private CalcFreight myCalcFreight; private AddressVer myAddressVer; public Client(AbsractFactory af){ myCalcTax = af.makeCalcTax(); myCalcFreight = af.makeCalcFreight(); myAddressVer = af.makeAddressVer(); } // The rest of the code uses the helper objects generically } public abstract class AbstractFactory{ abstract CalcTax makeCalcTax(); abstract CalcFreight makeCalcFreight(); abstract AddressVer makeAddressVer(); } public class USFactory : AbtractFactory{ public CalcTax makeCalcTax(){ return new USCalcTax(); } public CalcFreight makeCalcFreight(){ return new USCalcFreight(); } public AddressVer makeAddressVer(){ return new USAddressVer(); } public class CanadaFactory : AbtractFactory{ public CalcTax makeCalcTax(){ return new CanadaCalcTax(); } public CalcFreight makeCalcFreight(){ return new CanadaCalcFreight(); } public AddressVer makeAddressVer(){ return new CanadaAddressVer(); }
Questions, concerns, credibility checks
In order for the Abstract Factory to be effective, there must be a set of abstractions with multiple implementations, and these implementations must transition together under some circumstance (usually a large variation in the system), and these must all be resolveable to a consistent set of interfaces. In the example above, for instance, all tax systems in the supported countries must be supportable by a common interface, and the same must be true for the other services.
Also, an obvious question arises: what entity determines the right concrete factory to instantiate, and deliver to the Client entity? As this is an instantiation issue, the preferred approach would be to encapsulate this behavior in an entity that is seperate from any consuming object (encapsulation of construction). This can be done in an additional factory (arguably a "Factory factory"), or, as is often the case, in a static method in the base (Abstract) class of the factory.
public abstract class AbstractFactory{ abstract CalcTax makeCalcTax(); abstract CalcFreight makeCalcFreight(); abstract AddressVer makeAddressVer(); public static AbstractFactory getAFtoUse(String customerCode){ if(customerCode.startsWith("U")){ return new USFactory(); } else { return new CanadaFactory(); } } }
Note this author is not overly fond of this approach (it creates a mixture of perspectives in this abstract type; it is both a conceptual entity and an implementation of a factory), but it is nontheless quite common.
Also, one must ask how often and under what circumstances does the proper set of objects change? The example above sets the factory implementation in the Client's constructor, which would imply little change during its lifetime. If more flexibility is required, this can be accomplished another way.
In determining many how object types are in each set, how many sets there are, and the resulting number of combinations, once can get an early view of the complexity of the implementation. Also, there are sometimes objects that can be used in more than one family, such as a Euro object that might be used as a Currency implementation for many different countries in the European Union, though their Tax object might all be distinct. The degree to which implementations can be shared, and intermixed, will tend to reduce the burden on the implementing team, and is an early question to be asked.
Options in implementation
That fact that the Gang of Four example, and the example shown above, uses an abstract class with derivations should not mislead the reader to assume that this is required for an Abstract Factory. The fact that abstract class may be used is not the reason the pattern is named "Abstract" Factory, and in fact is simply one way of implementing the pattern.
For example, one could create a single concrete factory implementation, and for each method (makeCalcTaxI(), for instance) simply use a procedural switch or if/then logic to generate the correct instance. While this would not be particularly object-oriented (and probably not ideal), it would still be rightly termed an Abstract Factory.
The term Abstract Factory indicates that all the entities being created are themselves abstractions. CalcTax, CalcFreight, and AddressVer in the above example; these are all abstractions.
Another very popular way of implementing an Abstract Factory is to bind the factory to a database table, where fields contain the names of the proper classes to instantiate for a given client, and dynamic class loading is then used to instantiate the proper class. The advantage here is that changes to the rules of instantiation can be made simply by changing the values in the table:
//Example is in Java //Assumes myDataSource and ID are set elsewhere. ID reflects the current customer. CalcTax makeCalcTax () { String db = "jdbc:odbc:"+ myDataSource; Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); String query = “SELECT CALC_TAX FROM mytable WHERE ID = " + ID; Connection myConn = DriverManager.getConnection(db, "", ""); Statement myStatement = myConn.createStatement(); ResultSet myResults = myStatement.executeQuery(query); String classToInstantiate; classToInstantiate= myResults.getString("CALC_TAX"); return Class.forName(classToInsantiate); }
Consequent Forces
Testing issues
As with factories in general, the Abstract Factory's responsibility is limited to the creation of instances, and thus the testable issue is whether or not the right set of instances is created under a given circumstance. Often, this is covered by the test of the entities that use the factory, but if it is not, the test can use type-checking to determine that the proper concrete types are created under the right set of circumstances.
Cost-Benefit (gain-loss)
When we use the Abstract Factory we gain protection from illegitimate combinations of service objects. This means we can design the rest of the system for maximum flexibility, since we know that the Abstract Factory will eliminate any concerns of the flexibility yeilding bugs. Also, the consuming entity (Client) or entities will be incrementally simpler, since they can deal with the components at the abstract level. In our e-commerce example above, all notion of nationality will be eliminated from the Client, meaning that this same client will be usable in other nations in the future with little or no maintenance.
The Abstract Factory holds up well if the maintenance aspects are limited to new sets (new countries), or a given set changing an implementation (Canada changes its tax system). On the other hand, if an entirely new abstract concept enters the domain (trade restrictions to a new country), then the maintenance issues will be more profound as the Abstract Factory interface, all the existing factory implementations, and the Client entities will all have to be changed.
This is not a "fault" of the pattern, but rather points out the degree to which object-oriented systems are vulnerable to missing/new abstractions.