Functional programmers talk about two things a lot — avoiding side effects and avoiding state. At first, this seems impossible: how the heck am I supposed to write code without side effects and without state? The whole point of programs are to do stuff and remember things! Avoiding side effects is still something I’m figuring out, but this week, I have some tips and tricks on avoiding state.
I’ve had the most luck with this approach: don’t try to totally avoid state, but to limit it wherever possible.
There are lots of techniques for limiting state, and I’ll list a few here. It isn’t complete, but I hope that it provides enough a jump start to understand the general pattern.
Understating
The broad strategy here in all of these ideas is to reduce the number of instance variables you have, which simplifies your classes. Let’s take a simple example, like a table view controller. If you’re not using Apple’s built-in UITableViewController
, you might have an extra @property UITableView *tableView
. This generates an additional instance variable. Your -loadView
method might look like this:
- (void)loadView {
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
tableView.dataSource = self;
tableView.delegate = self;
self.tableView = tableView;
self.view = tableView;
}
While it seems easy enough to keep both of these properties in sync, what’s actually going on is that one is acting as a “cache” of sorts for the other. Keeping a cache in sync with the primary is always hard, even when it looks easy up front. Even the best programmers make mistakes. Instead, you can use a little-known feature called covariant return types to redeclare the view
property with the correct type:
@property (nonatomic) UITableView *view;
Mark it as @dynamic
:
@dynamic view;
And then forward the tableView
message to the view
property:
- (UITableView *)tableView {
return self.view;
}
With covariant return types, there’s no casting required! The compiler knows what you intend. You’re using a computed property and nothing needs to be kept in sync anymore because there’s nothing to be kept in sync!
Unite the States
Another way to limit instance variables is with state machines. I’ve written about state machines here before. Before state machines you might have a mish-mash of properties describing the state of, say, a network request:
@property BOOL isUnsent;
@property BOOL isFetching;
@property BOOL isCompleted;
@property NSError *error;
@property NSArray *results;
The problem is that the “space” of this state is huge, and large swaths of that state space are are invalid. For example, what does it mean if more than one of the BOOL
properties are YES
? What if none of them are YES
? What if isFetching
is YES
and the error
had a value? To solve this problem, you can keep one property around:
@property SKRequestState *state;
This state property can have store values of different types, like SKLoadingState
, SKErrorState
(which stores the error), and SKCompletedState
(which stores the results). You can then make those properties readonly and forward them directly to the state
property.
- (BOOL)isUnsent {
return self.state.isUnsent;
}
- (BOOL)isFetching {
return self.state.isFetching;
}
- (BOOL)isCompleted {
return self.state.isCompleted;
}
- (NSError *)error {
return self.state.error;
}
- (NSArray *)results {
return self.state.results;
}
All states respond to each of those messages, returning nil
where necessary. This way, while the external surface of the object is still the same, you’ll never fail to keep the class in internal synchrony.
If you’ve got a primitive, like a BOOL
or an NSInteger
in a class, you can ask yourself: is this really just a number, or do I need to wrap it in something that ascribes meaning to it?
If there are two or more primitives in your class, ask: are they unrelated, or should I formalize their relationship in code?
If a property is nil
for part of the object’s lifecycle, ask: what meaning is hidden in the nothingness of this property, and how can I make that meaning more obvious to the reader of my code?
Using state machines helps enforce honesty about what’s complicated.
The Null Hypothesis
Imagine a presenter that downloads a user object from the server and exposes an interface for displaying that user. Sometimes, the presenter encounters an error and displays a different message for the user’s name.
@implementation SKUserPresenter
- (void)fetchUser {
[self.fetcher fetchWithSuccessBlock:^(SKUser *user) {
self.user = user;
} failureBlock:^(NSError *error) {
self.userFetchError = error;
}];
}
- (NSString *)name {
if (!self.userFetchError) {
return self.user.name;
}
return @"User not found.";
}
//...
@end
This object is now keeping an extra thing ( userFetchError
) around just so it can handle a special case. The current intention of this code is that either user
or userFetchError
can have values, but never both. However, you aren’t constrained by the design of the class to ensure this invariant is maintained.
Another member of your team, perhaps future-you, could easily cause user
and userFetchError
to both have values. To solve this problem, we can constrain our instance variables better by using the Null Object pattern.
@implementation SKUserPresenter
- (void)fetchUser {
[self.fetcher fetchWithSuccessBlock:^(SKUser *user) {
self.user = user;
} failureBlock:^(NSError *error) {
self.user = [SKMissingUser new];
}];
}
- (NSString *)name {
return self.user.name;
}
//...
@end
If you wanted to go the extra mile, you could initialize the SKMissingUser
with the error, and have it pull its message from the type of error. The broad pattern is the same, however: fewer instance variables, more simplicity.
The Fourth Estate
When limiting state, I find the following rules of thumb to helpful.
- Using fewer instance variables is better than using more.
- Removing reliance on primitives gives meaning to an object’s state as it is exposed internally.
- Using computed or readonly properties gives meaning to an object’s state as it is exposed externally.