Monday, July 20, 2009

Refactoring Nav from Chapter 9

I received an interesting question (in the form of a tweet) today about Chapter 9. An observant reader asked if there was a way to "DRY" (Don't Repeat Yourself) the code where we add all the controllers to the array that drives the root view controller's table, this code here:

- (void)viewDidLoad {
self.title = @"First Level";
NSMutableArray *array = [[NSMutableArray alloc] init];

// Disclosure Button
DisclosureButtonController *disclosureButtonController =
[[DisclosureButtonController alloc]
initWithStyle:UITableViewStylePlain]
;
disclosureButtonController.title = @"Disclosure Buttons";
disclosureButtonController.rowImage = [UIImage
imageNamed:@"disclosureButtonControllerIcon.png"]
;
[array addObject:disclosureButtonController];
[disclosureButtonController release];

// Check List
CheckListController *checkListController = [[CheckListController alloc]
initWithStyle:UITableViewStylePlain]
;
checkListController.title = @"Check One";
checkListController.rowImage = [UIImage imageNamed:
@"checkmarkControllerIcon.png"
]
;
[array addObject:checkListController];
[checkListController release];

// Table Row Controls
RowControlsController *rowControlsController =
[[RowControlsController alloc]
initWithStyle:UITableViewStylePlain]
;
rowControlsController.title = @"Row Controls";
rowControlsController.rowImage = [UIImage imageNamed:
@"rowControlsIcon.png"
]
;
[array addObject:rowControlsController];
[rowControlsController release];


// Move Me
MoveMeController *moveMeController = [[MoveMeController alloc]
initWithStyle:UITableViewStylePlain]
;
moveMeController.title = @"Move Me";
moveMeController.rowImage = [UIImage imageNamed:@"moveMeIcon.png"];
[array addObject:moveMeController];
[moveMeController release];

// Delete Me
DeleteMeController *deleteMeController = [[DeleteMeController alloc]
initWithStyle:UITableViewStylePlain]
;
deleteMeController.title = @"Delete Me";
deleteMeController.rowImage = [UIImage imageNamed:@"deleteMeIcon.png"];
[array addObject:deleteMeController];
[deleteMeController release];

// President View/Edit
PresidentsViewController *presidentsViewController =
[[PresidentsViewController alloc]
initWithStyle:UITableViewStylePlain]
;
presidentsViewController.title = @"Detail Edit";
presidentsViewController.rowImage = [UIImage imageNamed:
@"detailEditIcon.png"
]
;
[array addObject:presidentsViewController];
[presidentsViewController release];

self.controllers = array;
[array release];
[super viewDidLoad];
}

It's a good spot. This is, in fact, a prime candidate for refactoring. Notice how similar all the chunks of code are. With the exception of the controller class, title, and image name, each chunk of code is basically identical.

The answer to whether this can be DRY'ed, yes. This can be refactored in Objective-C and probably should. We didn't do it in the book basically because Chapter 9 was already long enough without having to use Class objects or the Objective-C runtime, and we were concerned this would add something confusing to an already long and difficult chapter.

But, my blog doesn't have to be only beginner friendly, so let's look at how we could refactor this chunk of code. First and foremost, let's start by changing the controllers property from an NSArray to an NSMutableArray so its contents can be modified by an instance method.

#import <Foundation/Foundation.h>


@interface FirstLevelViewController : UITableViewController {
NSMutableArray *controllers;
}

@property (nonatomic, retain) NSMutableArray *controllers;
@end


Next, we can create a method that will add a controller to that array. Since the items that are not the same between the various chunks of code are the controller class, the title, and the image name, we need the method to take arguments for each of those.

If we know and have access at compile time to all the classes that we will be using, we can do this pretty easily by creating a method that takes a Class object. This is the object that represents the singleton meta-object that exists for every Objective-C class. When you call a class method, you are actually calling a method on this object and you can call class methods on Class objects. So, in this scenario where we know all the classes we'll be using, we can write this method:

- (void)addControllerOfClass:(Class)controllerClass usingTitle:(NSString *)title withImageNamed:(NSString *)imageName {
SecondLevelViewController *controller = [[controllerClass alloc] initWithStyle:UITableViewStylePlain];
controller.title = title;
controller.rowImage = [UIImage imageNamed:imageName];
[self.controllers addObject:controller];
[controller release];
}

We create an instance of the correct class by calling alloc on the Class object, which returns an instance, which we can then initialize ordinarily. We declare this an instance to be the abstract superclass of all the second level controllers, SecondLevelViewController, which allows us to use both the title and rowImage properties without having to typecast or set the values by key.

Then, our viewDidLoad method becomes much, much shorter and without all the repeated code:

- (void)viewDidLoad {
self.title = @"First Level";
NSMutableArray *array = [[NSMutableArray alloc] init];
self.controllers = array;
[array release];

[self addControllerOfClass:[DisclosureButtonController class] usingTitle:@"Disclosure Buttons" withImageNamed:@"disclosureButtonControllerIcon.png"];
[self addControllerOfClass:[CheckListController class] usingTitle:@"Check One" withImageNamed:@"checkmarkControllerIcon.png"];
[self addControllerOfClass:[RowControlsController class] usingTitle:@"Row Controls" withImageNamed:@"rowControlsIcon.png"];
[self addControllerOfClass:[MoveMeController class] usingTitle:@"Move Me" withImageNamed:@"moveMeIcon.png"];
[self addControllerOfClass:[DeleteMeController class] usingTitle:@"Delete Me" withImageNamed:@"deleteMeIcon.png"];
[self addControllerOfClass:[PresidentsViewController class] usingTitle:@"Detail Edit" withImageNamed:@"detailEditIcon.png"];

[super viewDidLoad];
}

But, what if you don't know all the classes at compile time? Say, if you want to create a generic class to go into a static library? You can still do it, but you lose the compile-time check for the class and have to use an Objective-C runtime method to derive a Class object from the name of the class. Easy enough, though. Under that scenario, here's our new method:

- (void)addControllerOfName:(NSString *)controllerClassName usingTitle:(NSString *)title withImageNamed:(NSString *)imageName {

Class controllerClass = objc_getClass([controllerClassName UTF8String]);
SecondLevelViewController *controller = [[controllerClass alloc] initWithStyle:UITableViewStylePlain];
controller.title = title;
controller.rowImage = [UIImage imageNamed:imageName];
[self.controllers addObject:controller];
[controller release];
}

Notice that the only difference is that we take an NSString * parameter rather than a Class parameter, and then we get the correct Class object using the Objective-C runtime function called objc_getClass(). This function actually takes a C-string, not an NSString, so we get a C-string using the UTF8String instance method on our NSString.

In this case, we have to change our viewDidLoad method slightly to pass string constants, rather than Class objects:

- (void)viewDidLoad {
self.title = @"First Level";
NSMutableArray *array = [[NSMutableArray alloc] init];
self.controllers = array;
[array release];

[self addControllerOfName:@"DisclosureButtonController" usingTitle:@"Disclosure Buttons" withImageNamed:@"disclosureButtonControllerIcon.png"];
[self addControllerOfName:@"CheckListController" usingTitle:@"Check One" withImageNamed:@"checkmarkControllerIcon.png"];
[self addControllerOfName:@"RowControlsController" usingTitle:@"Row Controls" withImageNamed:@"rowControlsIcon.png"];
[self addControllerOfName:@"MoveMeController" usingTitle:@"Move Me" withImageNamed:@"moveMeIcon.png"];
[self addControllerOfName:@"DeleteMeController" usingTitle:@"Delete Me" withImageNamed:@"deleteMeIcon.png"];
[self addControllerOfName:@"PresidentsViewController" usingTitle:@"Detail Edit" withImageNamed:@"detailEditIcon.png"];

[super viewDidLoad];
}


Either of these options will be much easier to maintain and extend than the version in the book. You should be on the lookout for refactoring opportunities in your own code, as well. Sometimes an ounce of refactoring can save a pound of headache down the line.

No comments:

Post a Comment