Adapterパターン

別システムの機能を引き継ごう!

現在、自社商品を発注する仕組みはありますが、他社商品から提供されたクラスも商品の発注の仕組みに適用させようと思いました。
ここでは、他社商品から提供されたクラスは修正が出来ないものとします。

さっそく書いてみましょう。

CurrentProductが自社商品を表すクラスです。
OtherProductが他社の商品を表すクラスです。
OrderProcessorが発注を行う仕組みです。

class CurrentProduct
{
  private string $name;
  private string $description;
  private int $price;
  public function setName(string $name)
  {
    $this->name = $name;
  }
  public function setDescription(string $description)
  {
    $this->description = $description;
  }
  public function setPrice(int $price)
  {
    $this->price = $price;
  }
  public function getName(): string
  {
    return $this->name;
  }
  public function getDescription(): string
  {
    return $this->description;
  }
  public function getPrice(): int
  {
    return $this->price;
  }
}

class OtherProduct
{
  private string $productName;
  private string $productDescription;
  private int $productPrice;
  public function setProductName(string $name)
  {
    $this->productName = $name;
  }
  public function setProductDescription(string $description)
  {
    $this->productDescription = $description;
  }
  public function setProductPrice(int $price)
  {
    $this->productPrice = $price;
  }
  public function getProductName(): string
  {
    return $this->productName;
  }
  public function getProductDescription(): string
  {
    return $this->productDescription;
  }
  public function getProductPrice(): int
  {
    return $this->productPrice;
  }
}

class OrderProcessor
{
  private array $products;

  public function OrderProcessor()
  {
    $this->products = [];
  }

  public function addProduct($product)
  {
    $this->products[] = $product;
  }

  public function order()
  {
    foreach ($this->products as $product) {
      echo "下記商品を発注します\n";
      echo "商品名: {$product->getName()}";
      echo "\n";
      echo "説 明: {$product->getDescription()}";
      echo "\n";
      echo "価 格: {$product->getPrice()}";
      echo "\n";
    }
  }
}

$processor = new OrderProcessor();

$currentProduct = new CurrentProduct();
$currentProduct->setName('はやいCPU');
$currentProduct->setDescription('ゲームにも最適');
$currentProduct->setPrice(50000);

$processor->addProduct($currentProduct);

$processor->order();

当然ですが、ここの状態でOtherProductをOrderProcessorで処理をしようとしてもエラーが出ます。
(タイプヒンティングを行っていた場合も、当然エラーが出ます)

$otherProduct = new OtherProduct();
$otherProduct->setProductName('SSD 512GB');
$otherProduct->setProductDescription('普通のSSDです');
$otherProduct->setProductPrice(30000);

$processor->addProduct($otherProduct);

$processor->order()

Adapterパターンを使わないで対応するとなると、OrderProcessor内のorderメソッド内で、
instanceofで処理を切り替えられるようにするでしょうか。

class OrderProcessor
{
  private array $products;

  public function ProductInfoViewer()
  {
    $this->products = [];
  }

  public function addProduct($product)
  {
    $this->products[] = $product;
  }

  public function order()
  {
    foreach ($this->products as $product) {

      $name = '';
      $description = '';
      $price = 0;

      if ($product instanceof OtherProduct) {
        $name = $product->getProductName();
        $description = $product->getProductDescription();
        $price = $product->getProductPrice();
      } else {
        $name = $product->getName();
        $description = $product->getDescription();
        $price = $product->getPrice();
      }

      echo "商品名: {$name}";
      echo "\n";
      echo "説 明: {$description}";
      echo "\n";
      echo "価 格: {$price}";
      echo "\n";
    }
  }
}

$processor = new OrderProcessor();

$currentProduct = new CurrentProduct();
$currentProduct->setName('はやいCPU');
$currentProduct->setDescription('ゲームにも最適');
$currentProduct->setPrice(50000);

$processor->addProduct($currentProduct);

$otherProduct = new OtherProduct();
$otherProduct->setProductName('SSD 512GB');
$otherProduct->setProductDescription('普通のSSDです');
$otherProduct->setProductPrice(30000);

$processor->addProduct($otherProduct);

$processor->order();

では、Adapterパターンを利用した場合、どのような実装になるでしょうか?

Adapterパターンとは

既存のクラスの処理に対して、インタフェースを加えることで別のシステムに適用できるようにするパターンです。
継承を利用する方法と、包含を利用する方法があります。

継承を利用した場合

interface Product
{
  public function getName(): string;
  public function getDescription(): string;
  public function getPrice(): int;
}

class CurrentProduct implements Product
{
  private string $name;
  private string $description;
  private int $price;
  public function setName(string $name)
  {
    $this->name = $name;
  }
  public function setDescription(string $description)
  {
    $this->description = $description;
  }
  public function setPrice(int $price)
  {
    $this->price = $price;
  }
  public function getName(): string
  {
    return $this->name;
  }
  public function getDescription(): string
  {
    return $this->description;
  }
  public function getPrice(): int
  {
    return $this->price;
  }
}

class ProductAdapter extends OtherProduct implements Product
{
  public function getName(): string
  {
    return $this->getProductName();
  }
  public function getDescription(): string
  {
    return $this->getProductDescription();
  }
  public function getPrice(): int
  {
    return $this->getProductPrice();
  }
}

class OrderProcessor
{
  private array $products;

  public function ProductInfoViewer()
  {
    $this->products = [];
  }

  public function addProduct(Product $product)
  {
    $this->products[] = $product;
  }

  public function order()
  {
    foreach ($this->products as $product) {
      echo "商品名: {$product->getName()}";
      echo "\n";
      echo "説 明: {$product->getDescription()}";
      echo "\n";
      echo "価 格: {$product->getPrice()}";
      echo "\n";
    }
  }
}

$processor = new OrderProcessor();

$currentProduct = new CurrentProduct();
$currentProduct->setName('はやいCPU');
$currentProduct->setDescription('ゲームにも最適');
$currentProduct->setPrice(50000);

$processor->addProduct($currentProduct);

$otherProduct = new ProductAdapter();
$otherProduct->setProductName('SSD 512GB');
$otherProduct->setProductDescription('普通のSSDです');
$otherProduct->setProductPrice(30000);

$processor->addProduct($otherProduct);

$processor->order();


包含を利用した場合

class CurrentProduct implements Product
{
  private string $name;
  private string $description;
  private int $price;
  public function setName(string $name)
  {
    $this->name = $name;
  }
  public function setDescription(string $description)
  {
    $this->description = $description;
  }
  public function setPrice(int $price)
  {
    $this->price = $price;
  }
  public function getName(): string
  {
    return $this->name;
  }
  public function getDescription(): string
  {
    return $this->description;
  }
  public function getPrice(): int
  {
    return $this->price;
  }
}

interface Product
{
  public function getName(): string;
  public function getDescription(): string;
  public function getPrice(): int;
}

class OrderProcessor
{
  private array $products;

  public function ProductInfoViewer()
  {
    $this->products = [];
  }

  public function addProduct(Product $product)
  {
    $this->products[] = $product;
  }

  public function order()
  {
    foreach ($this->products as $product) {
      echo "商品名: {$product->getName()}";
      echo "\n";
      echo "説 明: {$product->getDescription()}";
      echo "\n";
      echo "価 格: {$product->getPrice()}";
      echo "\n";
    }
  }
}

class ProductAdapter implements Product
{
  private OtherProduct $otherProduct;
  public function ProductAdapter(OtherProduct $otherProduct)
  {
    $this->otherProduct = $otherProduct;
  }
  public function getName(): string
  {
    return $this->otherProduct->getProductName();
  }
  public function getDescription(): string
  {
    return $this->otherProduct->getProductDescription();
  }
  public function getPrice(): int
  {
    return $this->otherProduct->getProductPrice();
  }
}

$processor = new OrderProcessor();

$currentProduct = new CurrentProduct();
$currentProduct->setName('はやいCPU');
$currentProduct->setDescription('ゲームにも最適');
$currentProduct->setPrice(50000);

$processor->addProduct($currentProduct);

$otherProduct = new OtherProduct();
$otherProduct->setProductName('SSD 512GB');
$otherProduct->setProductDescription('普通のSSDです');
$otherProduct->setProductPrice(30000);

$productAdapter = new ProductAdapter($otherProduct);
$processor->addProduct($productAdapter);

$processor->order();

Adapterパターンの最大の効果は、今回の例で言うところのorderメソッド内の処理をほとんど変える必要がないということでしょう。
また、OtherProduct以外にもorderメソッドで発注したいクラスが出てきたときもAdapterを作成しaddProductするだけで利用できます。

継承と包含、どちらが良いかという話になると思いますが、正直ケースバイケースと言えるでしょう。
PHPは単一継承のみ可能ですので、継承を利用したAdapterパターンの場合、他のクラスを継承することはできなくなります。
一方包含を利用したAdapterパターンの場合、コード量が若干増えます。

現状のプログラムに対する変更を最小に抑えつつ、他のシステムのコードを利用したい場合には、Adapter検討してみると良いでしょう。