juststeveking.uk

Lead Software Engineer at Geomiq .

Published on
Reading time
5 minute read

Adapter Pattern

Of all of the design patterns you could use in your code, the adapter pattern is one of my all time favourites. It allows you to abstract the implementation to an adapter that implements an interface. so you can switch implementation simply by switching the adapter.

Let us walk through an example.

Firstly we need something that will manage our adapters, what is it we will be building? For this I will use a fictional approach as it makes explaining the process easier.

We will be building a social poster, a class that allows us to post to different social media channels. Firstly we will need the adapters interface, it is important to bear in mind what the social networks allow you to do. Having a postVideo method on the interface when only 2 of the 3 social networks supports this isn't a great idea. The main reason for using this pattern is to simplify integrating across different services under a common API.

Let us set the basic requirements of our code.

  • We want to be able to post an update.
  • We want to be able to fetch a post.
  • We want to be able to delete a post.
  • We want to be able to get a list of posts.

So knowing that, we can design our contract/interface using that information:

1interface NetworkAdapterContract
2{
3 public function post(string $message): void;
4 public function fetch(mixed $identifier): mixed;
5 public function delete(mixed $identifier): void;
6 public function get(): array;
7}

For now we will stick to the mixed type and array type to keep things super simple. Unless we do a lot of additional processing this is the minimum viable code we need, but we can make this stricter later if we need to by processing API responses and standardising them under things like NetworkCollection and NetworkResource interfaces/contracts. But that is going too deep into implementation instead of focusing on the pattern.

Our first social network we want to integrate with is twitter, probably my most used social network. So we will build the twitter adapter using the NetworkAdapterContract contract/interface.

1class TwitterAdapter implements NetworkAdapterContract
2{
3 public function __construct(
4 private TwitterSDK $sdk,
5 ) {}
6 
7 public function post(string $message): void
8 {
9 $this->sdk->tweet(Formatter::tweet($message));
10 }
11 
12 public function fetch(mixed $identifier): mixed
13 {
14 return $this->sdk->fetchTweet($identifier);
15 }
16 
17 public function delete(mixed $identifier): void
18 {
19 $this->ssk->deleteTweet($identifier);
20 }
21 
22 public function get(): array
23 {
24 return $this->sdk->getLatestTweets();
25 }
26}

As you can see the SDK I am pulling in is completely made up and isn't based off of an actual package you can use. However it illustrated the point that the SDKs API and the contract API is different, and you are using the adapter to proxy calls through to the SDK.

Let us take another example, this time LinkedIn - a social network I rarely use and I have only found useful when looking for work. But nonetheless we will use this as an example:

1class LinkedInAdapter implements NetworkAdapterContract
2{
3 public function __construct(
4 private LinkedInSDK $sdk,
5 ) {}
6 
7 public function post(string $message): void
8 {
9 $this->sdk->postUpdate(Formatter::linkedin($message));
10 }
11 
12 public function fetch(mixed $identifier): mixed
13 {
14 return $this->sdk->fetchUpdates($identifier);
15 }
16 
17 public function delete(mixed $identifier): void
18 {
19 $this->ssk->delete($identifier);
20 }
21 
22 public function get(): array
23 {
24 return $this->sdk->getUpdates();
25 }
26}

As you can see the Adapaters API is the same because of the NetworkAdapterContract interface/contract we implement, but the implemented SDK is very different. However we achieve the same result when working with these adapters.

So how can we implement this adapter pattern? Let us create a new class called Poster which will be in charge of using these adapters.

There are a couple of approaches we could use here, we could use some PHP magic methods such as __get or __call to magically forward the calls to the adapter, or we could add helper methods which reflect the action we are performing in the adapter itself.

Let us first build out the Poster class without this part, and refactor to show the specifics.

1class Poster
2{
3 private function __construct(
4 private string $adapter,
5 ) {}
6 
7 public static function use(
8 string $adapter,
9 ): static {
10 return new static(
11 adapter: $adapter,
12 );
13 }
14}

The surrounding class is super simple, all it needs to do is understand that is has an adapter and can access the adapters. Currently all we are doing is passing through the string name of the adapter, because ideally we would rely on DI to create the adapter and SDK etc.

To use this class and to create an adapter we can simply:

1$twitter = Poster::use(TwitterAdapter::class);

At this point we do not have much other than a class with a string property. Nothing exciting. Let us look at how this could work in Laravel...

Out next step would be to refactor the Poster class to allow us to have an adapter and a driver. The driver will be the class string, and the adapter will be the class itself.

1class Poster
2{
3 private function __construct(
4 private string $driver,
5 private NetworkAdapterContract $adapter,
6 ) {}
7 
8 public static function use(
9 string $driver,
10 ): static {
11 return new static(
12 driver: $driver,
13 adapter: app()->make($driver),
14 );
15 }
16}

This allows us to Poster::use(TwitterAdapter::class) and have the class-string respresentation stored for reference, and the adapter itself to be built by Laravels container allowing us to interact efficiently. Let us next make a service provider called PosterServiceProvider

1class PosterServiceProvider extends ServiceProvider
2{
3 public function register(): void
4 {
5 $this->app->singleton(
6 abstract: TwitterSDK::class,
7 concrete: fn() =>
8 new TwitterSDK(
9 token: config('services.twitter.api_token'),
10 ),
11 ,
12 );
13 }
14}

What we are doing here is telling our container that when we are trying to build a new instance of TwitterSDK we want to create it with the stored API token. Thanks to Laravels containers zero config requirement, we can instantiate a new TwitterAdapter without having to add this to out service provider.

So now our container can build our twitter adapter we can look at how we want to be able to forward calls through to the adapter.

1class Poster
2{
3 private function __construct(
4 private string $driver,
5 private NetworkAdapterContract $adapter,
6 ) {}
7 
8 public static function use(
9 string $driver,
10 ): static {
11 return new static(
12 driver: $driver,
13 adapter: app()->make($driver),
14 );
15 }
16 
17 public function post(string $message): void
18 {
19 $this->adapter->post($message);
20 }
21}

In the above example we are using the Poster class to post using a helper method, which is a more than acceptable way to do this. One popular library that uses this approach is flysystem by Frank de Jonge. This is most likely the best approach as it requires the least amount of magic, and isn't going to give you IDE warnings. However, if you have a very complicated implementation that you don't want to duplicate methods you can use the __call() method:

1class Poster
2{
3 private function __construct(
4 private string $driver,
5 private NetworkAdapterContract $adapter,
6 ) {}
7 
8 public static function use(
9 string $driver,
10 ): static {
11 return new static(
12 driver: $driver,
13 adapter: app()->make($driver),
14 );
15 }
16 
17 public function __call($method, $args)
18 {
19 if (! method_exists($this->adapter, $method)) {
20 throw new AdapterException(
21 message: "Method [$method] not found in [$this->driver]."
22 );
23 }
24 
25 return $this->adapter->$method(...$args);
26 }
27}

All we are doing here is checking that the method exists on the forwarding class beforehand, and throwing a specific exception that describes the error if it does not exist. We then just call the method passing in the arguments.

This is a very useful pattern to use, but it isn't something that should be used everywhere. It has its place, but it is very powerful when needed.