Design Patterns In Action: The Adapter

The Adapter pattern is a design pattern that allows incompatible interfaces to work together by providing a wrapper that translates calls to one interface into calls to another.

Let’s say you’re building a cyberpunk RPG where the player character can hack into different systems. Each system has its own interface for interacting with it, but you want the player to use a consistent interface for hacking all of them.

interface Hackable {
    public function hack();
}

class OldSystem implements Hackable {
    public function hack() {
        echo "You hack into the old system... it's like trying to hotwire a hovercar from the 80s.\n";
    }
}

class NewSystem implements Hackable {
    public function hack() {
        echo "You hack into the new system... it's like hacking into a cyberpunk version of Fort Knox.\n";
    }
}

class LegacySystem {
    public function attemptHack() {
        echo "You try to hack into the legacy system... it's like trying to break into a cyberpunk version of a VCR.\n";
    }
}

class LegacySystemAdapter implements Hackable {
    private $legacySystem;

    public function __construct(LegacySystem $legacySystem) {
        $this->legacySystem = $legacySystem;
    }

    public function hack() {
        $this->legacySystem->attemptHack();
    }
}

$oldSystem = new OldSystem();
$newSystem = new NewSystem();
$legacySystem = new LegacySystem();
$legacySystemAdapter = new LegacySystemAdapter($legacySystem);

$systems = [$oldSystem, $newSystem, $legacySystemAdapter];

foreach ($systems as $system) {
    $system->hack();
}

The above defines an interface Hackable that has a single method hack(). The OldSystem and NewSystem classes both implement that interface. The LegacySystem class, however, does not implement the Hackable interface, but is wrapped by the LegacySystemAdapter class, which implements the Hackable interface and uses the attemptHack() method of the LegacySystem class to give it the same functionality.

This allows the player to use the same hack() method to interact with all the systems, even though the legacy system doesn’t use the same interface.

Let’s go for a more SOLID approach:

interface Hackable
{
    public function hack();
}

interface HackingTarget
{
    public function attemptHack();
}

class OldSystem implements Hackable
{
    public function hack() {
        echo "You hack into the old system... it's like trying to hotwire a hovercar from the 80s.\n";
    }
}

class NewSystem implements Hackable
{
    public function hack() {
        echo "You hack into the new system... it's like hacking into a cyberpunk version of Fort Knox.\n";
    }
}

class LegacySystem implements HackingTarget
{
    public function attemptHack() {
        echo "You try to hack into the legacy system... it's like trying to break into a cyberpunk version of a VCR.\n";
    }
}

class LegacySystemAdapter implements Hackable
{
    private $hackingTarget;

    public function __construct(HackingTarget $hackingTarget) {
        $this->hackingTarget = $hackingTarget;
    }

    public function hack() {
        $this->hackingTarget->attemptHack();
    }
}

$oldSystem = new OldSystem();
$newSystem = new NewSystem();
$legacySystem = new LegacySystem();
$legacySystemAdapter = new LegacySystemAdapter($legacySystem);

$systems = [$oldSystem, $newSystem, $legacySystemAdapter];

foreach ($systems as $system) {
    $system->hack();
}

This version follows SOLID principles by:

  • Using the Single Responsibility Principle (SRP) by separating the Hackable and HackingTarget interfaces and their respective implementations.
  • Using the Open-Closed Principle (OCP) by allowing new classes that implement the Hackable or HackingTarget interfaces to be added without modifying existing code.
  • Using the Liskov Substitution Principle (LSP) by ensuring that the LegacySystemAdapter class is a subtype of Hackable interface and can be used interchangeably with other implementations.
  • Using the Interface Segregation Principle (ISP) by avoiding adding unnecessary methods to the interfaces and keeping them focused on a single responsibility.
  • Using the Dependency Inversion Principle (DIP) by depending on interfaces rather than concrete implementations, which allows for greater flexibility in changing out the concrete implementations.
Related Posts