November 19, 2012

Switches Get Stitches

We have a new rule here at Roundwall Software: switches get stitches.

Switches and if statements can easily pile up in a method and make it difficult for someone to understand what a method does. They also makes testing harder because tests that cover a method need to cover every possible branch path one might take through the code. Combining ifs and switches in the same method can quickly turn into madness. They’re not very object oriented either and we’re mostly writing code in Objective-C.

We began with a challenge — can we write our code without switch statements? Turns out most of it could be replaced with clearer switch-free implementations without too much effort.

A common use case for switches appears in a table view’s data source. When the table needs to support multiple section, some of these sections are static information or perhaps each section is made of different kinds of dynamic data, the mixed-section requirement means you cannot simply depend on NSFetchedResultsController’s support for sections to do this for free. You might try to accomplish this with something like this:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
switch(section){
case 0:
  return 2;
  break;
case 1:
  return [self.items count];
  break;
}
}

It works, sure, and you can even try to eliminate magic numbers and such using enums like this:

typedef enum {
RWSSectionInfo,
RWSSectionDynamic,
RWSSectionCount
} RWSSection;

const NSInteger kNumberOfInfoRows = 2;

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
RWSSection sectionIndex = section;
	switch(sectionIndex){
		case RWSSectionInfo:
  		return kNumberOfInfoRows;
  		break;
		case RWSSectionDynamic:
  		return [self.items count];
  		break;
 	}
 }

This way at least gives some meaning to all the switches and makes it harder to ruin everything by accidentally typing a 4 when you meant to type a 2. The compiler will even remind you when you add a new section and forget to add it to one of your switches.

We could take it a step further and move the stuff in each case into its own method like so:

typedef enum {
	RWSSectionInfo,
	RWSSectionDynamic,
	RWSSectionCount
} RWSSection;

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	RWSSection section = indexPath.section;
	switch(section){
    	case RWSSectionInfo:
        	return [self infoCellForRow:indexPath.row];
        	break;
    	case RWSSectionDynamic:
        	return [self dynamicCellForRow:indexPath.row];
        	break;
    	case RWSSectionCount:
        	NSAssert(NO, @"This should never happen!");
        	break;
	}
}

Now you might say, “dude, that’s so easy to read now and I don’t think we need to do anything more,” to which I would reply, “certainly not, we have a switch to eliminate!” Much like mobster movies, we begin to plot a way to make sure these switch statements “sleep with the fishes”.

A fun place to get inspiration for architecting your Objective-C stuff is to look at Apple’s own frameworks. NSFetchedResultsController handles multiple sections without switches. Maybe we should model our solution to the way that works NSFetchedResultsController has an array of section objects that adhere to NSFetchedResultsSectionInfo. We don’t even know specifically what type they are, but we do know that we can ask for -numberOfObjects and -indexTitle and such to get the info we need to answer a table view’s data source questions. These objects let’s us write our data source methods without any switch statements (yay!). NSFetchedResultsController allows us to do nifty, object-oriented, switchless things like this:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	if<NSFetchedResultsSectionInfo> section = [self.resultsController sections][indexPath.section];
	return [section numberOfObjects];
}

Let’s do the same huh? Let’s make classes that can contain the information we need for each section and push the datasource responsibilities of our theoretical datasource into the appropriate section. Let’s take a quick stab at a solution. Perhaps something like this?

@protocol RWSSection
- (NSUInteger)numberOfObjects;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRow:(NSInteger)row;
@end

@interface RWSStaticSection : NSObject<RWSSection>
@property(nonatomic, readonly) NSArray *items;
@end


@implementation RWSSection

- (id)init
{
	self = [super init];
	if(self){
    	_items = @[@"Help I'm trapped in the debugger!", @"No no, save me instead!"];
	}
	return self;
}

- (NSUInteger)numberOfObjects
{
	return 2u;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRow:(NSInteger)row
{
	static NSString *staticIdentifier = @"staticSectionCell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:staticIdentifier];

	cell.textLabel.text = self.items[row];
	cell.detailLabel.text = @"I'm a static row!";

	return cell;
}

@end

Now we have a static section that works in a similar fashion to the NSFetchedResultsController style. No more switches, yay! We can easily make use of the section like this:

@implementation RWSViewController

- (id)initWithStyle:(UITableViewStyle)style
{
	self = [super initWithStyle:style];
	if (self) {
    	RWSStaticSection *section1 = [[RWSStaticSection alloc] init];
    	RWSStaticSection *section2 = [[RWSStaticSection alloc] init];
    	_sections = @[section1, section2];
	}
	return self;
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return [self.sections count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	RWSSection section = self.section[section];
	return [section numberOfObjects];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	RWSSection section = self.section[indexPath.section];
	return [section tableView:tableView cellForRow:indexPAth.row];
}

@end

Look at that! No switches, just objects. If you were writing unit tests, you could feed the controller a mock section to test that the view controller sends expected messages to your fake section. You could then test sections individually as you added them. Easier testing. Easier reading. Easier maintenance. Happy days are here again.