Living Apart Together: Decoupling Code and Framework

Of course you develop using the latest technologies and frameworks. You’ve written 2.5 frameworks yourself, your code is PSR-2 compliant, fully unit-tested, has an accompanying PHPMD and PHPCS config, and may even ship with proper documentation (really, that exists!). When a new version of your favorite framework is released, you’ve already used it in your own toy project and submitted a couple of bug reports, maybe even accompanied with a unit test to prove the bug and a patch that fixes it. If that describes you, or at least the developer you want to be: reconsider the relationship your code has with the framework.

The Framework and You

Most code written today at a professional level is dependent upon some framework in one way or another. This is a good thing as it means developers are aware they’re not alone in the world and are reusing the work of others to save loads of time in the long run. There’s plenty of arguments to be found online on why you should use frameworks, and in this article I’m taking it as a proven best practice. But exactly how dependent is your code on the framework?

In my off-hours I like to hang out in the IRC channel #zftalk on irc.freenode.net and help others. When Zend Framework 2 (ZF2) was in the works, a notable trend in the channel was people asking when it would be released. Not because they were eager to use it, but because they didn’t want to start a new ZF1 project when ZF2 was about to hit. A decent project could easily take up to 3 months and if they had to start over by the end of the development process to be able to ship code that depends on “the latest and greatest” then developing it now for ZF1 would be a huge waste of time. The thought is totally understandable. Nobody likes to put time, effort and/or money into something only to find out it’s outdated and has lost half its value. If you spend 3 months coding, you want it to be the best thing released to date with no apparent flaws.

So, use Symfony (or any other framework) instead? A lot of people went this route, or even completely switched languages (Python and Ruby being popular), so they wouldn’t have to delay their projects. Others completely put their projects off, pushing them back until after the ZF2 release date! Delaying a project should never be an option, so that leaves switching frameworks to not have to suffer the version bump. But let me tell you this right now: you should develop with ZF1, even if ZF2 could hit tomorrow. Rephrase that with ZF2 and ZF3 if you want to, or insert your favorite framework and the current and future version.

Urban Solitude

For the sake of argument, let’s pretend it’s 2011 and work on ZF2 is in progress but there’s no defined timeline yet; it’s going to be done when it’s done.

While it is awesome that you re-use as much code as your favorite framework has to offer, your code has to be able to switch frameworks within a matter of days. Are you are a master of ZF1? Then write your new project in ZF1, even though ZF2 might hit next month. If you design it right, that will not be a setback even if the project stakeholders decide the project has to ship with ZF2 support. Depending on the amount of framework components you use, this change can easily be done within a week. And with the same amount of effort, you can completely switch framework vendors, and use Symfony, CakePHP, Yii, or whichever framework instead. If you write your code without coupling dependencies, and instead write small wrappers that interface with the framework, your real logic is shielded from the harsh outside world where frameworks might be upgraded or replaced. Your code lives happily in it’s own little world where everything it’s dependent on stays the same.

This all sounds very nice in theory, but I understand it can be difficult to wrap your head around without having some code examples . So, we’re still in 2011, still waiting for ZF2, and we have this awesome idea for a component that will answer the ultimate question of life, the universe, and everything. Given that it will take a little bit of time to compute the answer, we decide to store the result so that if the question if ever asked again then we can fetch it from the datastore instead of waiting another 7.5 million years to recalculate it. I’d love to show the code that actually computes the answer, but since I don’t know the ultimate question either, I’ll instead focus on the data storage part.

<?php
$solver = new MyUltimateQuestionSolver();
$answer = $solver->compute();
// now that we have the answer, let's cache it
$db = Zend_Db::Factory('PDO_MYSQL', $config['db']);
$record = array('answer' => $answer);
$db->insert('cache', $record);

Plain, simple, works as designed. But this will break when we swap out ZF1 for ZF2, Symfony, etc.

Notice that we used the decoupled vendor mechanism of Zend_Db. This same code will work just fine for another data storage if we just swap PDO_MYSQL for another wrapper. The insert() and factory() calls will still work, even if we switch to, say, SQLite. So why not do the same thing for the framework itself?

Let’s move the code into a small wrapper:

<?php
class MyWrapperDb
{
    protected $db;

    public function __construct() {
        $this->db = Zend_Db::Factory('PDO_MYSQL', $config['db']);
    }

    public function insert($table, $data) {
        $this->db->insert($table, $data);
    }
}

// Business Logic
$solver = new MyUltimateQuestionSolver();
$answer = $solver->compute();
// now that we have the answer, let's cache it
$db = new MyWrapperDb();
$db->insert('cache', array('answer' => $answer));

We’ve taken the framework-specific details out of the business logic and can now swap frameworks at any time by only modifying the wrapper.

Staying in 2011, now let’s say our stakeholders decide we need to release with MongoDB support because it’s the hottest buzzword right now. ZF1 doesn’t support MongoDB natively, so we drop the framework here and use the PHP extension instead:

<?php
class MyWrapperDb
{
    protected $db;

    public function __construct() {
        $mongo = new Mongo($config['mongoDSN']);
        $this->db = $mongo->{$config['mongoDB']};
    }

    public function insert($table, $data) {
        $this->db->{$table}->insert($data);
    }
}

// Business Logic
$solver = new MyUltimateQuestionSolver();
$answer = $solver->compute();
// now that we have the answer, let's cache it
$db = new MyWrapperDb();
$db->insert('cache', array('answer' => $answer));

Abstraction Refined

If you paid attention, you’ll notice that none of the business logic has changed when we switched to MongoDB. That’s exactly the point I’m trying to make: by writing your business logic decoupled from the framework (be it ZF1 in the first example or MongoDB in the second example), your business logic stays the same. It doesn’t take much imagination to see how you can adapt the wrappers to every possible framework for data storage out there without having to change anything in the business logic. So, whenever ZF2 drops, your code stays exactly the same. You don’t have to go through each and every line of your application to see if it uses anything from ZF1 and then refactor it to use ZF2; all you have to update is your wrappers and you’re done.

If you use this together with Dependency Injection/Service Locator or a similar design pattern, you can very easily swap wrappers around. You make one interface, a design contract that all wrappers of that type must adhere to, per solution and the wrappers can be swapped around at will. You can even write a simple mockup wrapper adhering to the same interface and unit testing will be a breeze.

Let’s add an interface and a mockup wrapper, and since ZF2 has already been released, let’s add a wrapper for that too:

<?php
Interface MyWrapperDb
{
    public function insert($table, $data);
}

class MyWrapperDbMongo implements MyWrapperDb
{
    protected $db;

    public function __construct() {
        $mongo = new Mongo($config['mongoDSN']);
        $this->db = $mongo->{$config['mongoDB']};
    }

    public function insert($table, $data) {
        $this->db->{$table}->insert($data);
    }
}

class MyWrapperDbZf1 implements MyWrapperDb
{
    protected $db;

    public function __construct() {
        $this->db = Zend_Db::Factory('PDO_MYSQL', $config['db']);
    }

    public function insert($table, $data) {
        $this->db->insert($table, $data);
    }
}

class MyWrapperDbZf2 implements MyWrapperDb
{
    protected $db;

    public function __construct() {
        $this->db = new ZendDbAdapterAdapter($config['db']);
    }

    public function insert($table, $data) {
        $sql = new ZendDbSqlSql($this->db);
        $insert = $sql->insert();
        $insert->into($table);
        $insert->columns(array_keys($data));
        $insert->values(array_values($data));
        $this->db->query(
            $sql->getSqlStringForSqlObject($insert),
            $this->db::QUERY_MODE_EXECUTE);
    }
}

class MyWrapperDbTest implements MyWrapperDb
{
    public function __construct() { }

    public function insert($table, $data) {
        return ($table === 'cache' && $data['answer'] == 42);
    }
}

// -- snip --

public function compute(MyWrapperDb $db) {
    // Business Logic
    $solver = new MyUltimateQuestionSolver();
    $answer = $solver->compute();
    // now that we have the answer, let's cache it
    $db->insert('cache', array('answer' => $answer));
}

Using the interface at the dependency injection point has imposed a rule on the wrappers: they must adhere to the interface or the code will raise an error. That means they must implement the insert() method, else they won’t satisfy the contract. Our business logic can rely on that method being present by type-hinting the interface, and really doesn’t have to care about the implementation details. Whether it’s ZF1 or ZF2 storing the data for us, the MongoDB extension, a WebDAV module uploading it to a remote server: the business logic doesn’t care. And as you see in the last example, we can even write a small mockup wrapper, implementing the same interface. If we make the Dependency Injection/Service Locator use the mockup during unit testing then we can reliably test the business logic without needing any form of data storage present. All we really need is the interface.

Conclusion

Even though your code probably isn’t so complex that it takes 7.5 million years to run, you still should design it to be portable in case the earth does get destroyed by Vogons and you have to redeploy it on a different planet (or framework). You cannot assume your favorite framework will stay backwards compatible forever or will even be around forever. Frameworks, even backed by big companies, are an implementation detail and should be decoupled as such. That way, your cool genius application can always support the latest and greatest. The real logic will live happily in the little bubble created by wrappers, shielded from all the evil implementation details and angry dependencies. So when ZF3/ Symfony3/whichever-else-big-thing gets announced: don’t stop writing code, don’t learn new frameworks because you have to (you should because you want to learn more, though), be productive inside the bubble and write the wrappers for the next big thing as soon as the next big thing gets released.

Image via Fotolia

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • http://www.audero.it/ Aurelio De Rosa

    Hi Remi. I like the article, thank you for sharing. I think this is a good point of view but I guess how many times it’ll take in a real-world application. Do you have any stats on this? Not that you must have, I’m just curious.

  • http://aaronsaray.com Aaron Saray

    This is a great concept to master. A lot of times – specific to your db examples – I work with services / mappers / models. This means that I can use my service in any project with a mapper. That mapper might depend on ZF1 db, or an xml file, or a rest service (or in my most recent case, as400 db2 data). In the same way you’re demonstrating with ‘wrappers,’ that’s what the mapper concept can do too.

    Great post!

  • http://thatonefreeman.com Matthew

    Absolutely agree with this. I’ve had to work with a variety of config files (csv, tabbed, xml, json, etc) and implementing containers saved me HOURS of work.

  • Shameer C

    Thanks for the great article!. Though true decoupling is difficult to achieve, if we look from a different angle we can see many places in our code that can be improved significantly.

  • Juan

    Ok, you are right and this is how it should be done, but it isn’t realistic on a real project, you have to rely on the framework you have chosen, if you have to write a wrapper for any single piece of your application then you didn’t need a framework, only the components. Because this can work for the database, but the views, the controllers, …

  • Khm

    It would be interesting to apply the same concept on controllers. I guess it is little harder to achieve but it pays off during the framework’s first major version upgrade :)

  • Freephile

    While I agree with the theory, I think using the example of a db connection plus insert method oversimplifies the challenge of a true web application. Are there any examples that anyone can point to where full decoupling was done which encompasses all aspects of a web application? Model, Controller, Views, Form handling, library inclusion, etc.? I think if you did that, you’d certainly have written a GREAT deal of code for the sake of the possibility of changing frameworks. The reason you choose a framework is to gain all that the framework has already done for you. If then you must hide the framework underneath your application as a swappable component, then you’re not IN the framework and you’re doing a lot of work to abstract it. Even small (micro) frameworks weigh in at 5,000 lines of code with hundreds of methods, and dozens of classes. If you’re going to abstract those or write interfaces to those, you’re writing a ton of code to be able to switch horses mid-race. You’re certainly not going to win the race. Frameworks make choices too. This one uses jQuery, that one uses YUI. I don’t see practical ways to abstract some of the more hard-wired “internal” aspects of a given framework. What this article really seems to be saying is that when you’ve bet on a particular framework; which is going through a release cycle; there are ways that you can insulate yourself in advance from the changes needed to upgrade that framework.

  • Boabramah Ernest

    I do not know if he is referring to the whole application but certainly there are portions of our application we can apply this to. If we want to apply it to controllers then may be, we have to look into a new way of creating our controllers. I have no idea on this but I humbly submit myself to be enlighten

  • http://www.itsimple.nl Jop

    Nice post. It reminds me of Robert c. Martin, aka Uncle Bob and his talk/post on “Architecture, the lost years”. The framework should be a detail. It would be nice to see all application/business logic outside the framework, but I understand this requires a completely different view on software design when you’re used te use frameworks.
    I too, think it is hard to make a ramework a ‘detail’ of the application. I haven’t been able to find an example on the internet yet. This post provides an example of just a part of the challenge.
    I wonder if one would end up with another framework to be able to decouple frameworks and other ‘details’, like a database or (user) interface.

  • Alexander Cogneau

    As for models, you can create a custom model class, which extends the base model class for your framework. Your custom models, extent the custom model class, so if you’re switching frameworks, you’ll only have to work in that one class. For controllers, I don’t really see how this is possible, as some framework use CRUD keywords in their controller methods and so on, every framework has its own syntax …

  • Paul Redmond

    If you want some real-world examples of Dependency Injection, check out http://symfony.com/doc/current/book/service_container.html

  • http://alexfraundorf.com Alex Fraundorf

    Remi,
    I think this a great article on an important topic that we would all do well to work towards.
    Thank you for sharing. Very nicely done.

  • Feras

    I think that is the entire point of the Symfony2 IoC container the idea being that you can define your services in Java/Spring like way and then when change is needed you can just change the classes defined for the services, which is a very good approach. One thing i dont like it though is that PHP editors are still way behind Java ones and do not understand the DI configuration, hence dont provice code assistance whatsoever…

  • http://www.ertankayalar.com Ertan Kayalar

    I agree with Remi. Maybe all you can’t isolate your all code. But business logic should be.
    Thanks for the post.