Next article: Friday Q&A Skips a Week
Previous article: Friday Q&A 2010-03-05: Compound Futures
Welcome to another chewy edition of Friday Q&A. This week, Gwendal Roué has suggested talking about the techniques of subclassing class clusters.
Abstract Classes
To subclass a class cluster, you need to know what it is, and to understand class clusters you must first understand the concept of abstract classes. It's an easy concept, though.
An abstract class is a class which is not fully functional on its own. It must be subclassed, and the subclass must fill out the missing functionality.
An abstract class is not necessarily an empty shell. It can still contain a lot of functionality all on its own, but it's not complete without a subclass to fill in the holes.
Class Clusters
A class cluster is a hierarchy of classes capped off by a public abstract class. The public class provides an interface and a lot of auxiliary functionality, and then core functionality is implemented by private subclasses. The public class then provides creation methods which return instances of the private subclasses, so that the public class can be used without knowledge of those subclasses.
Take NSArray
as an example. It's an abstract class which requires its subclasses to provide implementations of the count
and objectAtIndex:
methods. It then provides a bunch of methods built on top of those two, such as indexOfObject:
, objectEnumerator
, makeObjectsPerformSelector:
, and many more.
The core functionality is then implemented in private subclasses such as NSCFArray
. The NSArray
creation methods such as +arrayWithObjects:
or -initWithContentsOfFile:
then produce instances of those private subclasses.
From the outside, the cluster nature of NSArray
is not readily apparent most of the time. It usually makes itself known if you start introspecting the classes of objects, and confuses programmers when they create an NSArray
and then start getting messages about an NSCFArray
. Other than that, NSArray
mostly looks and acts like any other class.
There is one place where the cluster nature is hugely important, and that's if you subclass the public class yourself.
Subclassing
Subclassing a class cluster (which means subclassing an abstract class) is completely different from subclassing a normal class.
When subclassing a normal class, your superclass provides full functionality for whatever it does. A subclass with an empty implementation is completely valid in this case, and will behave just like the superclass. You can then add methods to your implementation to add new functionality or override existing functionality.
When subclassing a class cluster, your superclass does not provide full functionality. It provides a lot of ancillary functionality, but you must provide the core yourself. This means that an empty subclass is not valid. There is a minimum set of methods that you must implement.
In class cluster teminology, those methods that you must implement are called primitive methods. How do you find them? There are two easy ways.
The first way is to crack open the documentation for the cluster class and search it for the word "primitive". The docs will tell you which methods you have to override.
The second way is to open the header for the cluster class. Primitive methods are always found in the class's main @interface
block. Additional methods provided by the cluster are always found in categories.
Watch out when looking at cluster classes which are themselves subclasses of another cluster class. The result inherits all primitive methods, and you must implement both sets. For example, NSMutableArray
has five primitive methods of its own plus the two from NSArray
. If you subclass NSMutableArray
, you must provide implementations for all seven.
Techniques
Now you know what to implement, but how? There are three main ways.
First, you can simply provide your own implementation of the primitive methods, implementing them all from scratch. For example, imagine you're writing a specialized array optimized for holding two elements:
@interface MyPairArray : NSArray
{
id _objs[2];
}
- (id)initWithFirst: (id)first second: (id)second;
@end
@implementation MyPairArray
- (id)initWithFirst: (id)first second: (id)second
{
if((self = [self init]))
{
_objs[0] = [first retain];
_objs[1] = [second retain];
}
return self;
}
- (void)dealloc
{
[_objs[0] release];
[_objs[1] release];
[super dealloc];
}
- (NSUInteger)count
{
return 2;
}
- (id)objectAtIndex: (NSUInteger)index
{
if(index >= 2)
[NSException raise: NSRangeException format: @"Index (%ld) out of bounds", (long)index];
return _objs[index];
}
@end
Second, you can keep a working instance around, obtained from the public API, and pass your calls through to it:
@interface MySpecialArray : NSArray
{
NSArray *_realArray;
}
- (id)initWithArray: (NSArray *)array;
@end
@implementation MySpecialArray
- (id)initWithArray: (NSArray *)array
{
if((self = [self init]))
{
_realArray = [array copy];
}
return self;
}
- (void)dealloc
{
[_realArray release];
[super dealloc];
}
- (NSUInteger)count
{
return [_realArray count];
}
- (id)objectAtIndex: (NSUInteger)index
{
id obj = [_realArray objectAtIndex: index];
// do some processing with obj
return obj;
}
// maybe implement more methods here
@end
The third technique is to simply add a category to the cluster class instead of subclassing it. People often subclass simply to add new methods, and not to modify existing functionality. In Objective-C, you can add new methods in a category:
@interface NSArray (FirstObjectAdditions)
- (id)my_firstObject;
@end
@implementation NSArray (FirstObjectAdditions)
- (id)my_firstObject
{
return [self count] ? [self objectAtIndex: 0] : nil;
}
@end
firstObject
method.)
Conclusion
Class clusters are different from normal classes, but are easy to subclass once you understand the differences and what they mean. You're required to implement the class cluster's primitive methods, which you can do by providing a from-scratch implementation, or by passing through to another instance. Finally, if your only purpose in subclassing is to add new methods, create a category instead.
That's it for this week. Come back in seven days for another crunchy post. Until then, keep your ideas coming. Friday Q&A is driven by reader ideas, so if you have a topic that you would like to see covered here, send it in!
Comments:
init
, so I don't need super
. In this particular case, both are equivalent, because I don't override init
, but if I did, I would want it to be called.
See http://www.mikeash.com/pyblog/the-how-and-why-of-cocoa-initializers.html
- (void)somePrimitiveMethod
{
NSLog(@"-[AbstractClass somePrimitiveMethod] called, this should never happen. Did you forget to implement -[%@ somePrimitiveMethod]?", [self class]);
[self doesNotUnderstand: _cmd];
}
This works well enough.
isKindOfClass:
checks.
If you really wanted, you could override
+alloc
to refuse to allocate instances of an abstract class, but there’s little advantage to having a runtime failure in +alloc
rather than slightly later when you try to actually use the object (or in the initializer).Incidentally, there’s another problem with compiler checking of virtual classes: it’s normal and correct to use the standard allocation pattern with a class cluster, as in
[[NSArray alloc] initWithSomething]
. In this case, a subclass (or proxy) could validly be returned by +alloc
, -initWithSomething
, or both, and this could change between system versions.NSMutableString
does? You could allow that with extra keywords and such, but it adds complication.
Worse, I think, is the fact that method implementations cannot all be seen by the compiler at compile time. What if your class is concrete by virtue of a category that the compiler doesn't know about? What if it's concrete because you add the necessary methods at runtime? Again, you can fix this with more extra keywords, but that's more complication. Although ObjC 2.0 is getting away from it to some extent, ObjC's underlying philosophy is to be a minimal set of extensions to C, so I think a complicated system of abstract class keywords wouldn't be worth it.
For the implementation, I'd probably do something like:
+ (id)allocWithZone: (NSZone *)zone
{
if(self == [MyAbstractClass class])
return [MyAbstractClassInitFactory allocWithZone: zone];
else
return [super allocWithZone: zone];
}
@implementation MyAbstractClassInitFactory
- (id)init
{
[self release];
return [[MyConcreteClassThatHandlesInit alloc] init];
}
- (id)initWithSomeObject: (id)
{
[self release];
return [[MyConcreteClassThatHandlesSomeObject alloc] init];
}
// etc.
Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.
if ((self = [self init])) { ... }
Shouldn't it be
[super init]
?