This is a continuation of the blog post 12 Principles of Good Software Design.
3. An idiom should make the intent of your code clear
I’ve seen in a number of places a number of rules for writing good code. Many of the rules revolve around having a class method do only one thing, or making sure functions have no side effects or that classes can be described with an “elevator pitch”; described in 10 seconds or less to someone who hasn’t a clue.
All of these ideas work because they help make clear the intent of your code.
Clarifying the intent of your code is important for a variety of reasons. By clarifying the intent of your code you make the intended functionality clear to yourself and to others who may work on your software. Clarifying the intent helps you organize your code in such a way so that it can be more easily maintained. And in striving for clear code you keep unnecessary complexity from creeping in.
There are a number of other ways to clarify the intent of your code.
Here’s a simple example. We have three buttons on iOS. In our -viewDidLoad: method we set up the user interface. One button stands out as the default button, the other two have lighter borders.
- (void)viewDidLoad { [super viewDidLoad]; /* * Set up the appearance of our interface */ self.firstButton.layer.borderColor = [UIColor lightGrayColor].CGColor; self.secondButton.layer.borderColor = [UIColor lightGrayColor].CGColor; self.firstButton.layer.borderWidth = 1; self.secondButton.layer.borderWidth = 1; self.thirdButton.layer.borderWidth = 2; self.thirdButton.layer.borderColor = [UIColor darkGrayColor].CGColor; self.firstButton.layer.cornerRadius = 6; self.secondButton.layer.cornerRadius = 6; self.thirdButton.layer.cornerRadius = 6; }
Sure, the purpose of the -viewDidLoad: method is to initialize the user interface. But notice we’re doing more or less the same thing for each of the buttons. If we were to take the initialization and roll it into a separate method we can make the intent of our setup code a lot more clear.
First, refactor our common startup in a separate method:
+ (void)setupButton:(UIButton *)button asDefault:(BOOL)defFlag { button.layer.borderColor = defFlag ? [UIColor darkGrayColor].CGColor : [UIColor lightGrayColor].CGColor; button.layer.borderWidth = defFlag ? 2 : 1; button.layer.cornerRadius = 6; }
Now our startup code in -viewDidLoad: becomes far more apparent:
- (void)viewDidLoad { [super viewDidLoad]; /* * Set up the apperance of our interface */ [ViewController setupButton:self.firstButton asDefault:NO]; [ViewController setupButton:self.secondButton asDefault:NO]; [ViewController setupButton:self.thirdButton asDefault:YES]; }
We can readily see the intent of our 9 lines of initialization code.
And the advantage of expressing our intent in this way is that, in the future, if our requirements for the appearance of our buttons change, we only need to change the +setupButton:asDefault: method, as we’ve separated the semantics of how the button is used (asDefault:YES verses NO) from the actual appearance.
We can find another example from older versions of iOS. The UIAlertView class has a method for a delegate to receive a callback if the user dismisses the alert using a particular button. This allows you to prompt a user, and take action if they select “OK” from the alert.
For example, suppose we have a method which prompts the user to save data:
- (void)shouldSave { UIAlertView *v = [[UIAlertView alloc] initWithTitle:@"Save?" message:@"Should this file be saved?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Save", nil]; [v show]; }
The problem is that we only show half of the intent of our code. Meaning the real intent of our code is to prompt the user, then to take action if the user responds in a particular way. Yet we only see half the intent; the other half is a few lines or a few hundred lines away:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == alertView.firstOtherButtonIndex) { ( Do save operation ) } }
It gets worse if we have multiple alert views within a single controller; if we use our view controller as the single delegate for multiple alerts, we have to set some parameter in the alert, then create a switch statement to figure out which alert we were displaying.
Awkward.
Yet a simple helper class to allow us to get a call back in an Objective C block when an alert button is pressed can help us keep the code together–making the intent of our code more obvious.
Our AlertView class extends the UIAlertView with a new method:
@interface AlertView : UIAlertView<UIAlertViewDelegate> - (void)showWithCallback:(void (^)(NSInteger btn))callback; @end
The implementation is straight forward: the alert uses itself as the delegate, and passes the click button to the block provided:
#import "AlertView.h" @interface AlertView () @property (copy) void (^callback)(NSInteger btn); @end @implementation AlertView - (void)showWithCallback:(void (^)(NSInteger btn))cb { self.callback = cb; self.delegate = self; [self show]; } - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { self.callback(buttonIndex); } @end
Now we can make the intent of our alert code more clear:
- (void)shouldSave { AlertView *v = [[AlertView alloc] initWithTitle:@"Save?" message:@"Should this file be saved?" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:@"Save", nil]; [v showWithCallback:^(NSInteger buttonIndex) { if (buttonIndex == v.firstOtherButtonIndex) { // Do save operation } }]; }
The intent is far clearer because the action of displaying the alert, and the code which handles the results of the alert, are now all together in one place.
Sometimes the intent of your code can be hard to convey because it implements a complex algorithm or does something unusual. But most of the applications we are asked to write are relatively simple: a screen is presented and the contents loaded from some source. You alter the contents of the data represented on the screen. The contents are then saved, and any errors along the way are displayed to the user.
Simple.
And by keeping the intent clear of each component we create, from setting up our user interface to saving the contents of our screen, you make it easier to maintain your code.