Living Apart Together: Decoupling Code and Framework

Share this article

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

Remi WolerRemi Woler
View Author

Remi is from The Netherlands and spends most of his time developing web applications and socializing. When he's not busy doing either of those, he likes watching NFL games, (ice)speed-skating, playing a game from the Half-Life(2) series, or watching a multi-layered movie.

Advanced
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week