Don’t Repeat Yourself ( DRY ) is a principle of software development and its main goal is to avoid code duplication.
“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system”
Andrew Hunt and David Thomas: The Pragmatic Programmer: From Journeyman to Master
Basically, when you find yourself writing the same code over and over again, there may be a better way to do it.
Practical case: Don’t Repeat Yourself
Let’s write an example, see whether it can be improved and do some refactors to avoid duplication.
Here there is a simple Report class which receives some data and print it via the console in a formatted way.
class Report
{
public function show(array $data)
{
echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
echo "Total: " . $data["total"] . "\n";
echo "Average x day: " . floor($data["total"] / 365) . "\n";
echo "Average x week: " . floor($data["total"] / 52) . "\n";
}
}
This code could be improved, but let’s say we are ok for the moment.
A new request for the report: it should be saved to a file. Easy right? Let’s do some copy and paste, some small changes and after some minutes we are done:
class Report
{
public function show(array $data)
{
echo "Report: " . ucwords(strtolower($data["name"])) . "\n";
echo "Product: " . ucwords(strtolower($data["product"])) . "\n";
echo "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
echo "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
echo "Total: " . $data["total"] . "\n";
echo "Average x day: " . floor($data["total"] / 365) . "\n";
echo "Average x week: " . floor($data["total"] / 52) . "\n";
echo "Average x month: " . floor($data["total"] / 12) . "\n";
}
public function saveToFile(array $data)
{
$report = '';
$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
$report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
$report .= "Total: " . $data["total"] . "\n";
$report .= "Average x day: " . floor($data["total"] / 365) . "\n";
$report .= "Average x week: " . floor($data["total"] / 52) . "\n";
$report .= "Average x month: " . floor($data["total"] / 12) . "\n";
file_put_contents("./report.txt", $report);
}
}
Wait, wait!!! Are we really done here? Of course, the request is fulfilled but it doesn’t seem very correct from a technical point of view. It is full of duplications everywhere (although the code is just a few bunches of lines). WET is everywhere (Write Everything Twice: the opposite of DRY).
Let’s do some refactoring here. These two methods mainly do the same and the only difference is the final output. A good start here is to extract the body to a new method.
This way we’ll have a single source of truth: the report will be only created in a unique point. The other methods will have the single responsibility to decide what to do with the report.
class Report
{
public function show(array $data)
{
echo $this->createReport($data);
}
public function saveToFile(array $data)
{
file_put_contents("./report.txt", $this->createReport($data));
}
private function createReport(array $data): string
{
$report = '';
$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
$report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
$report .= "Total: " . $data["total"] . "\n";
$report .= "Average x day: " . floor($data["total"] / 365) . "\n";
$report .= "Average x week: " . floor($data["total"] / 52) . "\n";
$report .= "Average x month: " . floor($data["total"] / 12) . "\n";
return $report;
}
}
Much better now, right? Anything else? There are some small duplications to be fixed.
For example, there are the same transformations on the name of the report and the product:
$report .= "Report: " . ucwords(strtolower($data["name"])) . "\n";
$report .= "Product: " . ucwords(strtolower($data["product"])) . "\n";
We could extract these transformations to a new method (or even better, to a library with its own unit tests):
private function normalizeName($name): string
{
return ucwords(strtolower($name));
}
Another duplication: the date format.
$report .= "Start date: " . date("Y/m/d", $data["startDate"]) . "\n";
$report .= "End date: " . date("Y/m/d", $data["endDate"]) . "\n";
Let’s extract that to:
private function formatDate($date): string
{
return date("Y/m/d", $date);
}
And the last one: the average calculation.
$report .= "Average x day: " . floor($data["total"] / 365) . "\n";
$report .= "Average x week: " . floor($data["total"] / 52) . "\n";
$report .= "Average x month: " . floor($data["total"] / 12) . "\n";
Although the calculation is not exactly the same, we could do something like:
private function calculateAverage(array $data, $period): string
{
return floor($data["total"] / $period);
}
So the final Report class could be:
class Report
{
public function show(array $data)
{
echo $this->createReport($data);
}
public function saveToFile(array $data)
{
file_put_contents("./report.txt", $this->createReport($data));
}
private function createReport(array $data)
{
$report = '';
$report .= "Report: " . $this->normalizeName($data["name"]) . "\n";
$report .= "Product: " . $this->normalizeName($data["product"]) . "\n";
$report .= "Start date: " . $this->formatDate($data["startDate"]) . "\n";
$report .= "End date: " . $this->formatDate($data["endDate"]) . "\n";
$report .= "Total: " . $data["total"] . "\n";
$report .= "Average x day: " . $this->calculateAverage($data, 365) . "\n";
$report .= "Average x week: " . $this->calculateAverage($data, 52) . "\n";
$report .= "Average x month: " . $this->calculateAverage($data, 12) . "\n";
return $report;
}
private function formatDate($date): string
{
return date("Y/m/d", $date);
}
private function calculateAverage(array $data, $period): string
{
return floor($data["total"] / $period);
}
private function normalizeName($name): string
{
return ucwords(strtolower($name));
}
}
Rule of three
This was a simple example but I tried to show the importance of avoiding duplication and how we could deal with it. Of course, sometimes the duplication may not be so easy to spot it. Or to get rid of it you may need some more complexity like applying some design patterns.
One important code refactoring is the Rule of three. To repeat once the same code may be ok. But the third time we build the same code, it’s time to refactor and fix the duplication.
Conclusion: Don’t Repeat Yourself
- With all these steps, WET has been removed from our code.
- If the report needs any update, it will only be done in one single point. No need to change the same in different places anymore.
- The small duplications have been also removed. The methods have meaningful names which explain what they do. Better code.
- These methods may be extracted to some helper libraries with unit tests.
- Let’s have DRY principle in mind the next you write some code.
- But don’t forget the Rule of three!
If you are interested in receiving more articles about Don’t Repeat Yourself principle, don’t forget to subscribe to our monthly newsletter here.
If you found this article about Don’t Repeat Yourself principle interesting, you might like…
Functional PHP: a first approach
Scala generics I: Scala type bounds
Scala generics II: covariance and contravariance
Scala generics III: Generalized type constraints
F-bound over a generic type in Scala
Microservices vs Monolithic architecture
The post DRY – Don’t Repeat Yourself appeared first on Apiumhub.
Top comments (0)