It’s hard to see any lines of code that don’t touch an API...The first step is to identify the computational core of code: What is this chunk of code really doing for us? — Michael Feathers
On a high level, there’s two kind of code
- Domain Code. I use the term in general way to refer to all the code that contain logic part of our project, be it Problem/business Domain logic or UI logic.
- Infrastructure. This is the code that we write to interface with external dependencies (I/O, network, third party library or framework). Typically it consist of generic or boilerplate code like stuff that you copy-paste from stack overflow.
They are not mutually exclusive and technically they can co-exist together on the same class or functions and sadly many code do it that way. The better way though is to separate them properly with dependency inversion. Here’s why.
Easy to test and fast (in effect, enable better workflow)
Your logic code will be easy to test in isolation. No need for file, storage, network, package manager or complicated linking process. This enable you to practice any modern development approach like TDD or pair programming.
It’s a really a waste to do pair programming when what you do half of the day together is to troubleshoot environment or library dependency issue.
You also can not really do test-first if your mind distracted with resolving I/O or networking issue with left you with so little brain power to think about your class behavior and interaction first.
Your logic will reveal the intention better (thus easier to understand and maintain)
As you see on the quote above. There is nothing more distracting from your code behavior than an array of API calls interleaving with your logic.
The problem with external SDK and API is that they are made on the perspective of their maker’s interest which won’t align with your exact need fully. So, how the function sound, the parameter will have various degree of mismatch with your intention. This will make the readability of your code suffer.
With them completely isolated, your code is free to use term and structure as closely with your domain terms and intention and give people much easier time on reading and understanding it.
Rate of Change Principle
One of principle in Programming said :
put logic or data that changes at the same rate together and separate logic or data that changes at different rates — Kent Beck, Implementation Patterns
Separating your logic with infrastructure concerns is the simplest and the most straightforward way to have code that align with this principle (and the benefit of more maintainable code that goes along with it).
You change your logic in response to your business need. For example, you will need to response to user demand or to keep up with competition. It will be on different settings and rate with your work on integrating certain library or response to certain API upgrades and maintaining compatibility. Having them well-separated will help each of the updates can be done in a simple and safe manner.
The feel of working on your craft
Want to do some fancy algorithm? do functional programming? apply certain patterns? doing TDD?. Not easy if your code is inside some one else’s framework or depends on various external API or runtime. You will spent your time mostly to fulfill their specification, manage so they don’t conflict with each other and pray a lot so the API don’t break on the next version. You’ll be on the fire-fighting mode most of the time.
When you have proper boundary with your dependency, you put them on a good balance where you can still benefit from them but they don’t dictate on how you structure the important logic part. You have the logic part clean but injected with needed dependency later on when, where it is safe to do so.
The Essence of Clean Architecture
If you find implementing Clean Architecture as a whole overwhelming, you can just start with this two layers of logic and infrastructure. Just extract all the infrastructure concerns from the rest of your code . You will at least have clean classes that you have a good control on which you can fully tested. You can always refactor further later on.
Making the right call
Of course there are various degree of situation that require your common sense to make the right call. On the one extreme, you might have a simple CRUD project with almost non-existent logic you can probably get away with no explicit separation and it’s perfectly fine.
On another you might have complex interrelated domains and sub-domains with rich logic that just insert any dependency mindlessly without properly abstracting them will not be a good idea.
One good guideline is to start pure, properly separate your logic with the needed infrastructure, then only get pragmatic strategically and on case by case basis.