Implementing enum with associated values in Objective-C
Swift has this amazing thing called enum with associated value. Enums are traditionally used to represent a type system with finite set of values. But in real life each of those enum types sometimes have a need to associate some data. And the data of one enum type might be very different than the rest. Imagine writing a parser where each node
is of type Node
. We usually want to deal with Node
as the type for doing things like passing Node
around, or generate an array of Node
. We might also have methods on Node
, such as node.print()
but Node
can never be instantiated directly as n = Node()
.
Swift programming guide provides a good example of this pattern with Barcode
that I’ve enhanced here a bit for readability:
// Barcode.swift
enum Barcode {
case upc(
numberSystem: Int,
manufacturer: Int,
product: Int,
check: Int
)
case qrCode(productCode: String)
func print() {
switch self {
case let .upc(numberSystem, manufacturer, product, check):
Swift.print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
Swift.print("QR code: \(productCode).")
}
}
}
// Main.swift
func print(barcode: Barcode) {
barcode.print()
}
func printBarcodes() {
print(barcode: .upc(
numberSystem: 10,
manufacturer: 20,
product: 30,
check: 40
))
print(barcode: .qrCode(productCode: "asdfa"))
}
How can we bring this pattern to Objective-C? This is how I think of the problem. Every use case of an enum with associated value can be represented as a subclass pattern, where every subclass is only a level deep. And the base class is an abstract class that can not be instantiated. The Barcode example from above might look something like:
With that in mind we can design our interface as:
// PLBarcode.h
@protocol PLBarcode <NSObject>
- (void)print;
@end
@interface PLBarcodeUPC : NSObject <PLBarcode>
+ (instancetype)barcodeWithNumberSystem:(NSInteger)numberSystem
manufacturer:(NSInteger)manufacturer
product:(NSInteger)product
check:(NSInteger)check;
@end
@interface PLBarcodeQR : NSObject <PLBarcode>
+ (instancetype)barcodeWithProductCode:(NSString *)productCode;
@end
Notice that we are not actually subclassing any implementation class from any common base PLBarcode
class. This keeps every implementation class completely independent of each other with no shared hierarchy. We can use PLBarcode
as the type that can easily pass around:
// Main.m
- (void)printBarcode:(id<PLBarcode>)barcode
{
[barcode print];
}
- (void)printBarcodes
{
[self printBarcode:[PLBarcodeUPC barcodeWithNumberSystem:10
manufacturer:20
product:30
check:40]];
[self printBarcode:[PLBarcodeQR barcodeWithProductCode:@"asfsdf"]];
}
Another advantage of enums is that we don’t have to deal with so many explicit types as we have here with PLBarcodeUPC
and PLBarcodeQR
. I can imagine adding more implementation types would require bombarding the clients of PLBarcode
with many implementation types that they don’t actually care about. Thinking again, what we need is the following:
- A way to instantiate
PLBarcode
with unrelated data. - Ability to send messages to
PLBarcode
which should be handled by the implementation type.
With that in mind, we can solve this problem by moving all the implementation subclasses internally to PLBarcode.m
and expose only typeless class methods in PLBarcode.h
, like a factory pattern
// PLBarcode.h
@protocol PLBarcode <NSObject>
- (void)print;
@end
@interface PLBarcode : NSObject
+ (id<PLBarcode>)barcodeWithNumberSystem:(NSInteger)numberSystem
manufacturer:(NSInteger)manufacturer
product:(NSInteger)product
check:(NSInteger)check;
+ (id<PLBarcode>)barcodeWithProductCode:(NSString *)productCode;
@end
// PLBarcode.m
@interface __PLBarcodeUPC : NSObject <PLBarcode>
+ (instancetype)barcodeWithNumberSystem:(NSInteger)numberSystem
manufacturer:(NSInteger)manufacturer
product:(NSInteger)product
check:(NSInteger)check;
@end
@interface __PLBarcodeQR : NSObject <PLBarcode>
+ (instancetype)barcodeWithProductCode:(NSString *)productCode;
@end
@implementation PLBarcode
+ (id<PLBarcode>)barcodeWithNumberSystem:(NSInteger)numberSystem
manufacturer:(NSInteger)manufacturer
product:(NSInteger)product
check:(NSInteger)check;
{
return [__PLBarcodeUPC barcodeWithNumberSystem:numberSystem
manufacturer:manufacturer
product:product
check:check];
}
+ (id<PLBarcode>)barcodeWithProductCode:(NSString *)productCode;
{
return [__PLBarcodeQR barcodeWithProductCode:productCode];
}
@end
With that change the call site looks much cleaner:
- (void)exampleBarcode
{
[self printBarcode:[PLBarcode barcodeWithNumberSystem:10
manufacturer:20
product:30
check:40]];
[self printBarcode:[PLBarcode barcodeWithProductCode:@"asfsdf"]];
}