Decoding The Adapter Pattern
Introduction
In the ever changing world of software development, the Adapter Pattern stands out as a beacon of flexibility and adaptability. I personally often find myself reaching out to this pattern, especially in an era dominated by open-source contributions. Talented engineers worldwide solve complex problems and generously make their solutions open-source, enriching the developer community. However, its paramount to make sure that we don't tightly couple our codebase to a certain implementation as we look at couple of real world scenarios in this article.
Analogy: The Tale of the Universal Remote
Picture a room full of gadgets, each with its unique remote control. It's a hassle managing them all. Then, you come across a universal remote that connects with every device. It doesn't change how each gadget works but offers a standard way to interact.
In software development world, the Adapter Pattern acts as this 'universal remote', connecting different interfaces without changing their core functions.
Real-world Example: Switching Media Services
At Housemates, we rely on ImageKit for media services, leveraging its capabilities to make media readily available through a CDN. But what if we decide to switch to another service like Cloudinary? Manually replacing every instance of ImageKit in our codebase would be tedious and a clear violation of the Open-Closed Principle.
To address this, we can use the Adapter Pattern. First, we define a contract:
interface MediaInterface
{
public function getUrl(string $path);
public function upload(string $fileUrl, string $filename);
}
Next we implement the interface for our current service, i.e. ImageKit:
class ImageKitService implements MediaInterface
{
public function getUrl(string $path)
{
// Implement getUrl() method.
}
public function upload(string $fileUrl, string $filename)
{
// Implement upload() method.
}
}
Then, we create an internal class to interact with our media service:
class MediaService
{
protected MediaInterface $media;
public function __construct(MediaInterface $mediaInterface) {
$this->media = $mediaInterface;
}
public function getUrl(string $path) {
return $this->media->getUrl($path);
}
public function upload(string $fileUrl, string $filename) {
return $this->media->upload($fileUrl, $filename);
}
}
Refactoring Geocoding service
Let's explore another common scenario that many developers encounter: geocoding services. Geocoding, the process of converting addresses into geographical coordinates, is a staple in many applications. But what happens if the library we using becomes obsolete and we need to swap it out with another library. The Adapter Pattern can assist.
Lets start refactoring by defining a common interface:
interface GeocoderInterface
{
public function fetchCoordinates(string $address);
}
Next we will make sure that our existing class or adpater conforms to the above contract
class LegacyGeoAdapter implements GeocoderInterface
{
public function fetchCoordinates(string $address)
{
// logic to return the coordinates
}
}
We can now use LegacyGeoAdapter in our codebase as below:
// keeping it simple for the sake of the example
class Address
{
public function __construct(
protected GeocoderInterface $geocoder,
protected string $address
) {}
public function getCoordinates() {
return $this->geocoder->fetchCoordinates($this->address);
}
}
$address = new Address(
new LegacyGeoAdapter,
'120 higway street, Manchester'
);
$address->geoCoordinates();
Lets define our new Adpater which will conform to GeocodeInterface
class NewGeoAdapter implements GeocoderInterface
{
public function fetchCoordinates(string $address)
{
// logic to return coordinates
}
}
Finally, we swap out the LegacyGeoAdapter with a NewGeoAdapter. Forgive me for the lack of better names.
$address = new Address(
new NewGeoAdapter,
'120 higway street, Manchester'
);
$address->geoCoordinates();
Benefits
Flexibility:
Easily switch between different services without major code refactoring.
Consistency:
Maintain a consistent method of interaction, regardless of the underlying service.
Open-Closed Principle:
Our code remains open for extension but closed for modification.
Dependency Inversion:
By injecting our classes during run time, we have successfully achieved the Dependency Inversion, one of the key SOLID design principles.
Comparison with Other Patterns
While the Adapter Pattern
might seem similar to the Factory
or Strategy
patterns, it's distinct in its role. The Factory
Pattern deals with creating objects, and the Strategy
Pattern defines a family of algorithms. The Adapter Pattern
, on the other hand, bridges the gap between two incompatible interfaces, making them work together seamlessly.
Conclusion
The Adapter Pattern isn't just about coding; it's about preparing our applications for the future. It ensures that our systems can adapt to new challenges or innovative open-source solutions. As developers, building enduring systems is our goal. The Adapter Pattern embodies this, emphasizing the importance of adaptability in an ever-evolving world.