Monday, June 18, 2012

Abstract Methods on Legacy Objects Using Categories

Let's say we have the following classes:


@interface HBAbstractLegacyNode : NSObject

@end

@interface HBChildNode : HBAbstractLegacyNode

@property (nonatomic, strong) NSString *childProperty;

@end

@interface HBParentNode : HBAbstractLegacyNode

@property (nonatomic, strong) NSString *parentProperty;

@end



If we want to add a template method onto HBAbstractLegacyNode, we ordinarily, just alter our classes thusly:


@interface HBAbstractLegacyNode : NSObject

- (void) templateMethod;
- (NSString *) abstractishMethod;

@end

@implementation HBAbstractLegacyNode

- (void) templateMethod {
  NSLog(@"templateMethod: %@", [self abstractishMethod]);
}

- (NSString *) abstractishMethod {
  [NSException raise:@"Override" format:@"%@ should override %@", NSStringFromClass(self), NSStringFromSelector(_cmd)];
  return nil;
}

@end

@implementation HBChildNode 

- (NSString *) abstractishMethod {
  return self.childProperty;
}

@end

@implementation HBParentNode 

- (NSString *) abstractishMethod {
  return self.parentProperty;
}

@end



But what if we're dealing with legacy code or a 3rd party library, and can't or don't want to modify our existing classes? Then, we can utilize categories to add methods, and use protocols to get some compile-time type safety.


@protocol HBOptionalAbstractish 

@optional
- (NSString *) abstractishMethod;

@end

@protocol HBAbstractish 

@required
- (NSString *) abstractishMethod;

@end

@interface HBAbstractLegacyNode(HBTemplate)

- (void) template;

@end

@interface HBParentNode(HBAbstractish)

@end

@interface HBChildNode(HBAbstractish)

@end

@implementation HBParentNode(HBAbstractish)

- (NSString *) abstractishMethod {
    return self.parentProperty;
}

@end

@implementation HBChildNode(HBAbstractish)

- (NSString *) abstractishMethod {
    return self.childProperty;
}

@end

@interface HBAbstractLegacyNode(HBOptionalAbstractish)

@end

@implementation HBAbstractLegacyNode(HBOptionalAbstractish)

@end

@implementation HBAbstractLegacyNode(HBConsumerCodeTemplate)

- (void) template {
    NSLog(@"template: %@", [self abstractishMethod]);
}

@end



We define two parallel protocols, one whose methods are optional, and one whose methods are required. Apple's Category Documentation says:
There’s no limit to the number of categories that you can add to a class, but each category name must be different, and each should declare and define a different set of methods.

Further restrictions exist (I can't find a link to Apple documentation beyond this stackoverflow answer, please help me find a link to real docs):

Although the Objective-C language currently allows you to use a category to override methods the class inherits, or even methods declared in the class interface, you are strongly discouraged from doing so. A category is not a substitute for a subclass. There are several significant shortcomings to using a category to override methods:
  • When a category overrides an inherited method, the method in the category can, as usual, invoke the inherited implementation via a message to super. However, if a category overrides a method that exists in the category's class, there is no way to invoke the original implementation.
  • A category cannot reliably override methods declared in another category of the same class.
    This issue is of particular significance because many of the Cocoa classes are implemented using categories. A framework-defined method you try to override may itself have been implemented in a category, and so which implementation takes precedence is not defined.
  • The very presence of some category methods may cause behavior changes across all frameworks. For example, if you override the windowWillClose: delegate method in a category on NSObject, all window delegates in your program then respond using the category method; the behavior of all your instances of NSWindow may change. Categories you add on a framework class may cause mysterious changes in behavior and lead to crashes.

I interpret the above to mean we should not override methods from categories. Thus, we define the optional protocol who we will implement with a category on our superclass, HBAbstractLegacyNode. Then, we'll implement the required protocol on each of our subclasses.

Unfortunately, we don't have complete compile-time type safety. We must manually ensure that the methods in HBAbstractish and HBOptionalAbstractish have matching signatures, and we must manually ensure that we create category implementations for each new subclass we create. Thus, we don't get complete fidelity with abstract methods you find in Java, but we get close.

Declaration and implementation order matter to ensure that the compiler doesn't think subclasses have inherited an implementation from their superclass. The protocol declarations must come first, but the subclass interface and implementations must come before the superclass'. If the superclass' interface comes before the subclass' implementation, the compiler will think the subclass already has an implementation from the superclass and will not warn us if we haven't provided an implementation. Since the superclass implements the optional protocol and we don't provide any methods in its implementation, the superclass actually provides nothing to its subclasses.

This is a lot of work to get a few helpful hints from the compiler, but when you have an enormous codebase spanning hundreds of files, all these checks (in concert with unit tests), go a long way towards stopping stupid errors.