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' => '...']
}
}
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;
}
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();
}
}
}
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(),
);
}
}
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`]
}
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());
}
}
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
andWysiwygEditor
. - ποΈ 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 aFtpFileSystem
.
It is at this point, I think one of the things that makes this pattern hard to grasp is this:
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.
}
}
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());
}
}
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.
}
}
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')
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)
Let's say I want to upload to Google Drive but sometimes upload to Dropbox. Which pattern should I use?
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.
I would go for Bridge Pattern
@doekenorg thanks for the article. I enjoyed reading this
Youβre most welcome π€ glad you enjoyed it!