How do you abstract business logic?
What exactly does abstracting business logic mean and when should you do it?
A different phrasing might be “moving a series of tasks behind a model that is easier to hold in your mind”.
More often than not, it is an opportunity to create a new class that models some real world business process.
In the real world, you could think of it like making your kid get a job as an auto mechanic rather than fixing all their friend’s vehicles in your driveway.
No more greasy tools laying everywhere (or gone missing), no more uninvited guests eating all your food, no more kid working all night and sleeping in too late.
You’ve replaced all of that with
You’ve taken a convolution of behaviors and tasks and wrapped them up into a simplified unit.
The work still gets done, but now you (and your team if you have one) can think about it using a real world mental model rather than a discrete set of individual tasks.
You can say “then we use the ticket queue” rather than “then we add a row to the database that will be processed at a later time once all the earlier rows get processed.”
Let’s take another example.
In the software world, lets say you’ve got a web page that allows someone to modify product inventory.
When you first built this page, you may have decided to put all your code in one file to make things easy. You’ve mixed the logic that handles page loads, field binding, and button clicks, with the code that actually processes data and handles what should happen when different products have their inventory modified (like sending back-in-stock emails).
In this scenario, the business logic is the latter. When you are modifying data that models real world work rather than user interface concerns or other technological infrastructure, you are most lilkely dealing with business logic.
You may decide to move that logic out of your web page and into an
InventoryService class, or some other kind of small class built specifically for the purpose of inventory or even just the individual task itself (like a CQRS command handler). You are thereby creating an abstraction of some thing or some grouping of related tasks in the real world (perhaps a human that would manually reshelf supplies to a warehouse and keep a tally of what goes in and out).
Once completed, your web page can just call
InventoryService.UpdateProductSupply() rather than dealing with your storage persistence and having to know what specific tables to update.
Now, do you have to do this?
Your code will function exactly the same way whether you move the code from one class to another. It will still satisfy the requirements.
So why do it?
The value in separating distinct processes in code has been written about by me and many others ad-nauseum so I won’t bore you with the details. Please eat your heart out on this article. Or this one.
So, assuming you now know why it can be useful to do, when should you do it? Should you do it any chance you get?
Speaking as a senior developer who has worked on line-of-business style web applications my entire career, I can say… it depends. Although 99.9% of the time, it does make for higher quality code.
Generally, as a solo dev or as part of a team, you’re constantly playing a balancing game, weighing the needs of the business in the short term and the long term.
In the short term, the business wants the feature now. They could be in a pickle, losing money, have an upcoming deadline, or some other concern that dictates you finish the feature sooner rather than later.
In this case, you may want to prioritize just getting the feature working so you can push it to production as soon as possible.
Because speed is high priority, things like code design may have to be temporarily moved to the backburner.
This strategy also mirrors the popular motto that goes something like “make it work, make it right, make it fast”, which encourages developers not to spend time focusing on optimization or design prematurely.
At some point in your dev career, many times likely, you’ll find yourself obsessing over minute design decisions before your feature even works. You don’t want to be there.
I think the better strategy is always to build the simplest working version you can and then allow friction to reveal itself organically over time. That way you can apply just-in-time refactoring, refactoring when it’s truly needed, so that the proper abstraction evolves with the feature’s real use-cases.
So when will that be?
It could be never, and that’s perfectly okay. The feature is doing its job sufficiently, and the way it’s designed doesn’t impede any other process. Don’t be a perfectionist about your code. Instead focus on providing business value where it’s needed.
Now, when the time comes that you start to see the threads starting to tear in a feature and you have the time, begin to analyze how you can ease the friction.
Perhaps you notice that a particular feature is starting to reveal subtle, hard to recreate bugs. If this feature was built simply by adding business logic directly to a web page where the supporting UI lives, you might then decide to abstract the feature into it’s own component so it can be unit or integration tested without depending on the UI. That’ll help you specify and lock in the desired behavior and prevent it from continuing to leak bugs into your application.
With that out of the way, your feature could grow in complexity over a few more years and its usage could reveal a much more accurate model. Perhaps the original synchronous design is holding up other tasks, so a multi-threaded model would work nicely.
At this moment in time (and no sooner) you can refactor to the better model.
Compare those scenarios to spending days or weeks at the very beginning, before the feature even exists, trying to come up with some grand design based entirely on imagined usage and behavior.
You could waste all those weeks building something that turns out to be overly complex and misses the mark by a mile once it’s actually being exercised.
Side story: I recall having an interview with one of the major AppAmaGoogleSoft companies and the interviewer seemed shocked when I explained why I didn’t immediately fix up some old code that I had come across “just because”. My hunch was that this company hires more developers than they know what to do with so they a lot more expiremental time than your typical software engineer. Apparently teams across said company often build apps that address an identical problem, and then have to fight via business politics to get theirs to be used company-wide.
Imagine the luxury!
This isn’t to say you shouldn’t try to improve your codebase when you can, but when you prioritize business value and have a smaller team or tight deadlines, many kinds of tune-ups can sit on the backburner.
It’s more of a balancing act than a sure-thing.