Code to an Interface: Everything You Need to Know

cover
18 Jun 2024

As you get more serious about programming, you inevitably encounter the phrase "Code to an interface", either in videos, books, or articles. And it never made sense to me. I questioned the need to create an interface and then implement it. How do I determine when and where to use these interfaces?

Whenever I watched a tutorial or read an article, they would explain what an interface is, "It is a class without implementation", and I am like "Ehmm, thanks 😏". I mean, I already knew that; what I really wanted to know was why and when to use it.

I remember one day asking on the Discord community, and one of the seniors simply said, "Don't worry; it will eventually click for you", and it did, it took some time, but it did. If you're experiencing this, know that we've all been there; let's help you understand why you need to code to an interface.

Let's Write Some Code

Since AI is taking over, and everyone is losing their shit about it, we don't want to be late to the party. We want to add it to our website, a small chatbot that answers questions about our product will do it.

I will be using PHP for my example; feel free to use any language you are comfortable with. What matters is the concept.

Our chatbot can be as simple as this:

<?php

class ChatBot
{
    public function ask(string $question): string
    {
        $client = new OpenAi();

        $response = $client->ask($question);

        return $response;
    }
}

There's a single method ask(), which uses the OpenAI SDK to connect to their API, pose a question, and then simply return the response.

We can now start using our chatbot

$bot = new ChatBot();

$response = $bot->ask('How much is product X'); // The product costs $200.

So far, the implementation looks good, it's functioning as expected, and the project is deployed and in use. But, we cannot deny that our chatbot is heavily dependent on the Open AI API; I'm sure you agree.

Now, let's consider a scenario where Open AI prices double, and continue to increase, what are our options? We either just accept our fate, or look for another API. The first option is easy, we just keep paying them, and the 2nd one is not as simple as it sounds. The new provider will likely have its own API and SDK; we will have to make updates to all classes, tests, and related components originally designed for Open AI, that's a lot of work.

This also raises concerns; what if the new API doesn't meet our expectations in terms of accuracy or has increased downtime? What if we want to just experiment with different providers simultaneously? For example, providing our subscribed clients with the OpenAI client while using a simpler API for guests? You can see how this can be complex, and you know why? Because our code was poorly designed.

We didn't have a vision; we just picked an API and were completely dependent on it and its implementation. Now, the principle of "Code to interface" would have saved us from all of this. How? Let's see.

Let's start with creating an interface:

<?php

interface AIProvider
{
    public function ask(string $question): string;
}

We have our interface, or as I like to call it, a contract. Let's implement it or code to it.

<?php

class OpenAi implements AIProvider
{
    public function ask(string $question): string
    {
        $openAiSdk = new OpenAiSDK();
        $response = $openAiSdk->ask($question);
        
        return "Open AI says: " . $response;
    }
}

class RandomAi implements AIProvider
{
    public function ask(string $question): string
    {
        $randomAiSdk = new RandomAiSDK();
        $response = $randomAiSdk->send($question);
        
        return "Random AI replies: " . $response->getResponse();
    }
}

In reality, both OpenAiSDK and RandomAiSDK will be injected through the constructor. This way ,we delegate the complex instantiation logic to a DI container, a concept known as inversion of control. This is because each provider typically requires certain configurations.

We now have two providers that we can use to answer questions. Regardless of their implementation, we are confident that when given a question, they will connect to their API and respond to it. They have to adhere to the contract AIProvider.

Now, in our ChatBot, we can do the following

class ChatBot
{
    private AIProvider $client;

    // A dependency can be injected via the constructor
    public function __construct(AIProvider $client)
    {
        $this->client = $client;
    }

    // It can also be set via a setter method
    public function setClient(AIProvider $client): void
    {
        $this->client = $client;
    }

    public function ask(string $question): string
    {
        return $this->client->ask($question);
    }
}

Note well that the example aims to demonstrate the multiple ways you can inject a dependency, in this case, an AIProvider. You are not required to use both constructors and setters.

You can see we made some tweaks; we no longer depend on OpenAI, and you won't find any reference to it. Instead, we depend on the contract/interface. And, somehow, we can relate to this example in real life; we have all been a ChatBot at least once.

Imagine purchasing a solar panel system. The company promises to send technicians to install it, assuring you that regardless of the employee they send, the job will be done, and you'll have your panels installed in the end. So, you don't really care whether they send Josh or George. They might be different, with one being better than the other, but both are contracted to install the panels.

They won't be like, you know what I am repairing your TV instead, they are obligated by the company to do the specified job. Both RandomAi and OpenAi act as employees of AIProvider; you ask a question, and they will provide an answer. Just as you didn't care about who installs the panels, the ChatBot shouldn't care at all about who does the job. It just needs to know that any implementation provided will do it.

Now, you can freely use one or another.

$bot = new ChatBot();

// For subscribed users
$bot = new ChatBot(new OpenAi());
$response = $bot->ask('How much is Product X'); // Open AI says: 200$

// For guests
$bot->setClient(new RandomAi());
$response = $bot->ask('How much is Product X'); // Random AI replies: 200$

Now, you have the flexibility to change the entire API provider, and your code will always behave the same. You don't have to change anything about it because you coded to an interface, so none of the concerns we raised earlier will be an issue.

There Are Bonuses, Always

In our example, by coding to an interface, we have also respected three of the SOLID principles, without even knowing we did, let me elaborate.

I won't go into details; each of the principles can have a long article. This is just a brief explanation to show what we gained by coding to an interface.

Open-Closed Principle

The first principle we respected is the Open-Closed Principle, which states that the code should be open for extension and closed for modification. As challenging as it may sound, you've achieved it. Think about it, the ChatBot is closed for modification now; we won't touch the code again. This was our goal from the beginning.

But, it is open for extension; if we were to add a 3rd, 4th, or even a 5th provider, nothing is stopping us. We can implement the interface, and our class can use it out of the box, no changes are required.

Liskov Substitution

I won't bore you with its definition, but basically, it states that you can substitute classes with ALL their subclasses and vice versa. Technically, all of our AI providers are-a AIProvider, and their implementations can be swapped for one another, without affecting the correctness of ChatBot, the latter does not even know which provider it is using 😂, so yes, we respected Ms. Liskov.

Dependency Inversion

I must admit, this one could have its own article. But to put it simply, the principle states that you should depend on abstractions rather than concretes, which is exactly what we are doing. We are dependent on a provider, not a specific one like Open AI.

Remember, all of this, is because we coded to an interface.

It Will Eventually Click for You

Whenever you update a class that you know you shouldn't update, and your code is getting hacky with if statements, you need an interface. Always ask yourself, does this class really need to know the how? Will I forever use this service provider? Or database driver? If not, you know what to do.

With that being said, just give it some time, it will eventually click for you.