De-coupling after the fact

This post was prompted by Facebook’s recent decision, at the time of writing, to withdraw their Parse app platform. This is a prime example of why you should make all reasonable efforts to de-couple your application/business logic from your platform logic: it’s much more difficult to migrate a tightly-coupled application than a loosely-coupled one.

That said, I also came to the realisation that a well-written, DRY application should be relatively easy  to de-couple retrospectively. How? Let’s take some examples.

The code

Let’s take as our example a module that saves user files to disk against an entity in a SQL database (for example, storing blog post assets). Firstly, let’s take a naive, highly-coupled approach:

private const string RootPath = "\\\\server\\share";

public void SaveFile(int entityId, string fileName, Stream fileStream)
{
    // should produce: \\server\share\1234\file.txt
    var fullPath = Path.Combine(RootPath, entityId, fileName);
    if (File.Exists(fileName))
    {
        throw new Exception("File already exists!!");
    }
    using (var fs = FileStream.Create(fullPath))
    {
        fileStream.CopyTo(fs);
    }
}

public IEnumerable LoadFiles(int entityId)
{
    var fullPath = Path.Combine(RootPath, entityId);
    return Directory.GetFiles(fullPath);
}

public Stream LoadFile(int entityId, string fileName)
{
    var fullPath = Path.Combine(RootPath, entityId, fileName);
    if (File.Exists(fullPath))
    {
        return File.OpenRead(fullPath);
    }

    throw new FileNotFoundException("Can't find the file", fullPath);
}

Actually, this isn’t particularly highly-coupled. What we have is a module that deals with loading and saving files. In our application code, we might have methods like:

public ActionResult Download(int entityId, string fileName)
{
    try {
        var mgr = new FileManager();
        var data = mgr.LoadFile(entityId, fileName);
        return new FileStreamResult(data, "application/octet-stream");
    } catch (Exception ex) {
        return new HttpStatusCodeResult(500);
    }
}

In this method, we instantiate a new instance of our dependency, and then use that dependency to perform our application logic. If all of our application is written like the code above, we don’t really have much of a problem. Why? Think about it: all we actually have to do to turn this into a fully de-coupled application is:

  • Create an interface from our FileManager object
  • Create a new implementation using our new underlying platform (for example, using Amazon S3 storage, or WebDAV)
  • Replace all instances of our old class with the new one (if need be, changing any references from FileManager to IFileManager)

That’s just typing. It’s a night of find/replace and manually fixing the edge cases, but it’s not a nightmare – all of your platform logic is already encapsulated into an object, and that object can be replaced.

Where it all goes wrong

So what’s the problem? That happens when code doesn’t keep itself DRY. In the example above, the developers kept all of the logic around saving and loading files in a single object, and then used that object as a gateway for all file functions. By doing this, they made it very easy to change the behaviour of IO (there are many other benefits, such as making it easy to add logging and error handling).

In a depressingly large number of applications, this does not happen. What happens is that the developers know the common location for saving files, and so they either don’t write a centralised component, or, if they do, they don’t always use it. Instead, you see code like this:

public ActionResult Download(int entityId, string fileName)
{
    try {
        var path = $"\\\\server\\share\\{entityId}\\{fileName}";
        var data = File.OpenRead(path);
        return new FileStreamResult(data, "application/octet-stream");
    } catch (Exception ex) {
        return new HttpStatusCodeResult(500);
    }
}

The problem is that the above code is often repeated throughout the application – the developers found it easier to write the logic every time than to write, or use, a re-usable component.

When you have code like that above, de-coupling becomes more difficult, and can become totally impractical (especially if the logic is replicated across multiple applications in various languages – yes, this really does happen: I’ve supported an app where such logic was repeated across VB6 components, C# services, a classic ASP application and JavaScript).

Conclusion

If your code is DRY, then events like the Parse shutdown should be annoying rather than catastrophic. To quantify it:

  • Code is de-coupled and uses DI for dependencies
    = Write new dependencies, config change – late night and pizza
  • Code is DRY but doesn’t use DI
    = Write new dependencies, create interfaces, find/replace – long weekend or a few late nights
  • Logic is repeated through the app
    = Find new job

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.