Make Your Own Abstract Factory Class Cluster in Objective-C

Crafting software is more than just mastering the syntax of any given language - the overall architecture, relationships and interaction among an application’s source code weigh in heavily to define true engineering elegance. A winning design is often harder to achieve than superior syntax, since the solutions are not as clear cut and mistakes are not immediately obvious. No compiler can warn us of poor architectural and design decisions, we instead realize such problems maintaining our code in the future. Thankfully, design principles and patterns exist to help us write elegant software.

One design template relied upon heavily by Cocoa’s Foundation framework is the class cluster, which is implemented in a manner consistent with the Abstract Factory design pattern. I’d like to explain how you’d actually go about rolling one of these yourself, and also touch on some of the motivating factors for introducing such a pattern into your own application. Later, I’ll relate to a real-world example of a class-cluster I designed for use in Camino to support the flexible parsing of search engine plugins.

First of all, what exactly is a class cluster?

In a nutshell, it’s a design that allows you to incorporate a family of functionally-related of objects into your application while keeping the code interacting with those objects loosely coupled, flexible, and easy to maintain or update.

A class cluster conforms this set of common objects to behave according to a single interface and, furthermore, channels all creation of them through that interface. The construction is made up of two key parts: a) one public, abstract interface serving as the “face” of the cluster which advertises the supported API, and b) many private, concrete subclasses of this interface responsible for actually implementing the advertised behavior of the superclass in their own specific way. The abstract superclass does implement a few methods itself, the most significant being a factory method to vend instances of the private subclasses; other common functionality shared across all subclasses, such as accessors, can also be defined here and shared as well.

Users of the cluster only ever see the one public superclass, are unaware that it is actually abstract, and know nothing about the existence of any private concrete subclasses. The superclass offers a factory creational method which is responsible for determining which subclass is appropriate for any given situation and transparently returning an instance of it. Since this returned object implements and behaves according to the public superclass’ interface, users can simply assume they have obtained a direct instance of this superclass.

What makes this a good idea?

Overview of a class cluster’s advantages:

  • Code is shared among a class hierarchy
  • Users of the collection are loosely coupled: They depend on only one new object
  • Change is isolated: the cluster is free to expand without affecting external code
  • Avoid duplicating code and logic: Determining which subclass is appropriate for any given situation is not repeated in multiple places.
  • Pain-free maintenance: New behavior can be added in only one place, the cluster itself.
  • External code is kept simple; Complexity is hidden inside the cluster
  • Your application can automatically use new behavior added to the cluster with no (or minimal) change to external code.

An Abstract Factory-based class cluster offers the benefits of shading code among a class hierarchy without subjecting users to depend upon the entire collection of classes. By encapsulating the awareness and creation of private subclasses inside the cluster, external code is loosely coupled to the hierarchy and clients end up programming according to (and depending upon) one interface, not various implementations.

"Depend upon Abstractions. Do not depend upon concretions"

A cluster hides the actual determination of which unique subclass to use for a certain situation into one area: the superclass’ init method. Leaving creational logic spread (and repeated) throughout the application is a maintenance problem because each time a new object is introduced all locations in code where one of these subclasses are created would require modification. The cluster can then use arguments to its initWith…: method as clues to determine which subclass can handle the desired behavior.

External code is thus dependent upon only one class. External users of your class cluster can be shielded from the fact that a different object (a subclass of the cluster) is returned rather than a direct instance of the cluster itself. The way to hide this fact is by ensuring the object responds to all methods declared in the cluster’s public interface. This strategy increases your code’s maintainability by allowing you to transparently introduce a new subclass to handle additional specific behavior flexibility by programming to an abstraction and not a concrete implementation.

"Identify the aspects of your application that vary and separate them from what stays the same."

Much of complexity and extra work behind designing a class cluster lies in the initial creation. Afterward, when that inevitable aspect of software engineering known as change comes around, you will immediately realize the importance and beauty of this pattern. Our application will be easier to maintain, nimble to change, and highly flexible.

A real example: Camino’s Search Plugin Parsing.

In Camino, I worked on support for the ability to parse and install plugins. Search engines can describe themselves (their name, query URL structure, etc) in a plugin file, which we can then parse and use that information in our built-in toolbar search field. I wanted to avoid locking us down to any one particular plugin format and at the same time make it quick and painless to introduce support for a new type, should another become widely used in the future.

Here’s a diagram of the Abstract Factory based class cluster I came up with:

Search Plugin Parsing Class Cluster

Following this design, introducing support for another plugin file format means all client code would just “automatically” start using it. Camino just supplies the MIME type of a plugin, and if the parsing cluster knows how to deal with that format an object is successfully created and returned. External code using the cluster would not require any modifications whatsoever to support a newly introduced format parser. External code does not need to be aware of each and every plugin parser.

External users of the parsing cluster have no knowledge of which SearchPluginParser subclasses should be used for each particular plugin format (or even that any such subclasses exist in the first place). It should appear, on the surface, that the SearchPluginParser itself actually implements all behavior.

Here’s some simplified sample code to demonstrate how to go about this:

SearchPluginParser.h

@interface SearchPluginParser
// Our class cluster’s public API:

- (id)initWithPluginMIMEType:(NSString *)mimeType;

- (BOOL)parseSearchPluginAtURL:(NSURL *)searchPluginURL;

- (NSURL *)searchPluginURL;

- (NSString *)searchPluginName;

@end

#pragma mark -

@interface SearchPluginParser (AbstractMethods)
// Abstract methods which should be implemented by subclasses:

-(BOOL)parsePluginData:(NSData *)searchPluginData;

@end

#pragma mark -

@interface SearchPluginParser (SubclassUseOnly)

// Private, concrete methods which should only be used by subclasses:

- (void)setSearchEngineName:(NSString *)newSearchEngineName;

- (void)setSearchEngineURL:(NSString *)newSearchEngineURL;

@end

SearchPluginParser.m

@implementation SearchPluginParser

// Returns nil if type is not supported.

- (id)initWithPluginMIMEType:(NSString *)mimeType
{
  // Since we’re an abstract object, transparently

  // return one of our concrete subclasses.
  [self release];

  // This is where you’d determine which concrete
  // subclass can best handle the desired behavior.

  if ([mimeType isEqualToString:kOpenSearchType])
    self = [[OpenSearchParser alloc] init];
  else

    self = nil;

  return self;
}

// Define any other default implementations
// for other methods, such as the accessors…


@end

OpenSearchParser.{h,m}

// Private subclass, known only to SearchPluginParser,
// which it uses to parse a certain file format:

@interface OpenSearchParser : SearchPluginParser

@end

@implementation OpenSearchParser


-(BOOL)parsePluginAtURL:(NSURL *)pluginURL
{
  // Implement this abstract method with the specific
  // knowledge of how to parse OpenSearch plugins.

}

@end

External Code in the Browser

- (void)detectedSearchPlugin:(NSDictionary *)searchPluginInfoDict
{
  SearchPluginParser *pluginParser =      [[SearchPluginParser alloc] initWithMIMEType:      [searchPluginInfoDict objectForKey:kSearchPluginMIMETypeKey]];

  // Check return value and then call methods declared publicly

  // in SearchPluginParser.h.

  // We’re essentially treating the returned object
  // as a SearchPluginParser, even though it’s actually
  // a subclass of it.  We know nothing about OpenSearchParser.
}

The cluster’s init method should have the knowledge to determine which private, concrete subclass to actually implement the required behavior. Now, if we decide to add another plugin format, the client code shouldn’t need to change at all - the cluster would introduce the new private subclass and handle the new behavior.

A note about over-engineering: Whenever such an advanced and complex design is introduced into an application one could convincingly argue that the initial work was unnecessary and over the top. To that I would caution that you should not use a design pattern simply because it exists and seems cool. Weigh your options to carefully determine if spending some initial development time up-front will definitely offer benefit to the long term future of your source code.

Permalink · Written on: 04-20-08