Writing Decoupled Code: Why Bother?

I had a conversation recently about writing testable code. It touched on coding against interfaces, and decoupling business logic from infrastructural side-effects. The conversation was with a seasoned Go developer, who seemed to take a different view than I’m used to.

In object-oriented land, decoupling is the law. I have an number of habits from my experience as a software architect in PHP, and I’m still learning whether they are common practice in Go. One of them is the instinct that, the moment there is any mention of infrastructure (filework, databases etc.), you must immediatly start coding against an interface. Your business logic should be completely decoupled from any infrastructure.

The Software Developer’s Answer

Ask a senior software developer in an OO language why filework can’t go in the same code as business logic, and the answer will sound a bit like this:

Because we have to write tests, and I don’t want them writing to disk. I have to write a mock object (or stub, or spy…) to represent the thing doing filework, in a unit testing context.

Whether this is deemed to be a good enough reason to write decoupled code may depend on the developer’s attitude towards the importance of testing.

The Software Architect’s Answer

Ask a decent software architect why filework gets cordoned off from business logic, however, and you may hear a different argument:

In the short term, it’s necessary for unit testing. But the real value has nothing to do with tests. When you call your logic from a production code point of invocation, and from a unit testing context, you are reusing your code. You are the first person to reuse your code, right now. If it’s annoying to reuse your code, that is a problem that gets solved now. If you constructor inject objects with infrastructural side effects, hidden behind an interface, however, your code is maintainable. It is easy to control what it is doing now through dependency injection alone.

Want your calculations written to disk? Pass in a real filework object. Want to fake the infrastructure layer while you test your code? Pass in a test double of some description. Want to replace the filework component with something that uses a database two years after the code was written? Just write a rival implementation of the data access object interface that does what you want, and swap it in through dependency injection. Your code is open to extension and closed to modification, so configuring the object graph alone is enough to control it from the outside. You don’t need to change it.

Because your code is testable, it is also maintainable, and not just because of the tests.