Understanding Interfaces
You might have heard of this design principle before: “Program to an interface, not an implementation.” Let’s understand what that really means.
Consider we are building an application that shows movie information to the user. To get things started, we will download a list of top 10 movies released in 2014 from Rotten Tomatoes and save it in a json file.
// movies.json
{
"movies": [
{
"id": "770860165",
"title": "Boyhood"
},
{
"id": "770860166",
"title": "The LEGO Movie"
},
// 8 more entries not shown here ...
]
}
We can easily write a class that reads the content of movies.json
into memory. Let’s call it MoviesInfoFileReader
.
@implementation MoviesInfoFileReader
- (NSArray *)readMoviesInfo {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *path = [bundle pathForResource:@"movies"
ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
NSDictionary *jsonObjects =
[NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:nil];
return jsonObjects[@"movies"];
}
@end
Although the example code in this post are in Objective-C, the concepts presented here should apply to other languages as well.
MoviesInfoFileReader
first determines the path of movies.json
file in app bundle. It then reads contents of the file into a NSData object which is then passed to NSJSONSerialization to create an array of dictionaries.
Error handling is not shown here to keep things simple.
Let’s create another class that takes this info and builds Movie
objects.
@implementation MoviesBuilder
- (NSArray *)buildMoviesWithInfoReader:(MoviesInfoFileReader *)infoReader {
NSArray *info = [infoReader readMoviesInfo];
NSMutableArray *movies = [NSMutableArray arrayWithCapacity:info.count];
for (NSDictionary *movieInfo in info) {
Movie *movie = [[Movie alloc] init];
movie.movieID = movieInfo[@"id"];
movie.title = movieInfo[@"title"];
[movies addObject:movie];
}
return [movies copy];
}
@end
Movie
is a simple model object that holds onto movie info.
@interface Movie : NSObject
@property (nonatomic) NSNumber *movieID;
@property (nonatomic) NSString *title;
@end
Now creating Movie
objects from the info read from movies.json
file is as simple as this:
MoviesInfoFileReader *reader = [[MoviesInfoFileReader alloc] init];
MoviesBuilder *builder = [[MoviesBuilder alloc] init];
NSArray *movies = [builder buildMoviesWithInfoReader:reader];
Problem #
Everything looks fine so far. As it turns out Rotten Tomatoes makes the top movies info available via RESTful APIs as well. Instead of just showing the top 10 movies from 2014 only, it would be nice to get that info from a server so that we don’t need to update the movies.json
file when year 2015 comes around. Let’s create a class that gets the movie info from a remote server.
@implementation MoviesInfoServerReader
- (NSArray *)readMoviesInfo {
// Read top 10 movies info from remote server
}
@end
Although the process of getting movies info from a remote server is quite different from reading it from a file, our MoviesBuilder
class doesn’t really care where
that info comes from. All it cares is that we give it an object that knows how to read that info. Despite the fact that MoviesInfoServerReader
provides the exact same method as MoviesInfoFileReader
, we can’t just pass it to MoviesBuilder
in place of a file reader. MoviesBuilder
is expecting an object of type MoviesInfoFileReader
. It seems we painted ourselves into a corner by programming to an implementation instead of an interface.
Alternate Implementation #
Luckily, solving this problem is not hard. Before we implement a solution though, let’s ask ourselves “what’s one thing that’s common between MoviesInfoFileReader
and MoviesInfoServerReader
?”. Both of them play a role of providing raw movies info. Let’s capture that role in an interface.
@protocol MoviesInfoReader <NSObject>
@required
- (NSArray *)readMoviesInfo;
@end
Objective-C allows us to define interfaces via protocols.
For any object to fulfill the role of providing raw movies info, it must conform to MoviesInfoReader
protocol. In other words, the object must implement the readMoviesInfo
method because it is defined as a required method. Let’s make MoviesInfoFileReader
and MoviesInfoServerReader
classes conform to MoviesInfoReader
protocol.
#import "MoviesInfoReader.h"
@interface MoviesInfoFileReader : NSObject <MoviesInfoReader>
@end
#import "MoviesInfoReader.h"
@interface MoviesInfoServerReader : NSObject <MoviesInfoReader>
@end
We are not quite there yet. We need to change buildMoviesWithInfoReader:
method’s signature to accept an object that conforms to MoviesInfoReader
protocol instead of the hard-coded type MoviesInfoFileReader
.
#import "MoviesInfoReader.h"
@interface MoviesBuilder : NSObject
- (NSArray *)buildMoviesWithInfoReader:(id<MoviesInfoReader>)infoReader;
@end
Objective-C has a rather awkward way of specifying that a parameter needs to conform to a protocol. Little nuisances like this are fixed in Swift.
Now we are ready to swap MoviesInfoFileReader
and MoviesInfoServerReader
without having to change MoviesBuilder
at all.
MoviesInfoServerReader *reader = [[MoviesInfoServerReader alloc] init];
MoviesBuilder *builder = [[MoviesBuilder alloc] init];
NSArray *movies = [builder buildMoviesWithInfoReader:reader];
MoviesInfoFileReader *reader = [[MoviesInfoFileReader alloc] init];
MoviesBuilder *builder = [[MoviesBuilder alloc] init];
NSArray *movies = [builder buildMoviesWithInfoReader:reader];
That is pretty neat, isn’t it?
Testing #
One other area where this programming to an interface principle comes very handy is testing. Now that MoviesBuilder
class accepts any object that conforms to MoviesInfoReader
protocol, we can easily create a fake movies info reader suitable for our testing purpose. Let’s see how that fake object looks like.
#import "MoviesInfoReader.h"
@interface FakeMoviesInfoReader : NSObject <MoviesInfoReader>
@end
@implementation FakeMoviesInfoReader
- (NSArray *)readMoviesInfo {
NSDictionary *fakeMovie1 = @{@"id": @1,
@"title": @"Fake movie 1"};
NSDictionary *fakeMovie2 = @{@"id": @2,
@"title": @"Fake movie 2"};
return @[fakeMovie1, fakeMovie2];
}
@end
Instead of reading movies info from a file or remote server, the fake object returns a hard-coded array of dictionaries. Being able to remove dependencies on actual implementation like this is very important when writing speedy and reliable tests. If the tests for MoviesBuilder
fail, we will know that it’s because we didn’t implement it right. Not because we failed to read movies info from a file or remote server. Here is a test for MoviesBuilder
that shows FakeMoviesInfoReader
in action.
@implementation MoviesBuilderTests
- (void)testMoviesBuiltContainValuesPresentInRawInfo {
MoviesBuilder *builder = [[MoviesBuilder alloc] init];
FakeMoviesInfoReader *fakeReader = [[FakeMoviesInfoReader alloc] init];
NSArray *movies = [builder buildMoviesWithInfoReader:fakeReader];
NSArray *rawMoviesInfo = [fakeReader readMoviesInfo];
Movie *movie1 = movies[0];
NSDictionary *rawMovieInfo1 = rawMoviesInfo[0];
XCTAssertEqualObjects(movie1.movieID, rawMovieInfo1[@"id"],
"1st movie's ID should be the ID in 1st raw dictionary");
XCTAssertEqualObjects(movie1.title, rawMovieInfo1[@"title"],
"1st movie's title should be the title in 1st raw dictionary");
Movie *movie2 = movies[1];
NSDictionary *rawMovieInfo2 = rawMoviesInfo[1];
XCTAssertEqualObjects(movie2.movieID, rawMovieInfo2[@"id"],
"2nd movie's ID should be the ID in 2nd raw dictionary");
XCTAssertEqualObjects(movie2.title, rawMovieInfo2[@"title"],
"2nd movie's title should be the title in 2nd raw dictionary");
}
@end
Conclusion #
Figure below shows the relationship between various classes we created so far.
At this point you might be wondering if we should create an interface for every class in our application so that we are always programming to an interface. If we were to do that things could get out of control very quickly. Our app will be riddled with interfaces.
We can solve this problem by relying on one of the most fundamental design principles: “Identify the aspects of your application that vary and separate them from what stays the same.” If you think a certain aspect of your app is going to change frequently, then that’s where you need to introduce interfaces.
There are other ways to adhere to the program to an interface design principle, namely abstract classes, mixins and traits. We will discuss those in a future post.
The full code is available on Github.