Inversion of Control Containers, Dependency injection and frameworks

I've been working on updating a private server with a few back-office web services. These are small tools with a range of very particular functions, but over the years they've become a bit scrappy and difficult to maintain. I don't need a full blown CMS - there's very little actual content, so I thought I would look at some PHP frameworks. I've worked with Slim, am interested in Symfony (because Drupal 8 is using various Symfony components) and there seems to be a big interest around Laravel.

What is a framework?

As I understand it frameworks provide reliable structures common to general web applications in such a way as to give the application developer the maximum freedom to choose the components they wish to use. I suppose like a building's framework structure, you don't really see it, but it's the thing that everything is attached to and holds the whole thing up.

In a PHP framework you'd expect to find things like routing, accepting requests and sending responses, and depending on the size of the framework, it may extend to providing patterns for authentication, tools for templating content etc.

One of the things that frameworks often include is an Inversion of Control (IoC) Container and Dependency Injection. These terms were new to me and I have to admit that, like any other technical jargon, they didn't make much sense and they inhibited my progress until I went away and read the theory. Turns out they're pretty simple concepts that I've been using for years! But to cement and further my learning from the theoretical angle I thought I'd write this blog, also quietly hoping that it might help someone else, too.

What is Inversion of Control?

To invert something you have to assume a default state. This term comes from back when the default was pretty much the inverse of what it is now, so the "inversion" part of the name is perhaps the most confusing part. Delegation seems a more fitting term to me.

There are two ways that control can be inverted, or delegated. The first is giving up control of when something happens, and the second is delegating control of what to do when something happens.

An example of delegating when things happen:  In the olden days a computer program might ask a user for their name, then print "Hello, {name}, what is your email?" and wait for the email to be provided. The control of events was with the program - you couldn't move forward until you had supplied your name; you couldn't choose to put your email in first and you didn't even know you were going to be asked for this until you'd put your name in. So a modern user interface inverts this: you'd probably expect a couple of text boxes one labelled email, one labelled name. It would be up to you which you completed first and the program has to react to events as they happen; you are in control, not the program.  Event-driven applications achieve IoC. It can help make a system a more efficient (e.g. it's only doing something when there's something to do) and flexible (you not required to impose a particular order of events).

An example of delegating what happens: Perhaps the simplest example is using class interfaces.  In the example that the system expects something to move, how you get it to move would depend on what it was. If it's a bicycle, you'd need to rotate the wheels, if it's a person you'd need them to walk. So Inversion of Control here could mean implementing a standard interface in both the bicycle and the person classes that accepts a "move" command. This way the system can tell the object to move without needing to care whether it's a bicycle or a human. OK, now, a further abstraction can be achieved by asking How is the object created? How do you get the object, bicycle or person? If you put this in your code, you can in future change it from bicycle to person and thanks to the shared "move" method, it's a minimal change, but it's still created a dependency between your program and a particular implementation. This is where the IoC container and dependency injection come in, so read on.

IoC container and Dependency Injection

I think the idea here is to think that any part of the program that controls something should be as isolated as possible from other functions on which it depends. This means components can be swapped out without a big re-code.

In the what happens example above we might have ended up with a moveThing function that could have looked like this:

function moveThing() {
   $object = new Bicycle($some_configuration_options);
   $object->move();
}

And while we can edit this and replace Bicycle with Person, we've still got a dependency to manage. If we now imagine that this functionality exists inside a bigger container, and that the configuration is none of our business, we might do something like

function moveThing(IMoveableThing $object) {
   $object->move();
}

Or

function moveThing() {
  $object = Container::getConfiguredThingToMove(); 
  $object->move();
}

We still have a dependency on a moveable object, but we no longer hard-code what that object is; instead the object is injected. We can change the object and its configuration separately (e.g. in Container::getConfiguredThingToMove ) and not have to change any of this code.

Frameworks provide mechanisms for providing the objects on which processes can act. The process of finding the dependency to inject is often called service location.

Two examples: Laravel and Slim Framework

Slim provides a pretty simple way for IoC. You can get a reference to the main container, the Slim app, using a static method, and code can add objects as properties on the application that other code can work with. This main app object allows properties to be added that can then be used by the rest of the process.  So the outer configuration layer could generate an object and put it on $app->foo, and processes can then grab a reference to the app and access foo. Slim allows you to assign objects, or accessor/factory functions so that any time the property is accessed, code is run to provide the suitable object. This allows for singleton patterns, too, for things like database connections.

Laravel is a lot more complex and feature-full. It breaks things down into "services" and service providers. You then request a particular type of service, the relevant service provider is found and the object returned. Laravel also has some syntactic sugar in that you can type hint a service in your object's constructor and this will be looked up and injected when your object is created. Pretty nifty way of bringing back some of the convenience and clarity lost by all this delegation!

Laravel also has various "facade" classes which are initially very confusing because your code might look like Request::getBody() which you might think calls a static method on a class called Request but it's just a facade! Actually what happens is that a service provider is sought to provide the request object, that object is created. Then that object's getBody method is called.

Good for testing

One of the reasons that this is a good way to write software is because it makes testing easier. By being able to swap out services at the top level, your code can run on mock objects which allow its inputs and outputs to be matched to expected behaviour. By setting up a test suite (e.g. PHPUnit) you can quickly run a whole range of tests which will help you realise if you broke something by an update, tweak or new feature.

Potential pitfalls and project considerations

As I mentioned, I'm not new to these concepts, but the language and specific implementations found in Laravel, Slim and other frameworks are new to me. It does make code less readable - you always need a lot of knowledge of the bigger system to be able to read a single line of code, such as the Request::getBody() Laravel example mentioned above. Also it inevitably means that you end up evaluating code burried in strings. e.g. Slim's controller pattern whereby you specify a callback like '\Greeting:sayHello'  (or Laravel's equivalent: 'Greeting@sayHello' ) which will create an object of type Greeting and call the sayHello method.

The other pitfall in any abstraction is that it has costs and in a fervor to use the latest shiny thing (oooh Laravel 5, but not long ago it was oooh someOtherFramework that you're now stuck with) the costs are not always weighed against the benefits.

Like building a house. OK so the one made of straw was blown down, so it's definitely worth implementing a brick solution. But there's people over here saying we need an earthquake proof flexible foundation. Well yes, I wouldn't want the house to collapse if we had an earthquake. But is that likely where I plan to build it?

And heating, yes we want that. Now gas is unsustainable and will run out, but perhaps that's what we want to start with, so we'll build a radiator system - we can always replace the boiler later on with a local wood-chip one. Oh, but maybe solar heating might be better. That's going to need a storage tank. So maybe we should have a non-combi boiler and storage tanks to leave that option open to us, it would be awful to have to change the pipework later on if we want to switch the heating "service provider". But that means we can't benefit from the efficiency now of a heat-on-demand combi. Do we choose an efficient system now, with a bit more cost if we change to one particular other in the future? Or do we choose an inefficient solution now that gives us flexibility to change in the future. And in the future, might there be even better things that our best efforts at abstraction today doesn't cover?

There are very sensible abstractions and architecture choices that bring flexibility and robustness to software in the real world. However, different services have different benefits - that's precisely why you would want to choose between them in the first place. Sometimes abstraction results in never using the best features of a system and adds cost and complexity that might not pay off.

All these things need to be considered and a balance struck when embarking on a new software project.

Thanks for reading, what do you think?

As I say, I am new to the language of frameworks, so please drop a comment below if I've got anything wrong or you have something to add from your own experience.

 

 

Add new comment