The DRY principle: striking the delicate balance between code reuse and clarity

This article explores the DRY (Don't Repeat Yourself) principle and its impact on code quality. While emphasizing its importance in avoiding repetition and facilitating maintenance, it highlights the risks of excessive application of this principle, which can be detrimental to clarity and project evolution. The aim is to strike the right balance between reuse and simplicity.
In this article, we explore the DRY (Don't Repeat Yourself) principle and its impact on code quality. While essential for avoiding redundancy and facilitating maintenance, its excessive application can sometimes complicate the understanding and evolution of a project. Let's find out how to strike the right balance between reuse and clarity.
The DRY principle: a double-edged sword
Recently, we put the finishing touches to the initial version of our current project, which will soon be launched in production. As the complexity of our code evolves due to the nature of the project, we're focusing on making it simpler and more streamlined. By implementing best practices such as the DRY (Don't Repeat Yourself) principle to avoid redundancy, we aim to maintain high quality.
This leads me to discuss a principle that I consider to be double-edged, and which should be used judiciously while remaining pragmatic.
Why apply the DRY principle?
The DRY “Don't Repeat Yourself” principle is a software development philosophy aimed at minimizing redundancy in source code. While this approach is widely praised for its effectiveness in managing code complexity, it is essential to understand how its rigid application can sometimes create confusion.
Example:
Let's take a case where we have two entities in a Symfony application: User
and Admin
. These two entities share common properties, such as firstname
, lastname
, and email
. However, without normalization, we would be duplicating these properties in every entity, which violates the DRY principle. Here's an example of how it would look without normalization:
// src/Entity/User.php
namespace App\Entity;
class User
{
private string $firstname;
private string $lastname;
private string $email;
private string $job;
}
// src/Entity/Admin.php
namespace App\Entity;
class Admin
{
private string $firstname;
private string $lastname;
private string $email;
private array $roles;
}
In this case, the PhpStorm inspector or PHP Mess Detector would report a duplication of firstname, lastname and email properties in both entities. A classic suggestion would be to use inheritance to centralize these properties in a parent class, Account, to eliminate the duplication:
//src/Entity/Account.php
namespace App\Entity;
abstract class Account
{
private string $firstname;
private string $lastname;
private string $email;
}
Next, we inherit User
and Admin
of this class Account
, while adding properties specific to each of these entities:
//src/Entity/User.php
namespace App\Entity;
class User extends Account
{
private string $job;
}
//src/Entity/Admin.php
namespace App\Entity;
class Admin extends Account
{
private array $roles;
}
This approach centralizes shared properties in a base class. It improves readability and reduces the risk of duplication errors. However, it's important to bear in mind that this solution doesn't always have to be coupled with an ORM integration like Doctrine. Sometimes, working directly with a pure object model is better suited to keeping code independent of any framework. If Doctrine becomes necessary later on, you can always map these models with tools like MappedSuperclass
or SingleTableInheritance
.
When abstraction becomes excessive
However, this quest to reduce redundancy can sometimes lead to excessive abstraction. When developers seek to generalize too quickly, the code becomes harder to understand for those unfamiliar with the specific details of the project. Complex abstractions can create layers of indirection which, instead of clarifying the code, introduce additional levels of confusion.
Another aspect to consider is that persistence in strictly following the DRY principle can lead to generic code structures that are not optimal for each particular case. For example, when factoring a property such as a unique identifier (ID) for different entities, we may find that the associated rules vary according to the business context. Let's take the case of a system where users have simple numeric IDs, while customers require IDs with a regional prefix (e.g. EU-12345). In this case, centralizing ID management can complicate the code with additional conditions. Sometimes, intentional duplication of certain parts of the code, adapted to each entity, can improve readability and maintenance, especially when specific variations are likely to appear in the future.
DRY in the context of modern architecture
In modern architectures, particularly with microservices and Polyrepo strategies, it is sometimes encouraged to copy and paste between isolated services. As each service is autonomous and independent, a moderate duplication of code between different services can allow better management of local modifications and facilitate deployment without impacting other parts of the system.
Finding the right balance between code reuse and clarity is a constant challenge for developers. It's important to recognize that DRY is a guideline, not an absolute rule. Compromises may be necessary to maintain readable, understandable code while enjoying the benefits of reuse.
Conclusion
In conclusion, although the DRY principle is a valuable guide to improving code quality, its rigid application can sometimes lead to confusion. In the context of complex architectures such as microservices, developers may even be encouraged to duplicate code to guarantee service independence. The important thing is to remain aware of the project's context, and find the optimum balance between code reuse, readability and maintenance.