Use Double-Dispatch and Polymorphism to Get Rid of Conditionals

null

I want to point out a particular refactoring Jimmy Bogard shows in his video I mentioned last time.

Jimmy transforms a switch statement into a double-dispatch method call, making use of good old polymorphism to deal with variety of options. We’ll see what these terms mean when applied.

Since your typical iOS application doesn’t have a very intricate class hierarchy with lots of subclasses except those deriving from the frameworks’s view controllers, thinking about the uses of polymorphism turns out to be a good exercise.

Watch his presentation on Vimeo if you haven’t already, because it’s that cool.

Jimmy demonstrates how assigning offers to members of a coupon business became a one-liner in the actual service object. He got rid of all business logic in the service object. Instead, he pushed business logic into the Domain Model. This way, the Domain Model deserves to be called thus: the difference between a Domain Model and data containers which merely wrap the persistence layer is the behavior of the Domain Model.

Let’s have a look at an everyday switch conditional, then see what polymorphism can do to find an object-oriented alternative, and finally use double dispatch to put information in one place and behavior in another.

Tell, Don’t Ask: Push Business Logic Into Collaborators

Translated to Objective-C, Jimmy’s example might have looked like this at some point:

@implementation Member
// ...

- (void)assignOfferWithOfferType:(OfferType *)offerType 
                 offerCalculator:(id<CalculatesOffer>)offerCalculator {
    
    // How much is the discount?
    NSInteger value = [offerCalculator calculateValueFor:self 
                                               offerType:offerType];
    
    // When does it expire?
    NSDate *expirationDate;
    NSTimeInterval daysValid = offerType.daysValid * 24 * 60 * 60;
    
    switch (offerType.expirationType)
    {
        case ExpirationTypeFixed:
            expirationDate = [NSDate dateWithTimeIntervalSinceNow:daysValid];
            break;

        case ExpirationTypeAssignment:
            expirationDate = [offerType.beginDate dateByAddingTimeInterval:daysValid];
            break;

        default:
            NSAssert(false, @"invalid expiration type");
            break;
    }
    
    Offer *offer = [[Offer alloc] initWithMember:self 
                                       offerType:offerType 
                                  expirationDate:expirationDate 
                                           value:value];
    
    [self.assignedOffers addObject:offer];
}
@end

Where expirationType property is an enum:

typedef NS_ENUM(NSInteger, ExpirationType) {
    ExpirationTypeFixed,
    ExpirationTypeAssignment
};

This isn’t too atypical. And it doesn’t even look to bad or too involved.

Viewed from the outside, it’s nice that Member takes care of creating its own Offer according to an OfferType and some calculation mechanism. When Member takes care of this, the client objects don’t have to know how an Offer is created. Also, since Offer takes the member as an argument itself to form a bi-directional relationship, this eliminates mismatches altogether.

To make the method more readable and focused, you can get rid of the switch statement and write the whole method like this:

- (void)assignOfferWithOfferType:(OfferType *)offerType 
                 offerCalculator:(id<CalculatesOffer>)offerCalculator {
    
    // How much is the discount?
    NSInteger value = [offerCalculator calculateValueFor:self 
                                               offerType:offerType];
    
    // When does it expire?
    NSDate *expirationDate = [offerType calculateExpirationDate];
    
    Offer *offer = [[Offer alloc] initWithMember:self 
                                       offerType:offerType 
                                  expirationDate:expirationDate 
                                           value:value];
    
    [self.assignedOffers addObject:offer];
}

Okay, the switch statement is now hidden inside OfferType’s -calculateExpirationDate. That shortens this method significantly and makes it easier to understand what’s going on. Nothing special yet, though.

What will -calculateExpirationDate look like?

- (NSDate *)calculateExpirationDate {
    return [self.expirationType calculateExpirationDateForOfferType:self];
}

This isn’t possible as long as expirationType is an NSInteger-based enum, though. It has to be a real object instead.

First takeaway: working with primitives is quick, but it also limits the amount of encapsulation when it comes to actual behavior.

Polymorphism: Making ExpirationType a Set of Classes

To make ExpirationType a class but retain its enum-ness, we are going to define constants to make equality comparison possible:

ExpirationType * const kExpirationTypeFixed = [ExpirationType expirationTypeFixed];
ExpirationType * const kExpirationTypeAssignment = [ExpirationType expirationTypeAssignment];

// Rename the enum to free the old name
typedef NS_ENUM(NSInteger, ExpirationRule) {
    ExpirationRuleFixed,
    ExpirationRuleAssignment
};

When we move the switch statement into -calculateExpirationDateForOfferType:offerType, the naive first take will look like this:

// ExpirationType.h
@interface ExpirationType
@property (assign) ExpirationRule expirationRule;
+ (instancetype)expirationTypeFixed;
+ (instancetype)expirationTypeAssignment;
- (instancetype)initWithExpirationRule:(ExpirationRule)expirationRule;
- (NSDate *)calculateExpirationDateForOfferType:(OfferType *)offerType;
@end

// ExpirationType.m
@implementation ExpirationType
+ (instancetype)expirationTypeFixed {
    return [[self alloc] initWithExpirationRule:ExpirationRuleFixed];
}

+ (instancetype)expirationTypeAssignment {
    return [[self alloc] initWithExpirationRule:ExpirationRuleAssignment];
}

// -initWithExpirationRule: is just what you'd expect: it sets the property

- (NSDate *)calculateExpirationDateForOfferType:(OfferType *)offerType {
    
    NSTimeInterval daysValid = offerType.daysValid * 24 * 60 * 60;
    
    if (self.expirationRule == ExpirationRuleFixed)
        return [NSDate dateWithTimeIntervalSinceNow:daysValid];
    else if (self.expirationRule == ExpirationRuleAssignment)
        return [offerType.beginDate dateByAddingTimeInterval:daysValid];
    
    NSAssert(false, @"invalid expiration type");
    return nil;
}
@end

We’ve pushed logic down even another level. That’s no gain in itself. It’s just good to know that ExpirationType can determine its expiration date because that’s a more natural place to look for the information.

The double-dispatch in here is in the flow of information there and back again: OfferType delegates calculation to its ExpirationType property (first “dispatch”) and provides itself for further information. ExpirationType polls the OfferType to determine the time interval (second “dispatch”, back to the originator).

Now we get to the fun part: using polymorphism to get rid of the if statement, too. Instead of ExpirationType, we will use concrete subclasses FixedExpirationType and AssignmentExpirationType. Objective-C doesn’t know a thing about abstract classes or abstract methods. That’s a good reason to not think in terms of abstract classes at all. But we will. And we’ll hide the concrete subclasses from the outside world.

ExpirationType can get rid of a lot of functionality. It’s just a better protocol. In fact, if it wasn’t for the convenience method -daysValidTimeInterval: and the factory methods, you can perfectly model this as a protocol:

// ExpirationType.h
@interface ExpirationType
- (NSDate *)calculateExpirationDateForOfferType:(OfferType *)offerType;
@end

// ExpirationType.m    
@interface FixedExpirationType : ExpirationType
@end

@interface AssignmentExpirationType : ExpirationType
@end

@implementation ExpirationType
+ (instancetype)expirationTypeFixed {
    return [[FixedExpirationType alloc] init];
}

+ (instancetype)expirationTypeAssignment {
    return [[AssignmentExpirationType alloc] init];
}

- (NSDate *)calculateExpirationDateForOfferType:(OfferType *)offerType {
    
    NSAssert(false, @"override this method in concrete subclasses");
    return nil;
}

- (NSTimeInterval)daysValidTimeInterval:(OfferType *)offerType {
    
    return offerType.daysValid * 24 * 60 * 60;
}
@end

Place the concrete subclasses in the .m file, too, so no-one else knows about them:

@implementation FixedExpirationType
- (NSDate *)calculateExpirationDateForOfferType:(OfferType *)offerType {
    
    return [NSDate dateWithTimeIntervalSinceNow:daysValid];
}
@end

@implementation AssignmentExpirationType
- (NSDate *)calculateExpirationDateForOfferType:(OfferType *)offerType {
    
    return [offerType.beginDate dateByAddingTimeInterval:daysValid];
}
@end

Suddenly, there’s no else-clause anymore, which means one whole reason less for the app to fail. There can only be two kinds of ExpirationType. You don’t even need the enum right now. Even the constants are optional: it’s okay to deal with more than one instance of FixedExpirationType, for example, because it only performs a simple method anyway.

You will need some kind of ExpirationRule equivalent to persist the type of expiration. Maybe add another “abstract” method -toInteger which returns 0 for FixedExpirationType and 1 for AssignmentExpirationType.

Next, I’d move what -daysValidTimeInterval does into a Days Value Type to put the computation where it belongs: certainly not in the client of offerType.daysValid.

I think this is a great reminder why polymorphism is useful, and how some things can’t be solved elegantly by delegation alone.

Second takeaway: You can replace switch and if statements with polymorphism.

Watch the video or read a summary to see the rest of Jimmy’s insights.