There's a big chance that you've heard of the design principle “Code to an interface, not to an implementation” before, but you don't know what it means. Or maybe you don't know how to apply it in your code. If that's the case, then continue reading; I'll try to explain it here.
If you know anything about Object-Oriented design, you've likely come across the terms coupling and cohesion. They are the two basic metrics we use to test how well our code is designed. Here's a quick refresher, in case you need it.
Coupling simply refers to the interdependencies between different components (objects) in the system. We know if a component depends on others by changing its implementation and see if others break. And in order to fix things, we have to change the others accordingly — in that case we say that this component is tightly coupled to others.
Cohesion describes how related the methods and properties of an object are. In other words, does an object do its job very well? If yes, then it's a high cohesive object.
Form the above description, it's clear that we need low coupling — to increase re-usability and maintainability — and high cohesion — to increase readability and keep complexity more manageable.
A common misconception among developers is that an interface is what we define like this:
interface MyInterface
{
//...
}
While this is true, we usually mean something different when we talk about it in terms of Object-Oriented design.
An interface is a collection of the public methods that we'll need to know in order to communicate with other objects — which we sometimes call an API (the class's API).
You can think of it as the protocol we use to communicate with a component — by the way, this is what it's actually called in Objective-C. So, we're only allowed to communicate with a certain object through this protocol (a.k.a public methods).
Now the question remains, “how to have low coupling in our code?” The answer is usually by coding to interfaces not to implementations. This can be done by depending on interfaces rather than concrete classes. All of this would be possible because of polymorphism (as you'll see later).
For example, instead of relying on a specific mailing service, you should rely on a specific interface that's common for all mailing services. If it's still confusing, see the following example.
Imagine you have a blog and you want to notify all subscribers about a newly published article. Let's say at first you decided to use Mandrill as a mailing service. So you would have something like this (this is somewhat a pseudo-like code just to illustrate the idea):
class Article
{
protected $mailer;
function __construct(Mandrill $mailer)
{
$this->mailer = $mailer;
}
public function publish()
{
// logic to publish the article
// ...
// use the Mandrill mailer object to notify all subscribers
}
}
And somewhere in your application, like a controller, you'd handle the request of publishing articles.
class BlogController
{
public function postArticle()
{
//store response data into $input
//...
$mailer = new Mandrill();
$article = new Article($mailer);
$article->publish($input);
}
}
But what would happen if after that you decided to switch to Mailgun?
In this case, you'd have to replace all related code from using Mandrill to using Mailgun, and that modification might break your code. Moreover, the modification process is not that straightforward.
So, we have to find an easier way to switch between mailing services — it should be as easy as changing one line of code! And that what we're going to do now.
The solution is, you guessed it, to code to an interface instead of an implementation.
The first thing we have to do is to define a fixed interface that all mailing services would implement. It's not necessary to be defined using the interface construct interface Mailer {...}
, we could instead define it as an abstract class if we want.
We usually choose the latter choice if we have any common methods among different mailers. However, since we don't have that yet, we'll just use the interface
.
interface ArticleMailer
{
function notifyAllSubscribers(Article $article);
}
After that, we need to have an implementation of this interface for each mailing service. In this example, we'll do it for both: Mandrill and Mailgun.
Mandrill:
class MandrillArticleMailer implements ArticleMailer
{
public function notifyAllSubscribers(Article $article)
{
//implementation specific to mandrill mailers
}
}
Mailgun:
class MailgunArticleMailer implements ArticleMailer
{
public function notifyAllSubscribers(Article $article)
{
//implementation specific to mailgun mailers
}
}
Now in the Article's class constructor, we'd reference the interface name instead of a concrete class.
class Article
{
protected $mailer;
function __construct(ArticleMailer $mailer)
{
$this->mailer = $mailer;
}
public function publish()
{
// logic to publish the article
// ...
$this->mailer->notifyAllSubscribers($this);
}
}
With that last change we are no longer coupled to any specific mailer. The great thing about that is we don't have to touch this class again for anything related to mailing. The controller is the only place where we would need to specify which mailer to use — which is awesome!
I hope you can now see how flexible this principle can make our code design. But, please note that this principle shouldn't be applied everywhere. Only apply it on the things that may vary.
I mean if you're pretty sure that you won't switch to another mailer in the future, you don't have to bother with it at all. Just apply the XP principle, which says:
Do the simplest thing that works.
I'm a freelance web developer. Laravel & VueJS are my main tools these days and I love building stuff using them. I write constantly on this blog to share my knowledge and thoughts on things related to web development... Let's be friends on twitter.