Adding is Better Than Changing
The Coder's Proverbs is a series where I summarize some lessons and principles I've learned over my career by using a memorable and simple saying of wisdom.
In the previous article of this series, I touched on the subject of change. I mentioned change as the number one enemy of any software project, and I showed how we can defeat change by isolating the things that could change so that when change shows its ugly face, it does not affect the whole of our program. This is the Single Responsibility Principle slightly rephrased.
In this article, I will go beyond that idea and say that well-designed and robust code does not need changing. A well-designed system allows for improvements to be made and new features to be coded without modifying existing code but creating new code. Let me explain what I mean by that.
Let's come back to our Pipeline
example from the previous article. In it, we defined an interesting interface we didn't touch much upon.
<?php
interface Step
{
public function process(array $record): void;
}
We made the Pipeline
class use this interface to process a record, but we didn't define such a process; that's the point behind an abstraction. Anyhow, let's pretend we implement a step that writes records somewhere.
<?php
class WriterStep implements Step
{
private Writer $writer;
public function __construct(Writer $writer)
{
$this->writer = $writer;
}
public function process(array $record): void
{
$this->writer->write($record);
}
}
For the sake of the exercise, let's pretend that Writer
just writes records to a database or some other form of persistent storage. The writer is not the important part here.
So, so far so good. This WriterStep
is pretty neat. It does what it says it does. Until someone says: "We need to log the id of every record we are going to write". So, you might be tempted to do something like this:
<?php
class WriterStep implements Step
{
private Writer $writer;
private Logger $logger;
public function __construct(Writer $writer, Logger $logger)
{
$this->writer = $writer;
$this->logger = $logger;
}
public function process(array $record): void
{
$id = (string) ($record['id'] ?? 'unknown');
$this->logger->log("Writing record of id '$id'");
$this->writer->write($record);
}
}
The problem with this approach is that it has "touched" existing code and code that was working perfectly fine without the Logger
being in there. Is not that we are afraid of bugs: we have tests for that. But we have added another responsibility to the WriterStep
. Now, it not only writes, but it also logs stuff. This class has two reasons to change now. What happens when we need to log the id in some of the pipelines and not others? Both writing and logging are coupled now, and they are impossible to separate.
When you modify a piece of code that was doing a perfectly fine job as it was, is usually an alert. There is almost always an alternative to this. The best changes in a software project are those changes that don't touch what is already there but build on top of it. Adding new stuff is much better than changing existing stuff.
A system must be well-designed to allow for that to happen. If the system you are working on is poorly designed you might not be able to follow this principle, and it might be a good idea to consider refactoring. However, when you are the one designing the system, you must make sure to provide for this kind of change: it usually involves designing and using good interfaces or abstractions.
In this case, the design we defined in the previous article allows for this without a problem. It is perfectly possible to add logging without even touching the existing WriterStep
class. For this, we use a technique called composition. This is how it looks:
<?php
// The WriterStep stays exactly the same
class WriterStep implements Step
{
private Writer $writer;
public function __construct(Writer $writer)
{
$this->writer = $writer;
}
public function process(array $record): void
{
$this->writer->write($record);
}
}
// The new LoggerStep wraps a Step and it is also
// a step itself. This is composition.
class LoggerStep implements Step
{
private Step $next;
private Logger $logger;
public function __construct(Step $next, Logger $logger)
{
$this->next = $next;
$this->logger = $logger;
}
public function process(array $record): void
{
$id = (string) ($record['id'] ?? 'unknown');
$this->logger->log("Writing record of id '$id'");
// We pass to the next step, that could be the WriterStep
$this->next->process($record);
}
}
// Instead of passing the WriterStep as is to the pipeline, you pass a compisition of both the WriterStep and the LoggerStep.
$pipeline = new Pipeline(
new LoggerStep(
new WriterStep($writer),
$logger,
),
);
This achieves the requirement of logging every id that is going to be written, but it keeps logging and writing as completely separate steps. Going back to the first principle, changes in one class should not affect the other. But now we have gone even further: we have delivered a new feature or capability without touching existing code.
I need to make a disclaimer here. It stands to reason that this principle does not mean that a codebase should not have any changes at all; that would be silly. As we have seen, the bootstrapping code in our above example (the code that created the pipeline) changed. And this is fine because this code does not contain our business logic, it's just glueing things together.
Summary
I think I got you again! What I have explained here is just the Open-Closed Principle rephrased. Many people think that this principle has exclusively to do with inheritance, but it can also be applied using composition as we have seen here. And it applies not just to classes, but to systems as well (i.e. pluggable architectures). Any sort of technique that lets you add extra behaviour to a program without modifying it, would be following this principle. This means the system would let you add behaviour (here lies the openness of it) but without modifying it (here lies the closeness).
So remember to design classes and systems that allow their functionality to be augmented, not by modifying what already is there, but by adding new things that are not there. Adding is much better than changing.
Subscribe to my newsletter
Read articles from Matías Navarro-Carter directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Matías Navarro-Carter
Matías Navarro-Carter
I'm a Chilean Software Engineer living in the United Kingdom, specifically in the cold and beautiful north coast of Northern Ireland. I moved here in March 2020 -- yes, at the very start of a global pandemic! -- with my wife Briony and our cat Pua to be closer to her side of the family. Originally a History B.A., I made the career switch around 2015 once I discovered how fun and intellectually stimulating Engineering is. Apart from building useful stuff other people can use, I love sharing what I've learned and hearing what others have learned too.