23 votes

Code Quality Tip: Wrapping external libraries.

Preface

Occasionally I feel the need to touch on the subject of code quality, particularly because of the importance of its impact on technical debt, especially as I continue to encounter the effects of technical debt in my own work and do my best to manage it. It's a subject that is unfortunately not emphasized nearly enough in academia.


Background

As a refresher, technical debt is the long-term cost of the design decisions in your code. These costs can manifest in different ways, such as greater difficulty in understanding what your code is doing or making non-breaking changes to it. More generally, these costs manifest as additional time and resources being spent to make some kind of change.

Sometimes these costs aren't things you think to consider. One such consideration is how difficult it might be to upgrade a specific technology in your stack. For example, what if you've built a back-end system that integrates with AWS and you suddenly need to upgrade your SDK? In a small project this might be easy, but what if you've built a system that you've been maintaining for years and it relies heavily on AWS integrations? If the method names, namespaces, argument orders, or anything else has changed between versions, then suddenly you'll need to update every single reference to an AWS-related tool in your code to reflect those changes. In larger software projects, this could be a daunting and incredibly expensive task, spanning potentially weeks or even months of work and testing.

That is, unless you keep those references to a minimum.


A Toy Example

This is where "wrapping" your external libraries comes into play. The concept of "wrapping" basically means to create some other function or object that takes care of operating the functions or object methods that you really want to target. One example might look like this:

<?php

class ImportedClass {
    public function methodThatMightBecomeModified($arg1, $arg2) {
        // Do something.
    }
}

class ImportedClassWrapper {
    private $class_instance = null;

    private function getInstance() {
        if(is_null($this->class_instance)) {
            $this->class_instance = new ImportedClass();
        }

        return $this->class_instance;
    }

    public function wrappedMethod($arg1, $arg2) {
        return $this->getInstance()->methodThatMightBecomeModified($arg1, $arg2);
    }
}

?>

Updating Tools Doesn't Have to Suck

Imagine that our ImportedClass has some important new features that we need to make use of that are only available in the most recent version, and we're several versions behind. The problem, of course, is that there were a lot of changes that ended up being made between our current version and the new version. For example, ImportedClass is now called NewImportedClass. On top of that, methodThatMightBecomeModified is now called methodThatWasModified, and the argument order ended up getting switched around!

Now imagine that we were directly calling new ImportedClass() in many different places in our code, as well as directly invoking methodThatMightBecomeModified:

<?php

$imported_class_instance = new ImportedClass();
$imported_class_instance->methodThatMightBeModified($val1, $val2);

?>

For every single instance in our code, we need to perform a replacement. There is a linear or--in terms of Big-O notation--a complexity of O(n) to make these replacements. If we assume that we only ever used this one method, and we used it 100 times, then there are 100 instances of new ImportClass() to update and another 100 instances of the method invocation, equaling 200 lines of code to change. Furthermore, we need to remember each of the replacements that need to be made and carefully avoid making any errors in the process. This is clearly non-ideal.

Now imagine that we chose instead to use the wrapper object:

<?php

$imported_class_wrapper = new ImportedClassWrapper();
$imported_class_wrapper->wrappedMethod($val1, $val2);

?>

Our updates are now limited only to the wrapper class:

<?php

class ImportedClassWrapper {
    private $class_instance = null;

    private function getInstance() {
        if(is_null($this->class_instance)) {
            $this->class_instance = new NewImportedClass();
        }

        return $this->class_instance;
    }

    public function wrappedMethod($arg1, $arg2) {
        return $this->getInstance()->methodThatWasModified($arg2, $arg1);
    }
}

?>

Rather than making changes to 200 lines of code, we've now made changes to only 2. What was once an O(n) complexity change has now turned into an O(1) complexity change to make this upgrade. Not bad for a few extra lines of code!


A Practical Example

Toy problems are all well and good, but how does this translate to reality?

Well, I ran into such a problem myself once. Running MongoDB with PHP requires the use of an external driver, and this driver provides an object representing a MongoDB ObjectId. I needed to perform a migration from one hosting provider over to a new cloud hosting provider, with the application and database services, which were originally hosted on the same physical machine, hosted on separate servers. For security reasons, this required an upgrade to a newer version of MongoDB, which in turn required an upgrade to a newer version of the driver.

This upgrade resulted in many of the calls to new MongoId() failing, because the old version of the driver would accept empty strings and other invalid ID strings and default to generating a new ObjectId, whereas the new version of the driver treated invalid ID strings as failing errors. And there were many, many cases where invalid strings were being passed into the constructor.

Even after spending hours replacing the (literally) several dozen instances of the constructor calls, there were still some places in the code where invalid strings managed to get passed in. This made for a very costly upgrade.

The bugs were easy to fix after the initial replacements, though. After wrapping new MongoId() inside of a wrapper function, a few additional conditional statements inside of the new function resolved the bugs without having to dig around the rest of the code base.


Final Thoughts

This is one of those lessons that you don't fully appreciate until you've experienced the technical debt of an unwrapped external library first-hand. Code quality is an active effort, but a worthwhile one. It requires you to be willing to throw away potentially hours or even days of work when you realize that something needs to change, because you're thinking about how to keep yourself from banging your head against a wall later down the line instead of thinking only about how to finish up your current task.

"Work smarter, not harder" means putting in some hard work upfront to keep your technical debt under control.

That's all for now, and remember: don't be fools, wrap your external tools.

6 comments

  1. [2]
    onyxleopard
    Link
    I wonder if, in the real world, any external libraries that make significant breaking changes can be truly abstracted away to be wrapped so cleanly. While this is a nice write-up, I have this...

    I wonder if, in the real world, any external libraries that make significant breaking changes can be truly abstracted away to be wrapped so cleanly. While this is a nice write-up, I have this inkling that if you tried to wrap any non-trivial library you’d quickly create a lot more work and basically end up kicking the tech-debt ball down the road such that if the wrapped lib actually made a significant breaking change, even if it were O(1), the ‘1’ would be significant. Maybe, in general, it’s still worth it. Would be really neat to see a more real-world example of this with before and after.

    6 votes
    1. Emerald_Knight
      (edited )
      Link Parent
      This is a very valid concern. That being said, even for a fairly large 1, the technical debt will typically be much smaller than n. My advice is generally to not wrap the entire library all at...

      This is a very valid concern. That being said, even for a fairly large 1, the technical debt will typically be much smaller than n.

      My advice is generally to not wrap the entire library all at once. Instead, wrap it incrementally, adding additional wrappers as needed. It should be a very procedural thing, not an all-at-once thing. After all, there's no point in wrapping the entire library if you'll only ever end up using the tiniest fraction of it.

      3 votes
  2. [2]
    meghan
    Link
    I don't think this is looking at the whole picture. At least personally, by the time I make the decision to change libraries, I'll be changing a lot more code than a few function calls and library...

    I don't think this is looking at the whole picture. At least personally, by the time I make the decision to change libraries, I'll be changing a lot more code than a few function calls and library hooks. It's very likely I'll also be refactoring large parts of the code in hand with the library change. This is because I most likely picked a new library because I decided to approach my problem from a different angle. In which case, neither the previous library or implementation are acceptable moving forward.

    6 votes
    1. Emerald_Knight
      Link Parent
      That's a very good point. I think that an important consideration here is the likelihood that you'll be switching technologies in and out of your stack. If you generally prefer to keep things...

      That's a very good point. I think that an important consideration here is the likelihood that you'll be switching technologies in and out of your stack. If you generally prefer to keep things stable and use the same tech stack, then you should anticipate the need for upgrading to newer versions and structure your code accordingly. Otherwise, you may not end up seeing any of the benefits.

      This kind of code quality recommendation is really more of a rule of thumb. As with anything, your mileage may vary and a different approach may be necessary to suit your particular development needs.

      2 votes
  3. [2]
    alexandria
    Link
    Personally my favoured thing to do is take the thing I want to abstract, be it a library or an interface, and define a new library around that. The library then stays the same, but the interfaces...

    Personally my favoured thing to do is take the thing I want to abstract, be it a library or an interface, and define a new library around that. The library then stays the same, but the interfaces it uses underneath can change and shift like sand.

    3 votes
    1. Emerald_Knight
      Link Parent
      This is exactly what wrapping is! :) Great description, by the way. It's a fantastic high-level summary of what makes it such a useful construct.

      This is exactly what wrapping is! :)

      Great description, by the way. It's a fantastic high-level summary of what makes it such a useful construct.