< Day Day Up > |
An oft-quoted rule of computer science is "Most problems in computer science can be solved by another level of indirection ." One might complain that too many levels of indirection can create maintenance problems. But like many other design facets, taking out indirection is often easier than adding it later. The Factory pattern (see Design Patterns by Erich Gamma et al.) is a common form of indirection. Instead of creating an object directly with a constructor, you obtain the object from a Factory class method. This method hides the actual implementation of the object's creation from the caller (see Example 11-7). Example 11-7. ZipCodeVerificationServiceFactoryenum ZipCodeVerificationServiceType {NORMAL, TRACKING}; class ZipCodeVerificationServiceFactory { static ZipCodeVerificationService get_instance (ZipCodeVerificationServiceType type_to_instantiate); { switch(type_to_instantiate) { case NORMAL: return new ZipCodeVerificationImplementation( ); break; case TRACKING: return new ZipCodeVerificationTracker( ); break; } } We alter the initialization of zip_code_verification in Example 11-4 to call the factory get_instance( ) method with the type requested ( NORMAL or trACKING ): static ZipCodeVerificationService zip_code_verification = ZipCodeVerificationServiceFactory.get_instance(TRACKING); Note that the caller can choose his desired implementation type, but does not need to specify the name of the actual implementation. The caller specifies the what , but not the how . If the decision to use logging should be made at execution time rather than compile time, the factory can read the type value from a configuration file. The factory code is changed, as shown in Example 11-8. Example 11-8. ZipCodeVerificationServiceFactory with configurationenum ZipCodeVerificationServiceType {NORMAL, TRACKING}; class ZipCodeVerificationServiceConfigurationDTO { ZipCodeVerificationServiceType service_type; } class ZipCodeVerificationServiceFactory { static ZipCodeVerificationService get_instance( ); static ZipCodeVerificationServiceConfigurationDTO read_configuration( ); } ZipCodeVerificationService get_instance( ) { ZipCodeVerificationServiceConfigurationDTO configuration = read_configuration( ); switch(configuration.service_type) { case NORMAL: return new ZipCodeVerificationImplementation( ); break; case TRACKING: return new ZipCodeVerificationTracker( ); break; } } In addition to using configuration information, the factory could check the environment in which the program is running and instantiate a logging class appropriate to that environment. In this example, the environment might permit an Internet connection. If so, the factory could instantiate a logging implementation that used a remote server. Note that the configuration information in Example 11-8 has been removed by one level of indirection from the format of the configuration file. The read_configuration( ) method returns an object containing all the configuration information that ZipCodeVerificationServiceFactory requires.
To avoid ZipCodeVerificationServiceFactory depending on a configuration file, we can alter the design to use Dependency Injection. Martin Fowler describes Dependency Injection (also called Inversion of Control) in detail at http://martinfowler.com/articles/injection.html. The user of a class injects data on which an object depends when the object is created. The dependencies are defined through constructor arguments, arguments to a factory method, or settable properties. The code using Dependency Injection appears in Example 11-9. Example 11-9. ZipCodeVerificationServiceFactory Dependency Injectionclass ZipCodeVerificationServiceFactory { static ZipCodeVerificationService get_instance( ZipCodeVerificationServiceConfigurationDTO configuration); } ZipCodeVerificationService get_instance( ZipCodeVerificationServiceConfigurationDTO configuration) { switch(configuration.service_type) { case NORMAL: return new ZipCodeVerificationImplementation( ); break; case TRACKING: return new ZipCodeVerificationTracker( ); break; } }
|
< Day Day Up > |