PHP - - By Alejandro Gervasio

Inversion of Control – The Hollywood Principle

There’s a consensus among programmers (myself included, so here’s my own public mea culpa) that Inversion of Control (IoC) is nothing more than a synonym for plain old Dependency Injection (DI). There’s a pretty intuitive reason that sustains this mindset: if the motivation behind DI is to promote the design of classes whose external collaborators are supplied by its surrounding context rather than inversely looking up them, the process can effectively been seen as a form of IoC.

But while the DI = IoC equation can be considered generally valid, the concept of inversion of control itself is actually much broader. In fact, it could be said that DI is a specific use case which exploits the benefits of IoC, but it’s far from being the only one. This leads us back to the start; if DI is just a pattern that relies on the strengths of IoC, what’s IoC really then?

Traditionally, application components have been designed to operate on and control the execution environment, an approach that delivers well to some extent. For instance, a logging module could be implemented to log data to a file, and how and when to log the data would be a entirely under control of the module. The log file (a part of the environment in this case) would be just an external, passive element with no influence on the way the module works. But let’s say we need to extend the module’s functionality and give it the ability for additionally logging data to a database, or eventually even via email. Upgrading the module to expose the extra functionality will make it grow in complexity, becoming more bloated as the logic required to attend to these additional duties is packaged behind the same API. The approach works, but won’t scale at all.

This tangled situation can be sorted out in a fairly simple manner. Instead of making the module completely responsible for logging data to multiple endpoints, we can transfer the responsibility straight to the external environment. The module’s implementation would remain ridiculously simple, limited to acting as a simple event dispatcher. On the flip side, the environment would be responsible for implementing all of the logic required to log data entirely independent from the module in question.

Not surprisingly, the process of inverting these responsibilities between components and the environment is formally known as Inversion of Control (or in a more relaxed jargon, The Hollywood Principle), and its implementation can be a real boost when it comes to developing extensible, highly-decoupled program modules.

Of course, IoC is a language-agnostic paradigm, and as such it’s possible to consume it in the PHP world without much fuss.

Achieving Inversion of Control – Observing Domain Objects

IoC has indeed an ubiquitous presence, so it’s pretty easy to find implementations of it production. The first use case that comes to mind is Dependency Injection, but there are many other cases equally demonstrative, especially when stepping on the terrain of Event-Driven Design. If you’re wondering in what parallel universe IoC gets along with event handling mechanisms, consider a classic in the GoF repertoire: the Observer pattern.

Used nearly everywhere, even client-side via JavaScript, observers are a shining example of the IoC concept in action; there’s a highly-decoupled subject focused on doing just a few narrow tasks without polluting the surrounding context while one or more external observers are responsible for implementing the logic required for handling the events triggered by the subject. How to handle the events, and even processing new ones, is entirely a responsibility of the observers rather than the subject.

An example might be a nice way to make my previous babbling a little bit clearer. So, let’s say we’ve implemented a primitive Domain Model which defines a one-to-many relationship between blog posts and comments. In this case we’ll be deliberately ambitious and give the model the ability for firing off an email to notify the system administrator when a new comment is added to a post.

Honestly, implementing such a feature without appealing to IoC would literally be a tangled mess, as we’d be asking the domain objects to doing something that’s way outside their scope. Instead, we could take an IoC-based approach and define the domain classes as follows:

<?php
namespace Model;

interface PostInterface
{
    public function setTitle($title);
    public function getTitle();
    
    public function setContent($content);
    public function getContent();
    
    public function setComment(CommentInterface $comment);
    public function getComments();
}
<?php
namespace Model;

class Post implements PostInterface, SplSubject
{
    private $title;
    private $content;
    private $comments  = [];
    private $observers = [];

    public function __construct($title, $content) {
        $this->setTitle($title);
        $this->setContent($content);
    }
    
    public function setTitle($title) {
        if (!is_string($title) 
            || strlen($title) < 2
            || strlen($title) > 100) {
            throw new InvalidArgumentException(
                "The post title is invalid.");
        }
        $this->title = $title;
        return $this;
    }

    public function getTitle() {
        return $this->title;
    }
    
    public function setContent($content) {
        if (!is_string($content) || strlen($content) < 10) {
            throw new InvalidArgumentException(
                "The post content is invalid.");
        }
        $this->content = $content;
        return $this;
    }
    
    public function getContent() {
        return $this->content;
    }

    public function setComment(CommentInterface $comment) {
        $this->comments[] = $comment;
        $this->notify();
    }
    
    public function getComments() {
        return $this->comments;
    }

    public function attach(SplObserver $observer) { 
        $id = spl_object_hash($observer);
        if (!isset($this->observers[$id])) {
            $this->observers[$id] = $observer;
        }
        return $this;
    }
    
    public function detach(SplObserver $observer) {
        $id = spl_object_hash($observer);
        if (!isset($this->observers[$id])) {
            throw new RuntimeException(
                "Unable to detach the requested observer.");
        }
        unset($this->observers[$id]); 
        return $this;
    }
    
    public function notify() {
        foreach ($this->observers as $observer) {    
            $observer->update($this);
        }
    }
}
<?php
namespace Model;

interface CommentInterface
{
    public function setContent($content);
    public function getContent();
    
    public function setAuthor($author);
    public function getAuthor();
}
<?php
namespace Model;

class Comment implements CommentInterface
{
    private $content;
    private $author;
    
    public function __construct($content, $author) {
       $this->setContent($content);
       $this->setAuthor($author);
    }
    
    public function setContent($content) {
        if (!is_string($content) || strlen($content) < 10) {
            throw new InvalidArgumentException(
                "The comment is invalid.");
        }
        $this->content = $content;
        return $this;
    }
    
    public function getContent() {
        return $this->content;
    }
    
    public function setAuthor($author) {
        if (!is_string($author) 
            || strlen($author) < 2
            || strlen($author) > 50) {
            throw new InvalidArgumentException(
                "The author is invalid.");
        }
        $this->author = $author;
        return $this;
    }
    
    public function getAuthor() {
        return $this->author;
    }
}

The interaction between the Post and Comment classes is trivial, but the Post class deserves an in-depth look. Effectively, it has been designed as a “classic” subject, hence providing the typical API which permits to attach/detach and notify observers at will.

The most interesting facet of this process is the implementation of setComment() where the actual inversion of control takes place. The method just fires a “pull” update to all the registered observers whenever a comment is added. This means that all the logic required for sending out the email notification is delegated to one or more external observers, thus offloading the dirty work from Post, keeping it focused on just its own business logic.

With this simple but effective schema of inversion of control in place, the only structure that needs to be added to the picture is at least one observer which should be responsible for dispatching the aforementioned email. To keep things easy to follow, I’m going to implement the observer as a thin entity living and breathing in the service layer.

Delegating Control to the External Environment – Implementing a Comment Notification Service

Building an observer service capable of triggering an email notification when a new comment is added to a blog post is a simple process, reduced to defining a class that implements the pertaining update() method. If you’re curious and want to see how the service in question looks, here it is:

<?php
namespace Service;

class CommentService implements SplObserver
{
    public function update(SplSubject $post) {
        $subject = "New comment posted!";
        $message = "A comment has been made on a post entitled " .
            $post->getTitle();
        $headers = "From: "Notification System" <notify@example.com>rnMIME-Version: 1.0rn";
        if (!@mail("admin@example.com", $subject, $message, $headers)) {
            throw new RuntimeException("Unable to send the update.");
        }
    }
}

The CommentService class does exactly what it’s supposed to; it invokes its update() method to dispatch an email to the sysadmin each time a user drops a comment related to a given post.

It’d be a lot easier to see the benefits brought by inversion of control in this situation if I showed you a script that puts all the sample classes to work, so below is some code:

<?php
use LibraryLoaderAutoloader,
    ModelPost,
    ModelComment,
    ServiceCommentService;
         
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader();
$autoloader->register();

$post = new Post(
    "A sample post",
    "This is the content of the sample post"
);

$post->attach(new CommentService());

$comment = new Comment(
    "A sample comment",
    "Just commenting on the previous post"
);

$post->setComment($comment);

Quite possibly, and this is just my personal preference, I don’t feel quite right injecting a service into the internals of a domain object. The powers that be have always proclaimed the domain layer must be agnostic about the service layer (unless you appeal to separated interfaces), and this one should lay down on an upper level interoperating with multiple clients. But in this case the approach isn’t really that sinful considering the Post class is just a dummy container for the registered observers which are only consumed when triggering event updates. Moreover, taking into account how neatly the responsibilities between the service in question and the Post class have been inverted, my complaint should be considered pretty much a picky whim.

Closing Thoughts

Quite often considered an obscure, tangled concept, especially in PHP where many developers tend to intuitively associate the concept only with plain old Dependency Injection, Inversion of Control is a simple yet killer programming methodology which, when properly implemented, is a fantastic way for creating decoupled, orthogonal systems whose components can be easily tested in isolation.

If you’re using Dependency Injection within your applications (you are, right?) then you should feel your coder’s instincts pretty well satisfied as you’re already exploiting the benefits that Inversion of Control provides. As I attempted to demonstrate before, however, there’s a wide array of situations where the approach fits well other than just managing class dependencies the right way. Event-Driven Design is certainly a good example.

Image via Fotolia

Sponsors