Photo by Martin W. Kirst on Unsplash
Domain Logic in one place
Write more maintainable and cohesive code with this one suggestion
In this article, I want to examine one of the most common problems I have faced whilst working on many applications, legacy or not. I will also suggest an alternative solution to it, but first I will need to lay out why I think this is happening and what we could look out for to avoid it in the first place. I am not aware of an official name for this phenomenon, but I call it "scattered business logic", and it simply means that the business rules that describe the problem and its solution live, spread, in multiple files and even technologies instead of being in one place.
Why it is happening
As developers, we try to write code that describes and solves real-world problems. Every application we build tries to satisfy the needs of real people, and once more, solve their real-world problems.
As developers, we also love technology and making things work. These two characteristics often make developers focus more on the implementation of the solution and less on the problem which needs to be solved. This state of focus is affecting, of course, the development process from start to finish and leads us to prioritize, for example, the usage of tools (just because it is fun to use them) instead of delivering something meaningful to our users.
All the top-tier developers I have worked with had a very direct approach of how to tackle a new feature required by the users. They were good problem solvers like most of us but also their focus was always on solving the users' problems, and not on the technologies used to do provide the solution. So, when they were faced with a problem to solve, they always first tried to avoid building a new feature to solve it. That was an eye-opening experience for me, the people who were the most skillful in the team, tried to offer a solution for the users with the existing tools of the platform, or even eliminate the problem by approaching it from different angles while I was ready to dive into my editor and code out a solution.
I have found myself many times trying to justify the use of a new data store that everyone is talking about (and is a cool and shiny toy) instead of working towards providing a solution which matters for the users of my app. And although at the end of the sprint I always deliver the feature, being so deep in the code and its intricacies, I almost always forget why I was doing all the coding for.
On top of all these challenges, we frequently receive the requirements of a new feature in so many distinct ways (tasks, tickets, slack messages) that makes it hard in the first place to extract meaningful business rules which need to be translated in clean, meaningful and easy to work on code. Once more the focus is key here, we can focus on just making the application “do” the things requested. Open our fancy editor, throw in some arrays and classes that transfer data around, save them in the store, and close the ticket. Next! This would be an easy and fast way to do everything. We satisfied our clients because the app is doing what they wanted, we did it fast enough because we jumped into the implementation straight away, and we used the document storage everyone is talking about. All boxes checked.
The problem with this approach will not show up until the next time you are called to add something to that feature, and even then it will be too early for it to be big enough to scare you into refactoring it. It is only after a handful of iterations of the same feature that everything will start to be harder to do, requiring more time and brain power.
All the complexities mentioned above can and usually (in my experience) do affect the development process and how we write and organize our code. We can easily get lost in the different/conflicting interests that are manifesting themselves in every opportunity, resulting to turn all of our efforts and focus on the wrong, not so important, things.
Problem: How the misguided focus affects our code
If we don't address all the issues mentioned above, and we are not conscious that we need to face and tackle the difficulties presented by them as soon as possible, we will probably fall into one of many pitfalls. Starting writing code before planning what you are going to build is one of these pitfalls. We need (at least) to write down what we want to achieve in every code session and how it will affect our users. These pre-dev notes can be taken just in a notepad, and they will still be enough to stir our focus in the right direction. On the other hand, jumping immediately to implementation can lead us to focus more and more on how the code comes together and how we can make a fancy query to save all the info we want, instead of focusing on the actual info (business logic) and how it is used to solve a real-life problem. As a consequence, we end up scattering important information across multiple files, as we just focus on getting the job done fast and it almost feels like we hack something together and everything works out of luck.
In the example below I will try to showcase the things that can go wrong, and although the example will be very simple it can be enough to make visible the outlying dangers and the uncertainty of the code.
<?php
// $inputs represents the inputs of the request
$book['title'] = $inputs['title'];
if (isset($inputs['discount'])) {
$book['discount'] = $inputs['discount'];
}
$this->repository->add($book);
Let's take a look at the simple example in Listing 1. There is a variable, called inputs that holds the request’s payload and more specifically the information for a new book inserted by a user. We have the title and optionally the discount of the new book. At the end of the script, the book is passed to a repository function to save it in a data store.
When I see something like the example in Listing 1, I always focus on the fact of how many things can just be implied in a small number of lines of code. For example, what is the default discount for a book? Where is it defined, is it defined in the add method of the repository, or maybe it is defined as a column default in the database? What happens if the title is just 1 letter? Is that acceptable? Are these rules enforced in all the places where our application can accept a book addition?
One other quality missing from the example code above is the declaration of the intention of the code. In a simple example like this, you can "imagine" what the code is doing but the developer who wrote it put zero effort into making the intention visible to the reader. There are many resources for intention-focused code but a simple rule I follow is to imagine being a first-time reader of my code and asking myself "what is the intention behind all that?" if the answer is not provided from the names of the variables and functions used, I try to rewrite the code until it does.
Every time I had to make a change on something seemingly simple I found myself searching in a gazillion files with zero correlation between them and sometimes I had to go as far as the table defaults to realize why the system was working as it did. There was no one place to define what a book is and how it behaves in the application. Back then I had the feeling things shouldn’t be so hard, but I did not have a solution. Convoluted logic scattered across multiple files and many times repeated, again and again, is what I saw in almost any legacy application that I have worked on, and as a beginner, at the time I always thought this as the only way to go, but it doesn't have to be like that and maybe there is also a good solution to start with.
Suggestion: Use your objects
If we take a step back, and try to imagine a better solution but simple enough so everyone can pick it up when they work on our code, we may turn our heads on the OOP side of the language we use. We can just use the raw PHP materials is offering to us. These can be our classes and their objects. Imagine instead of an array of data going around as in the example above you had an object. Not any object, but a “book object” that has properties like discount and methods like applyDiscount that contain any logic relevant to a book. OOP is giving us all the tools to translate the real world in our classes and their instances. No need for fancy advanced techniques or patterns here. We can create simple classes that just describe the information that solves the problem, and the actions that can be done upon this information. These classes are usually called entities, as they describe a real-world entity in our application. Even better, you can name these classes and their methods with the real-world words you use to describe the actions that the application performs, so you can reveal the intention of your code. If the user "can set a book title" and can "apply a discount" to it, there you go, the user case helped you tackle one of the harder things to do in programming, to name your variables!
In a hypothetical question from a business person to a developer "What is the default value of a book discount in our system" the developer should have one and only one place to look to get the answer.
<?php
class Book {
protected string $title;
protected float $discount = 0;
public function __construct($title, $discount = 0) {
$this->setTitle($title);
$this->applyDiscount($discount);
}
public function setTitle($title) {
if(strlen($title) < 3) {
throw new Exception('Book title must be more that two characters');
}
$this->title = $title;
}
public function applyDiscount($discountAmount) {
if($discountAmount < 0) {
throw new Exception('Book discount cant be a negative number');
}
$this->discount = $discountAmount;
}
}
// In the class's client file
// $inputs represents the inputs of the request
$book = new Book($inputs['title'], 1);
$this->repository->add($book);
In the above example, we have a class that is called Book. It has two properties that hold information for the title and the discount, and two methods to set these properties. Each method is called in the constructor, instead of directly assigning values to the properties, so the rules defined for each property are always validated.
By just looking in this class we can extract that the title is a string, and it cannot be less than 3 characters and the discount is float which cannot be a negative number and has the default value set to 0.
The name of the methods is also a very useful tool as mentioned before. The methods are used to hold the logic and the business rules, but also to describe what actions a book can perform by naming them with terms used by the application's users. The next developer looking into this class will be able to understand what a book is in the system and what actions can do.
With just one class, and basic OOP knowledge we have a good structure of a book in our application that describes what a book means in the real world, for our users. This class also allows us to define all the actions that can be performed in a book by a user and create a method for each action. The book class can be utilized across different scenarios, forcing us to always comply with the same rules, without the need of duplicating the same code and business logic.
By using these simple classes, we can find a suitable place for our business logic to live in. All the logic can be inside the methods of the related entity, and with the separation of concerts in mind, that entity class can just care about performing the actions on the data and not worry about anything else (e.g how all the info is persisted, etc.). In a more convoluted business domain, you can use multiple object hierarchies to express the business logic, and that is when design patterns come very useful.
Conclusion
Simple OOP knowledge can help us to describe the real-world domain our application lives in and write code that is meaningful and intention-revealing. There is a place to use everything, but my humble opinion is, to try and use objects to describe entities (and their actions) of our system instead of data holder arrays. Also, we need to be mindful that someone else is going to read our code and try to work on it, someone else can be our future self 6 months later, and we want to be helpful to them. We can try to keep all our business rules in one place, in the entities, and not scatter it everywhere because it was the fastest way to solve the problem. We need to put some thought into every decision we make with the right goal in mind, which is to solve our user's problems.
Something else to chew on after reading this article is the fact that our job as developers is to solve problems for real people and not to write fancy code using cool technologies. We need to remind ourselves of our intentions (solve the users' problem) all the time. We, either way, love to solve problems, this is why we chose this profession, we just need to focus on the right ones. There will be opportunities to solve a problem without writing a single line of code, grab that opportunity and be proud of not opening your editor and the ticket is done! I have a post-it note on my screen that reads "What does the business need?" to always remind me that I am providing a solution for the business, and of course, by extension for the application's users.