DEV Community

Cover image for Adapter Pattern vs. Bridge Pattern
Doeke Norg
Doeke Norg

Posted on • Originally published at doeken.org

Adapter Pattern vs. Bridge Pattern

The Adapter pattern and the Bridge Pattern have brought along a lot of confusion. In this post we're going to look at what they are, what their differences are & where they might be similar.

πŸ”Œ Adapter Pattern

The Adapter Pattern tries to solve the problem of making two (or more) incompatible classes compatible, by using an intermediate class that implements a predefined interface.

- Wait what? Let's try this again!

The problem

Imagine a single Feed that wants to display the latest topics from multiple sources, like: Reddit & Hacker News. For these sources we have two API clients: RedditApi and HackerNewsApi. Both return a list of topics, but their APIs are not the same.

class RedditApi {
    public function fetchTopicItems(): RedditFeedIterator {
        // Returns a `RedditFeedIterator` that provides `Topic` objects, that hold a `title`, `date` and `url`.
    }
}

class HackerNewsApi {
    public function getTopics(): array {
        // returns an array of ['topic_title' => '...', 'topic_date' => '...', 'topic_url' => '...']
    }
}
Enter fullscreen mode Exit fullscreen mode

We don't want to make our feed know about the different implementations, because we might want to add another source in the future and that would mean adding even more code to the feed. Instead, we'll apply the Adapter Pattern.

The solution

The Adapter Pattern consists of these 4 elements:

  • πŸ™‹ Client: This is the class that want's to connect to multiple sources. This would be Feed in our example.
  • πŸ“š Adaptee: A source the Client wants to connect to. In our example we have two: RedditApi & HackerNewsApi.
  • 🎯 Target: An interface or contract that defines a single API the Client will connect to.
  • πŸ”Œ Adapter: A class that implements the Target interface and delegates to an Adaptee source and formats its output.

First let's settle on a Target interface; we'll call it TopicAdapterInterface and it will have a getTopics() method that returns an iterable of topics, where every topic is an array with title, date and url. So it can be an array of arrays, or a Generator/Iterator of arrays.

If you aren't familiar with Generators or Iterators, then please check out my Generators over arrays post.

interface TopicAdapterInterface
{
    /**
     * @return iterable Iterable of topic array ['title' => '...', 'date' => '...', 'url' => '...']
     */
    public function getTopics(): iterable;
}
Enter fullscreen mode Exit fullscreen mode

Now we can create a Feed class that uses these adapters. We'll loop over every adapter, and yield their results, so we get a single continuous stream of topics as a Generator. This of course doesn't take a date into consideration, but it's enough for this example.

class Feed
{
    /**
     * @param TopicAdapterInterface[] $adapters The adapters.
     */
    public function __construct(public array $adapters) {}

    public function getAllTopics(): iterable
    {
        foreach ($this->adapters as $adapter) {
            yield from $adapter->getTopics();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

So we have a Client Feed, a Target TopicAdapterInterface and two Adaptees RedditApi & HackerNewsApi. That means that we are only missing two Adapters. We'll create these first, and then we'll look at what makes them tick.

To make it a bit easier working with the Iterators, I'll be using the iterator_map() function from my doekenorg/iterator-functions package.

class RedditTopicAdapter implements TopicAdapterInterface
{
    public function __construct(public RedditApi $reddit_api) {}

    public function getTopics(): iterable
    {
        return iterator_map(
            fn (Topic $topic) => [
                'title' => $topic->getTitle(),
                'date' => $topic->getDate('Y-m-d H:i:s'),
                'url' => $topic->getUrl(),
            ],
            $this->reddit_api->fetchTopicItems(),
        );
    }
}

class HackerNewsTopicAdapter implements TopicAdapterInterface
{
    public function __construct(public HackerNewsApi $hacker_news_api) {}

    public function getTopics(): iterable
    {
        return iterator_map(
            fn (array $topic) => [
                'title' => $topic['topic_title'],
                'date' => \DateTime::createFromFormat('H:i:s Y-m-d', $topic['topic_date'])->format('Y-m-d H:i:s'),
                'url' => $topic['topic_url'],
            ],
            $this->hacker_news_api->getTopics(),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Here you can see our two adapters: RedditTopicAdapter and HackerNewsTopicAdapter. Both of these classes implement the TopicAdapterInterface and provide the required getTopics() method. They each get their own Adaptee injected as a dependency, and use this to retrieve topics and format it to the required array.

This means that our Feed can now use these adapters by injecting them in its constructor. To connect this all together it could look a little something like this:

$hacker_news_adapter = new HackerNewsAdapter(new HackerNewsApi());
$reddit_adapter = new RedditTopicAdapter(new RedditApi());
$feed = new Feed([$hacker_news_adapter, $reddit_adapter]);

foreach ($feed->getAllTopics() as $topic) {
    var_dump($topic); // arrays of [`title`, `date` and `url`]
}
Enter fullscreen mode Exit fullscreen mode

Benefits of the Adapter Pattern

  • πŸ”„ You can plug in an extra Adapter at a later time, without having to change he Client implementation.
  • πŸ–– Only the Adapter needs to know about the Adaptee which enforces separation of concerns.
  • πŸ”¬ The Client code is easily testable, because it only relies on a Target interface.
  • πŸ“¦ When working with an IoC container you can usually get / tag all services with a specific interface, making it very easy to find and inject or auto-wire all Adapters into the Client.

Real world examples

The Adapter Pattern is one of the most used patterns, because of its extendability. It can even be extended by other packages without the original packages having to change. Here are some real world examples of this.

Cache adapters

Most frameworks have a caching system that has a single API for working with it, while providing adapters for different implementations, like: redis, memcache or a filesystem cache. Laravel calls these adapters a Store and you can find these stores in illuminate/cache. They provide the Target interface for such a store in the illuminate/contracts
repository.

Filesystem adapters

Another common thing is to write data to files. Files that may be located somewhere else, like: an FTP server, a Dropbox folder or Google Drive. One of the most used packages for writing data to files is thephpleague/flysystem. This packages provides a FilesystemAdapter interface that can have specific implementations. And because of this Target interface, others can build 3rd-party packages that provide another Filesystem; like: spatie/flysystem-dropbox by Spatie.

πŸ”€ Bridge Pattern

The Bridge Pattern is often confused with the Adapter Pattern, and with good reasons. Let's look at what problem this pattern tries to solve and how it is different from the Adapter Pattern.

The problem

Let's say we have two editors: a MarkdownEditor and a WysiwygEditor. Both editors can read and format some file and update the source on that file. The MarkdownEditor obviously returns Markdown text, while the WysiwygEditor returns HTML.

class WysiwygEditor
{
    public function __construct(public string $file_path) {}

    protected function format(): string
    {
        return '<h1>Source</h1>'; // The formatted source.
    }

    public function read(): string
    {
        return file_get_contents($this->file_path);
    }

    public function store(): void
    {
        file_put_contents($this->file_path, $this->format());
    }
}

class MarkdownEditor
{
    public function __construct(public string $file_path) {}

    protected function format(): string
    {
        return '# Source'; // The formatted source.
    }

    public function read(): string
    {
        return file_get_contents($this->file_path);
    }

    public function store(): void
    {
        file_put_contents($this->file_path, $this->format());
    }
}
Enter fullscreen mode Exit fullscreen mode

At some point in time, we need a Markdown editor and a WYSIWYG editor that can read & store files on an FTP server. We could create a new editor that extends the MarkdownEditor or WysiwygEditor and overwrites the read() and store() method. However, this will likely introduce a lot of duplicate code between the two. Instead, we'll use the Bridge Pattern.

The solution

The Bridge Pattern also consist of 4 elements:

  • 🎨 Abstraction: An abstract base class that delegates some predefined functions to a Implementor. In our example this will be an AbstractEditor.
  • πŸ§‘β€πŸŽ¨ Refined Abstraction: A specific implementation of the Abstraction. In our example this will be MarkdownEditor and WysiwygEditor.
  • πŸ–ŒοΈ Implementor: An interface that the Abstraction uses for delegation. In our example this will be a FileSystemInterface
  • πŸ–ΌοΈ Concrete Implementor: A specific implementation of the Implementor that actually does the work. In our example this will be LocalFileSystem and a FtpFileSystem.

It is at this point, I think one of the things that makes this pattern hard to grasp is this:

There is no bridge

Unlike the Adapter Pattern, where there is an actual Adapter; the Bridge Pattern does not have a Bridge. But no worries, we'll see the thing that makes this the Bridge soon enough!

Refactoring the code

Let's refactor our example code by implementing the Bridge Pattern. We'll start by extracting the Abstraction from our two Editors.

abstract class AbstractEditor {
    public function __construct(public string $file_path) {}

    abstract protected function format(): string;

    public function read(): string
    {
        return file_get_contents($this->file_path);
    }

    public function store(): void
    {
        file_put_contents($this->file_path, $this->format());
    }
}

class WysiwygEditor extends AbstractEditor
{
    protected function format(): string
    {
        return '<h1>Source</h1>'; // The formatted source.
    }
}

class MarkdownEditor extends AbstractEditor
{
    protected function format(): string
    {
        return '# Source'; // The formatted source.
    }
}
Enter fullscreen mode Exit fullscreen mode

In this refactoring we've created an AbstractEditor that now contains all the duplicate code there was between the editors, and made the editors extend this abstraction. This way the editors, or Refined Abstractions, are only focussing on what they do best: formatting the source of the file.

But remember, we still don't have a Implementor or a Refined Implementor and we really want to use multiple file systems. So let's create the Implementor and a LocalFileSystem as the first Refined Implementor. Then we'll update the AbstractEditor to use the Implementor.

interface FilesystemInterface {
    public function read(string $file_path): string;

    public function store(string $file_path, string $file_contents): void;
}

class LocalFileSystem implements FilesystemInterface {
    public function read(string $file_path): string
    {
        return file_get_contents($file_path);
    }

    public function store(string $file_path, string $file_contents): void
    {
        file_put_contents($file_path, $file_contents);
    }
}

abstract class AbstractEditor {
    public function __construct(private FilesystemInterface $filesystem, private string $file_path) {}

    abstract protected function format(): string;

    public function read(): string
    {
        return $this->filesystem->read($this->file_path);
    }

    public function store(): void
    {
        $this->filesystem->store($this->file_path, $this->format());
    }
}
Enter fullscreen mode Exit fullscreen mode

So here is the "Bridge". It's the connection between the Abstraction and the Implementor. It connects one editor to one filesystem. But now the two can vary independently. We can add multiple editors that all have their own formatting, like yaml, json or csv. And all these editors can use any filesystem to read and store those files.

So now we can create a FtpFileSystem that reads and stores the formatted content on an FTP server.

class FtpFileSystem implements FilesystemInterface {
    public function read(string $file_path): string
    {
        // Imagine the ultimate FTP file reading code here.
    }

    public function store(string $file_path, string $file_contents): void
    {
        // Imagine the ultimate FTP file writing code here.
    }
}
Enter fullscreen mode Exit fullscreen mode

By using the Bridge Pattern we've made it possible to make 4 different implementation combinations:

// 1. A local markdown file editor
new MardownEditor(new LocalFileSystem(), 'local-file.md')
// 2. An FTP markdown file editor
new MardownEditor(new FtpFileSystem(), 'ftp-file.md')
// 3. A local WYSIWYG file editor
new WysiwygEditor(new LocalFileSystem(), 'local-file.html')
// 4. An FTP WYSIWYG file editor
new WysiwygEditor(new FtpFileSystem(), 'ftp-file.html')
Enter fullscreen mode Exit fullscreen mode

And if we were to add another AbstractEditor and another FileSystem we'd have 9 possible combination, while only adding 2 classes 🀯!

Benefits of the Bridge Pattern

As we've seen there are some benefits to using the Bridge Pattern:

  • πŸ’§ The code is more DRY (Don't Repeat Yourself) by extracting the Abstraction.
  • 🧱 It is more extendable by creating two separate abstractions that can vary independently.
  • πŸ”¬ The individual classes are smaller and therefore easier to test and understand.

Similarities with Adapter Pattern

Another reason why some have trouble understanding the difference between the Bridge Pattern and the Adapter Pattern is that the connecting part of the "bridge" actually looks like an Adapter.

  • A Client could be seen as the Abstraction as that also delegates to an interface.
  • A Target could be seen as the Implementor as this also defines an interface to adhere to.
  • An Adapter could be seen as the Refined Implementor because this implements the interface and fulfills the requirements.

That last one is probably the most confusing; as a Refined Implementor can actually be an Adapter to a dependency or Adaptee, but this is not required. The Refined Implementor will often be a class on its own, while an Adapter will always delegate. But the two are indeed not mutually exclusive.

Thanks for reading

I hope you enjoyed reading this article! If so, please leave a ❀️ or a πŸ¦„ and consider subscribing! I write posts on PHP almost every week. You can also follow me on twitter for more content and the occasional tip.

Top comments (5)

Collapse
 
huyduy profile image
Huy Duy

Let's say I want to upload to Google Drive but sometimes upload to Dropbox. Which pattern should I use?

Collapse
 
doekenorg profile image
Doeke Norg

It always depends, but first thought would be adapter. And I can hugely recommend using FlySystem in that case. There are third party adapters for both.

Collapse
 
sanzhardanybayev profile image
Sanzhar Danybayev

I would go for Bridge Pattern

Collapse
 
sanzhardanybayev profile image
Sanzhar Danybayev

@doekenorg thanks for the article. I enjoyed reading this

Collapse
 
doekenorg profile image
Doeke Norg

You’re most welcome πŸ€— glad you enjoyed it!