mokacoding

unit and acceptance testing, automation, productivity

Explicit Dependencies for Code with No Surprises

Imagine you go back to your childhood bedroom, and find a cardboard box with "Marvel Comics" written on its lid, in fat black marker. You open the box and what you find is your collection of Spiderman, Daredevil, X-Men comics, but also your old Game Boy, a crayon, your sister's favourite Barbie, and a bag of lollies that is clearly past its due date. That is not what you were expecting. Inside that box there were a lot of hidden, and in case of the lollies disappointing, surprises.

Like that box, our classes, and structs, can expose deceivingly simple interfaces, while hiding a blob of spaghetti code, complexity and dependencies in their implementations.

What lies under the hood of your interface

In this post we are going to see how to make an object's dependencies explicit, why it is a good idea, and the trade-offs we make when choosing such a design.

Consider this interface:

typedef void (^Completion)(NSArray *products, NSError *error);

@interface ProductsService: NSObject

- (void)allProducts:(Completion)completion;

@end

It looks fairly simple, right? Let's have a look at the implementation:

@implementation ProductsService

- (void)allProducts:(Completion)completion {
    User *user = [[AppStateService sharedInstance] currentUser];
    [NetworkService sharedInstance] getAllProductsForUser:user withSuccess:^(NSDictionary *responseDictionary) {
      Parser *parser = [Parser alloc] init];
      NSArray *products = [parser parseProducts:responseDictionary];
      completion(products, nil);
    } failure:^(NSError *error) {
      completion(nil, error);
    }];
}

@end

See what's going on in there? Apart from the disgusting sharedInstances, the -allProducts: method is using other three objects in its internals: AppStateService, Parser and NetworkService.

Now, there is conceptually nothing wrong with this. You could say that ProductsService is acting as a facade and that is the embodiment of the information hiding principle. And you would be right.

However, there is a high surprise effect between what the interface exposes and what the implementation does. Hiding all the components involved in the process can make it harder to see all the moving parts of the system, reason about it, and understand how to make changes safely.

How to make dependencies explicit

Making a class's dependencies explicit is quite simple. Let's look at the ProductService from above.

@interface ProductsService: NSObject

- (instancetype)initWithParser:(Parser *)parser
                  stateService:(AppStateService *)stateService
                networkService:(NetworkService *)networkService;

//...

@end

@implementation ProductsSevrice

- (instancetype)initWithParser:(Parser *)parser
                  stateService:(AppStateService *)stateService
                  networkService:(NetworkService *)networkService {
  self = [super init];
  if (!self) { return nil; }

  self.parser = parser;
  self.stateService = stateService;
  self.networkService = networkService;

  return self
}

//...

@end

And we can then rewrite allProducts like this:

- (void)allProducts:(Completion)completion {
    User *user = [self.stateService currentUser];
    [self.networkService getAllProductsForUser:user withSuccess:^(NSDictionary *responseDictionary) {
      NSArray *products = [self.parser parseProducts:responseDictionary];
      completion(products, nil);
    } failure:^(NSError *error) {
      completion(nil, error);
    }];
}

The idea is simple, instead of instantiating the objects used in the method's internals, we simply pass them as init arguments, that will be stored in instance properties.

The benefits of explicit dependencies

Dependencies are clear in the interface. By exposing all the objects our class needs in its internals either in the init or as method parameters we provide readers of the interfaces all the information and all the context around the class.

Testability. This is probably the bigger advantage of explicit dependencies. By doing this we provide what Michael Feathers calls seams in Working Effectively with Legacy Code. We can now use mocks or fakes when testing, therefore controlling the inputs our class receives and making sure the test is deterministic.

When to use this approach

Software development is all about tradeoffs, as Kent Beck reminds us:

Kent Beck tradeoffs gif

We already touched on the fact that by making all the dependencies explicit we lose some of the benefits of information hiding, and our code takes more time do deal with, we have to instantiate or pass around all those instances. What we lose from that side we gain in clarity, flexibility, and testability.

So the question is when to choose which?

My rule of thumb is that when working with application code, code that implements the functionality of the product, that is strictly related to the problem space the app is addressing, and that is not intended to be used in other projects, all the dependencies should be explicit. This makes it easier to put into context, and marries well with a test driven development style.

When instead the code is for a library, or when there is the need to add a layer of abstraction, for example to provide a simpler set of APIs, hiding complexity, than that's a good case for hiding dependencies.

There is a middle ground

Turns out you can have your pie and eat it too when it comes to explicit dependencies and simple interfaces.

For example you can make the dependencies explicit in the designated initializer, and provide a convenience factory method that does all the hard work for you.

@interface ProductsService: NSObject

- (instancetype)initWithParser:(Parser *)parser
                  stateService:(AppStateService *)stateService
                networkService:(NetworkService *)networkService NS_DESIGNATED_INITIALIZER;

//...

@end

@interface ProductsService (Convenience)

+ (instanceType)productsService;

@end

I personally like to put factory methods in a category extension, just to make the separation from the core code of the class clearer.

When designing code is important to remember that being too smart is not a clever idea. Code that does too much, with side effects, or with implementations that don't behave as one would expect when looking at the interface are dangerous. When writing code we should think of our readers and apply the principle of least astonishment, everything should behave as the they would expect. Making the dependencies of your classes explicit is one good technique to make sure that once the interface lid is lifted and we take a look at the implementation there are no surprises.

Leave the codebase better than you found it.

Vote on Hacker News