Update: This post was written in May 2014, before the release of Swift. Swift adds enumerations that can have functions called on them, making the techniques in this post unnecessary when working in Swift. The titular “enumerations” refer to C style enumerations, which are nothing more than some type sugar around primitive integers. If you’re still working in Objective-C, and you want enumerations that can hold data and respond to messages, read on.
In the last post, we talked about wrapping simple Foundation types in their own class, so that the type checker can let you know that you’re passing the wrong kind of string to a method. We can extend this idea of value objects further.
I’m quickly falling in love with this pattern: replacing enumerations with polymorphic objects. In the last post, it was important to wrap a value, but here, there’s no value, just a kind. Normally, to represent a kind of thing, we create an enumeration, which is just an integer with a little bit of type sugar.
Where do enumerations fall short? Let’s look at some terrible code I wrote for an old app.
+ (NSString*) stringForType:(PodcastListType)listType {
if (listType == unplayedType) {
return @"Unplayed";
} else if (listType == favoriteType) {
return @"Favorites";
} else if (listType == archiveType) {
return @"Archive";
} else if (listType == trashType) {
return @"Trash";
} else if (listType == downloadsType) {
return @"Downloads";
} else if (listType == recentlyAddedType) {
return @"Recents";
}
return @"";
}
+ (PodcastListType) listTypeForString:(NSString*)name {
if ([name isEqualToString:@"Unplayed"]) {
return unplayedType;
} else if ([name isEqualToString:@"Favorites"]) {
return favoriteType;
} else if ([name isEqualToString:@"Archive"]) {
return archiveType;
} else if ([name isEqualToString:@"Downloaded"]) {
return downloadsType;
} else if ([name isEqualToString:@"Recently Posted"]) {
return recentlyAddedType;
}
return unplayedType;
}
This code is bad for several reasons.
- The enumeration can’t be messaged, so any code or behavior that should live with the enumeration now hangs out wherever the code needs to be used.
- Everything in iOS revolves around the view controller, so the “easy” place to put this code is in a class method on the view controller (which is what I did!). This makes your view controller longer and cruftier. The reader of the view controller class doesn’t care how list types are converted into display strings.
- It’s not immediately obvious if cases aren’t covered, especially as the number of cases increases. The astute reader will note that
+listTypeForString:
doesn’t include the “Trash” case, which was an actual mistake I made writing this code. (Creating C functions can be a solution to the view controller problem, but the cases are still easy to miss.) - I can call
[MyClass stringForType:2]
with no compiler warning. The compiler will warn if I send a different typedef, but not if I send a plain old integer.
It’s not called Enumerative-C! Let’s make some objects.
SKPodcastListType.h:
@interface SKPodcastListType : NSObject <NSCopying>
- (instancetype)initWithString:(NSString*)string; //probably from the server
@property (nonatomic, readonly) NSString *displayName;
@end
@interface SKUnplayedPodcastListType : SKPodcastListType @end
@interface SKFavoritesPodcastListType : SKPodcastListType @end
//...
And SKPodcastListType.m
:
@implementation SKPodcastListType
- (instancetype)initWithString:(NSString *)string {
if ([string isEqualToString:@"unplayed"]) {
return [SKUnplayedPodcastListType new];
}
if ([string isEqualToString:@"favorites"]) {
return [SKFavoritesPodcastListType new];
}
return nil;
}
- (NSString *)displayName {
return @"";
}
- (NSUInteger)hash {
return [NSStringFromClass(self.class) hash];
}
- (BOOL)isEqual:(id)object {
return [self isMemberOfClass:[object class]];
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
@end
@implementation SKUnplayedPodcastListType
- (NSString *)displayName {
return @"Unplayed";
}
@end
@implementation SKFavoritesPodcastListType
- (NSString *)displayName {
return @"Favorites";
}
@end
//...
Now, while this is more lines of code, the code is now out of the way. It now lives in the right place. My view controller no longer knows the possible types of podcast lists and is shorter and simpler for it. Any additional behavior can be put right on these classes, instead of adding more class methods, and if you make a new PodcastListType
, it’ll be immediately obvious if you haven’t implemented the right methods.
The compiler will also now check our work to make sure we’re passing around SKPodcastListType
objects, instead of accidentally passing around numbers for enumerations.
Note that it doesn’t matter which SKUnplayedPodcastListType
object you have. It doesn’t hold any data except for its type. This means our -isEqual:
method just checks to make sure the classes are the same. And -copyWithZone:
can just return itself, since it’s immutable, which feels very elegant. You can get even more crazy and make each subtype be its own singleton, so you could just compare firstListType == secondListType
. You could metaprogram the -displayName
message to take the class name and remove the “SK” and the “PodcastListType”. The possibilities are limitless.