Item 24: Use Categories to Break Class Implementations into Manageable Segments
A class can easily become bloated with many methods all interspersed throughout a huge implementation file. Sometimes, that’s just the way it is, and no amount ofrefactoring to split up the class can make the situation better. In that case, Objective-C categories can be used to great effect to split up a class into logical subsections that aid not only development but also debugging.
Consider a class that models a person. The person might have a few methods available on it:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;
/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
The implementation for such a class would contain all methods in a long list in one big file. As more methods are added to the class, the file will only get longer and more unmanageable. So it is often useful to split up such a class into separate, distinct portions. For example, the preceding could be rewritten to make use of categories:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
@end
@interface EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
@interface EOCPerson (Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end
@interface EOCPerson (Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
Now, each distinct part of the class is split into separate categories of methods. Not surprisingly, this language feature is called categories! In the example, the bases of the class, including the properties and initializer, are declared within the main implementation. Additional sets of methods, relating to different types of actions, are split into categories.
The entire class could still be defined within one interface file and one implementation file, but as the categories grow, the single implementation file could easily become unmanageable. In that case, categories could be extracted into their own files. For example, the categories in EOCPerson
could be extracted into separate files:
EOCPerson+Friendship(.h/.m)
EOCPerson+Work(.h/.m)
EOCPerson+Play(.h/.m)
For example, the friendship category would look like this:
// EOCPerson+Friendship.h
#import "EOCPerson.h"
@interface EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"
@implementation EOCPerson (Friendship)
- (void)addFriend:(EOCPerson*)person {
/* ... */
}
- (void)removeFriend:(EOCPerson*)person {
/* ... */
}
- (BOOL)isFriendsWith:(EOCPerson*)person {
/* ... */
}
@end
The class has been split up into much more manageable chunks of code that can be inspected individually. You must then remember to import the EOCPerson.h
header along with any category headers anywhere that the category methods are required. But this is a good way of making your code more manageable.
Even if a class is not too large, using categories to split it into subsections can be a useful way to segment code into functional areas. An example of such a class that does this within Cocoa is NSURLRequest
and its mutable counterpart,NSMutableURLRequest
. This class performs requests to obtain data from a URL and is used mostly with HTTP to obtain data from a server on the Internet somewhere, but the class is generic and can be used with other protocols as well. However, HTTP requests require additional information to be set on them beyond what standard URL requests require, such as the HTTP methods (GET, POST, etc.) or HTTP headers.
But NSURLRequest
is not easily subclassed, since it wraps a set of C functions that act on a CFURLRequest
data structure, including all the HTTP methods. So the HTTP-specific methods are simply added to NSURLRequest
as a category calledNSHTTPURLRequest,
and the mutation methods are added toNSMutableURLRequest
as a category called NSMutableHTTPURLRequest
. This way, all the underlying CFURLRequest
functions are wrapped within the same Objective-C class, but the HTTP-specific methods are split into a separate area to avoid confusion such that users might wonder why they are able to set the HTTP method on a request object that uses the protocol FTP.
Another useful reason to split up a class into categories is for debugging purposes; the category name is added to the symbol for all methods within that category. For example, the addFriend:
method has the following symbol name:
-[EOCPerson(Friendship) addFriend:]
When it appears in backtraces in a debugger, it will look something like this:
frame #2: 0x00001c50 Test'-[EOCPerson(Friendship) addFriend:]
+ 32 at main.m:46
The category name within the backtrace can be extremely useful for pinpointing exactly which functional area of the class the method relates to, which is particularly useful when certain methods should be regarded as private. In that case, it might be useful to create a category called Private
that contains all these methods. Such categories’ methods are generally used only internally to a class or framework and not exposed. If a user finds such a method—perhaps by reading a backtrace—the private
in the name should help to indicate that the method should not be used directly. In a way, it is a method of self-documenting code.
The idea of a Private
category is useful when creating a library that will be shared by other developers. Often, some methods should not form part of the public API but are quite nice to use within the library itself. In this scenario, creating aPrivate
category is a good option, since its header can be imported wherever the methods need to be used within the library. If that category header is not released as part of the library’s release, consumers of the library would have no idea that those private methods exist.
Things to Remember
Use categories to split a class implementation into more manageable fragments.
Create a category called
Private
to hide implementation detail of methods that should be considered as private.