Interfacing is Decoupling

Interfacing is Decoupling

The Coder's Proverbs #5

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.


I think this is one of the most incredible inventions on earth.

UK Electrical Outlet

I'm not talking about the UK electrical outlet in specific, but of the electrical outlet in general. Think about how our lives would be without electrical outlets, and just by having the cables there, ready to be used. Just like this:

8 Signs You May Have a Problem with Your Electrical Wiring | UL Solutions

We would need to wire up all the appliances in our homes manually, from cable to cable. Apart from being something extremely dangerous, it would be a slow and cumbersome process. Imagine you just need to temporarily unwire a lamp to connect a vacuum cleaner. It would take loads of time! Also, you could get the wires all mixed up: connect the ground to the live and the live to the neutral, and so on. All of our devices would be coupled together to the point of being too hard to change.

The convenience of the electrical outlet is that it removes all that complexity. My appliances only need to implement an interface that conforms to the outlet (the plug) to be able to be correctly connected and be easily swappable to any of the outlets in my house. It removes the need for me to have low-level knowledge about electricity and wiring. It's just so much simpler.

Outlet Mentality

When building software, we should be thinking like the inventor of the electrical outlet at all times. But many times we don't think like that. For instance, if our program logic requires us to write a report, we immediately write it to the filesystem. We do something like this:

<?php

class ReportWriter
{
    public function writeReport(array $records): void
    {
        $resource = fopen('/some/file.path', 'wb');

        foreach ($records as $i => $record) {
            $line = sprintf('Record %d: %s', $i, $record['contents']);
            fwrite($resource, $line);
        }

        fwrite($resource, PHP_EOL);
        $end = sprintf('Number of records: %d', count($records));
        fwrite($resouce, $end);
    }
}

The code above is like connecting the appliances in your house (your business logic) directly to the electrical wires (the filesystem). Here, we have high-level business logic (the writing and structuring of a report), depending on something low-level (the place where it is stored, the filesystem).

It's much better if we make an abstraction for writing (we don't care where we write), and make both our business logic and our filesystem depend on it.

<?php

// This is the abstraction
interface Writer
{
    public function write(string $data): int;
}

// The filesystem implementing the abstraction
class PhpResource implements Writer
{
    public static function open(string $filename): PhpResource
    {
        return new self(fopen($filename, 'wb'));
    }

    private function __construct(
        private $resource
    ) { }

    public function write(string $data): int
    {
        return fwrite($this->resource, $data);
    }
}

// The business logic using the abstraction
class ReportWriter
{
    public function __construct(
        private readonly Writer $output,
    ) { }

    public function writeReport(array $records): void
    {
        foreach ($records as $i => $record) {
            $line = sprintf('Record %d: %s', $i, $record['contents']);
            $this->writer->write($line.PHP_EOL);
        }

        $this->writer->write(PHP_EOL);
        $end = sprintf('Number of records: %d', count($records));
        $this->writer->write($end.PHP_EOL);
    }
}

// This is how you bootstrap it
$resource = PhpResource::open('/some/file.path');
$reportWriter = new ReportWriter($resource);

Now the high-level business logic does not need to know about the filesystem. And the low-level stuff (writing to a file in the filesystem) has also been simplified under the Writer interface. The Writer interface is our electrical outlet: is what makes it possible to connect our business logic to the filesystem without them knowing anything about each other.

This decoupling is powerful. This is what makes programs to be resilient and also easy to test. Because we don't depend on the filesystem now, while testing, we can have an in-memory Writer in which we can assert that the contents were written as intended.

This is how you decouple software components: by putting an interface in between them. Interfaces, like the electrical outlet, are one of the best inventions since Object Oriented Programming itself.

Conclusion

This is just a summary of the Dependency Inversion Principle in SOLID, which states that "High-level modules should not depend on low-level modules, but rather both should depend on abstractions". In our example above, high-level business logic (the writing of a report) was depending on the low-level filesystem operations (for writing a file). Introducing an abstraction that both parties rely on, making it possible to decouple them. Now our business logic can be freely used with any type that implements Writer.

Generally speaking, making your business code rely on abstractions (interfaces) is the best way to decouple it from other things. This is how I write my programs nowadays: I don't even start with the database or the HTTP framework, but rather, I design commands and handlers that rely only on interfaces to do the business actions I need, and then I implement them later. This ensures I focus on the things my business actions need. This approach has the nice benefit that it completely decouples your code from a framework or database library, making it more robust, easier to test and easier to change.

Just remember: interfacing is decoupling.

Did you find this article valuable?

Support Matías Navarro-Carter by becoming a sponsor. Any amount is appreciated!