Why Naming Domain Rules Is Essential for Code Semantics
Albert
Posted on August 22, 2024
One of the main concerns of every software developer is to write clean code — code that not only works correctly but is also easy to understand, maintain, and improve. The code should be clear and intuitive for any developer who reads it, facilitating collaboration and continuity of work, even after a long period. Usually, when we talk about clean code, the first thing that stands out is the importance of naming within the code. Classes, methods, variables—each element needs to be named in a way that accurately reflects its function and purpose, ensuring quick and effective comprehension.
However, code clarity doesn't just depend on the naming of classes, methods, and variables. If we dig a little deeper, we realize that one of the most detrimental factors to readability and semantics is conditionals. These structures, although essential, can become confusing and difficult to interpret if not handled with due care.
Basically, conditionals are IF statements. If you have an IF in your code, it's because you have a business rule. If there is a rule, it's important that it is clear not only to the person coding but also to anyone who reads the code later.
If you write a rule in your code that isn't self-explanatory, you have a problem. The chances are high that someone will encounter your IF in the future. And if someone is going to read this code, it needs to be clear, right? Let's do a small exercise on this. The code below simulates a class that handles the finalization of an order in a store:
public class OrderProcessor
{
public void FinalizeOrder(Order order)
{
if (order.IsPaid ||
(order.HasDelivery && order.PaymentMethod == "CashOnDelivery"))
{
if (order.Products.Any(product
=> product.PromotionalDiscount == 0
&& product.AccruesLoyaltyPoints))
{
CalculateCustomerLoyaltyPoints();
}
ProcessOrderConfirmation();
IssueInvoice();
}
else
{
// Cancel order finalization
}
}
}
The code itself is relatively simple, right? However, note that we have business rules written implicitly in the code. For example, there is a rule in this store where, for a product to generate loyalty points for the customer, it cannot be sold at a promotional price.
This condition is described in product.PromotionalDiscount == 0
. Notice that, by reading the code alone, it's not so explicit that this is a business rule in the store. A developer maintaining this code in the future might question the reason for this condition, not fully understanding how it relates to the context and other rules involved. This could lead to errors during future modifications, such as incorrectly modifying, moving, or removing the rule.
In the book Clean Code, Uncle Bob mentions that we should write code that humans understand, not just machines. And for that, it's necessary that your code uses, whenever possible, human language and not just programming language.
The solution to these problems is simple: make the business rules explicit using encapsulation and human language. Let's refactor this function so that the code tells us what the domain rules are involved in finalizing an order:
public class OrderProcessor
{
public void FinalizeOrder(Order order)
{
// Domain rules for an order ready for finalization moved to a method
if (order.CanBeFinalized())
{
// Rule for generating or not loyalty points encapsulated
// in a property
if (order.GeneratesCustomerLoyaltyPoints)
{
CalculateCustomerLoyaltyPoints();
}
ProcessOrderConfirmation();
IssueInvoice();
}
else
{
// Cancel order finalization
}
}
}
public class Order
{
// Grouping the domain rules of an order that can be concluded
public bool CanBeFinalized()
{
if (IsPaid) return true;
return HasDelivery && PaymentMethod == "CashOnDelivery";
}
// The product tells us if it can generate loyalty points or not
public bool GeneratesCustomerLoyaltyPoints
=> Products.Any(product => product.CanGenerateLoyaltyPoints);
}
public class OrderProduct
{
// The rules that define whether a product is eligible to generate loyalty
// points have been encapsulated in a property of the product.
public bool CanGenerateLoyaltyPoints
=> PromotionalDiscount == 0 && AccruesLoyaltyPoints;
}
Now, all our domain rules are encapsulated in methods or properties:
- The rules that define whether an order is ready to be finalized are encapsulated in the
CanBeFinalized
method of theOrder
class. - The validations that tell us whether a product is eligible to generate loyalty points are encapsulated in
CanGenerateLoyaltyPoints
of theOrderProduct
class. - The validation in the lambda expression that tells us whether the order will generate loyalty points is encapsulated in
GeneratesCustomerLoyaltyPoints
of theOrder
class.
This adds semantics to the main method (FinalizeOrder
), making it much easier to read. If you ask someone on the team who isn't a developer to read this piece of code, they will probably understand what's happening and which domain rules are considered to finalize an order, even without understanding anything about code.
This happens because we made our domain rules clear and concise, bringing them into our natural language by encapsulating them. This is a big difference between semantic and non-semantic code. Good names for variables and methods are important, but more than that, naming our business rules is vital for the long-term survival of the code.
Note: In this article, we applied and omitted several good coding practices. Understand that the focus is on the concept of turning loose conditionals into explicit domain rules, and therefore, it is not relevant to discuss all aspects involving the quality of the exemplified code.
Share your thoughts in the comments and help the community learn from you too.
Posted on August 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.