This is about Cocoa, and in particular about class clusters. The problem I wanted to solve was having a class cluster with easily extendable hierarchy without too much interdependency. In my case, I want to create a number of different UITableViewCell descendants, depending on the particular data element the cell should handle. If the data element has a field “string value”, then a UITableViewCell with a text field for such a string value should be created. If the data element has a field “check” representing a yes/no answer, then a UITableViewCell with a yes/no functionality widget should be created instead, and so on. In total, I have less than ten different kinds of UITableViewCell derived classes, but they could become more at any time.
I prefer building stuff like this with an object factory in the base class. In this case, my base class is called ItemTableCell and has a factory method with a prototype like this:
+ (ItemTableCell *)cellForItem:(IDRItem *)item;
This function needs to determine exactly which ItemTableCell child class can hande the item passed as parameter, and then create an instance of that class. It’s easy to solve in a naive way, like this:
#import "ItemTableCellStringInput.h" #import "ItemTableCellCheckInput.h" ... another ten or so imports + (ItemTableCell *)cellForItem:(IDRItem *)item { if ([item hasStringInput]) return [[[ItemTableCellStringInput alloc] initWithItem:item] autorelease]; else if ([item hasCheckInput]) return [[[ItemTableCellCheckInput alloc] initWithItem:item] autorelease]; else if .... (another ten or so if statements) }
There are several things I don’t like about this solution. The first one is the imports, namely that the base class needs to import and know about every child class, while the child classes need to import the base class. This kind of mutual dependency works (at least it does in Objective C), but offends my sense of esthetics. What also disturbs me greatly is that stupid if-statement that is necessary to decide which class gets to create an object. That statement contains knowledge about the capability of handling data objects that properly belongs only in the particular child class that can handle the data object. The way I wrote it here, the knowledge about the data is kept in two places: the base class and the derived class. Not good.
To get to grips with this, I came up with the following freakishly simple solution which I think only works in Objective C, and not in C++, C# or similar. (It may very well work in any number of other languages I don’t know that much about, though.)
In this pattern, the child class knows about the base class (duh), but the base class does not import the child class. Instead, the base class learns about the existence of the child class at runtime, just as the app starts up. It also delegates to the child classes the decision of which class can handle a particular data item. The result is that if you add a new kind of data item and a matching kind of ItemTableCell child class, you don’t need to touch the ItemTableCell base class code at all. It still works.
The key to this little miracle is the NSObject class function +load. This function gets called on every class in the system at startup. In case of derived classes, it gets called on the base class first, then on child classes. In my solution, I use this function to register derived classes with the base class. The base class then keeps these child classes in an array, so it can call them one by one to ask them if they can handle a particular data item, and if so it creates an instance and hands that instance the data item. It actually turns out to be pretty simple to do. Minimalistic example code follows:
@implementation ItemTableCell static NSMutableArray *subclasses; + (void)load { [super load]; subclasses = [[NSMutableArray alloc] initWithCapacity:5]; } + (void)addSubclass:(Class)cls { [subclasses addObject:cls]; } + (ItemTableCell *)cellForItem:(IDRItem *)item { ItemTableCell *cell = nil; for (Class cls in subclasses) { if ([cls canHandle:item]) { if (cell != nil) [NSException raise:@"Ambiguous canHandle" format:@""]; cell = [cls subCellForItem:item]; } } if (cell == nil) [NSException raise:@"No cell could handle item" format:@""]; return cell; }
You will note that the cellForItem function does not return the first child it finds can handle an item, but runs through them all to make sure there isn’t a second child class that claims it can handle the same item. Also, it throws an exception if no subclass at all finds itself able to handle an item. Believe me, this little extra runtime effort is well worth finding obscure bugs.
A derived class looks like in the following (I’m just showing one, you can imagine the others):
#import "ItemTableCellString.h" @implementation ItemTableCellString + (void)load { [super addSubclass:self]; } + (BOOL)canHandle:(IDRItem *)item { return [item hasStringInput] && [item doesWhateverElse] && ![item doesNotDoSomething]; } + (ItemTableCellString *)subCellForItem:(IDRItem *)item { ItemTableCellString *cell = [[[self alloc] initWithItem:item] autorelease]; return cell; }