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.
understanding_interfaces.png

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.

 
10
Kudos
 
10
Kudos

Now read this

Creating Debug Builds on iOS

If you are like me, you tend to use the same device for testing the version of your app submitted to the App Store and the version you are currently working on. It would be great if we could install those versions separately so that we... Continue →