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.



9 comments:

Sikachu! said...

Thank you for this post, Jeff.

I didn't know that Objective-C has a class name Class that actually the same as a Class in Ruby, as what I used to.

These new lines of code make it look nicer, and I think for my curiosity sake, that code made me a day :)

sco said...

At the risk of being overly picky, I don't think this example really illustrates DRY.

It's a perfectly fine refactoring, and obviously makes the main part of the code more concise. And arguably, it makes the code easier to read and maintain.

But the original version, though verbose, doesn't seem to violate DRY at all. (Assuming we're using Hunt & Thomas' definition: "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.") The appearance of repetition on the syntax level doesn't necessarily mean that any knowledge is being repeated.

On the other hand, here's a DRY violation from one of my view controllers:

self.title = @"Spots";
[self.navigationItem setTitle:@"Spots"];

There's no syntactic symmetry there, but clearly there's a piece of knowledge represented twice.

Jeff LaMarche said...

sco:

What is "knowledge"? The problem with a vague rule like DRY is that it's, well, vague. Is knowledge just discrete chunks of data or can a sequence of instructions or commands be "knowledge"? I would argue that knowledge encompasses instructions and logic..

The sequence of commands used here to create and add a controller to an array is repeated multiple times. Refactoring so that the same sequence of commands only exists once eliminates repetition of something I would say constitutes knowledge, so I would argue that it meets both the intent and spirit of DRY. Certainly, many Ruby programmers seem to agree.

Certainly, if you read the interview with Dave Thomas here:

http://www.artima.com/intv/dry.html

He certainly seems to think that way. He says that the idea is greater than code, but that it certainly includes code. He states:

If it's just code, then you can obviously organize your code so you don't repeat things, with the help of methods and subroutines

"Things" in this quote seems to necessarily point to sequences of instructinos. Neither methods nor subroutines would fix your example of DRY. His own words seem to indicate that DRY wasn't meant to be limited to discreet chunks of data only.

But, frankly, I don't really care if this qualifies as "DRY" or not. The question was posed in terms of "DRY" and I answered the question as I understood it. From Sikachu!'s comment, I'm pretty sure I answered the question that was asked.

Ole Begemann said...

There is also NSClassFromString() that saves you the conversion of the NSString to a C string.

Jeff LaMarche said...

Ole:

Good point. I'd forgotten about NSClassFromString()!

sco said...

Jeff- good point; I suppose it's possible to view "knowledge" more abstractly than I was doing.

And of course, I don't mean to malign the great work you're doing here -- I've learned a ton from this blog over the past year.

Thanks!

Smith Williams said...

wow very nice information about iPhone application development object oriented class NSClassFromString()!
thanks keep it man...

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels