🦋

条件分岐を減らすためのポリモーフィズム

2025/02/22に公開

目次

  1. 経緯
  2. ポリモーフィズムとは
  3. ECサイトの料金の端数処理の例
  4. まとめ

経緯

クリーンコードクックブック』と言う本を読み、ポリモーフィズムを利用したアプリケーションの複雑さの元になる条件分岐を減らす設計について学んだため、今回備忘録としてまとめたいと思います。

ポリモーフィズムとは

ポリモーフィズムとはカプセル化、継承と並びオブジェクト指向の特徴の1つであり、型が異なっていても同一のインターフェースの実装などによる同じ振る舞いを持つ場合は同じものとして扱うことができる性質のことを指します。
これにより、メソッドが同じであっても異なる実装が可能になります。

サンプルコード

<?php declare(strict_types=1);

interface Pet
{
  public function cry(): string;
}

final class Dog implements Pet
{
  public function cry(): string
  {
    return 'ワン';
  }
}

final class Cat implements Pet
{
  public function cry(): string
  {
    return 'ニャン';
  }
}

function call(Pet $pet): void
{
  echo $pet->cry();
}

call(new Dog());
// ワン

call(new Cat());
// ニャン

このサンプルコードでは、呼びかける対象が犬クラスか猫クラスかを問わず鳴くと言う振る舞いがペットのインターフェースで共通しているため、呼びかけることができることになります。
また、ペットの種類によって異なる鳴き声を出すことが可能になります

ECサイトの料金の端数処理の例

このECサイトではお店ごとに商品を選び注文を行います。
合計金額を計算する際に発生する端数の処理については、お店が四捨五入、切り上げ、切り捨てを選ぶことができ、お店が選んだ端数処理を行います。

if文を用いた実装

<?php declare(strict_types=1);

enum RoundingType
{
    case HALF_UP; // 四捨五入
    case UP; // 切り上げ
    case DOWN; // 切り捨て

    public function round(float $number): int
    {
        // ifではなくmatchですが、条件分岐が発生している
        return match ($this) {
            self::HALF_UP => (int) round($number),
            self::UP => (int) ceil($number),
            self::DOWN => (int) floor($number),
        };
    }
}

final class Item
{
    public function __construct(
        public readonly string $itemName,
        public readonly int $price,
    ) {}
}

final class Order
{
    public function __construct(
        public readonly array $items,
    ) {}

    public function totalPrice(RoundingType $roundingType): int
    {
        $totalPrice = 0;

        foreach($this->items as $item) {
            $totalPrice += $item->price * 1.1;
        }

        return $roundingType->round($totalPrice);
    }
}

$order = new Order([
    new Item('まぐろ', 123),
    new Item('中トロ', 155),
    new Item('サーモン', 108),
    new Item('ウニ', 221),
]);
// 123*1.1 + 155*1.1 + 108*1.1 + 2211.1 = 667.7

echo $order->totalPrice(RoundingType::HALF_UP);
// 668

echo $order->totalPrice(RoundingType::UP);
// 668

echo $order->totalPrice(RoundingType::DOWN);
// 667

ポリモーフィズムを用いた実装

<?php declare(strict_types=1);

// 端数処理の振る舞いを持つインターフェース
interface Roundable
{
    public function round(float $number): int;
}

// 四捨五入の端数処理の実装を持つクラス
final class RoundingHalfUp implements Roundable
{
    public function round(float $number): int
    {
        return (int) round($number);
    }
}

// 切り上げの端数処理の実装を持つクラス
final class RoundingUp implements Roundable
{
    public function round(float $number): int
    {
        return (int) ceil($number);
    }
}

// 切り捨ての端数処理の実装を持つクラス
final class RoundingDown implements Roundable
{
    public function round(float $number): int
    {
        return (int) floor($number);
    }
}

final class Item
{
    public function __construct(
        public readonly string $itemName,
        public readonly int $price,
    ) {}
}

final class Order
{
    public function __construct(
        public readonly array $items,
    ) {}

    public function totalPrice(Roundable $roundable): int
    {
        $totalPrice = 0;

        foreach($this->items as $item) {
            $totalPrice += $item->price * 1.1;
        }

        return $roundable->round($totalPrice);
    }
}

$order = new Order([
    new Item('まぐろ', 123),
    new Item('中トロ', 155),
    new Item('サーモン', 108),
    new Item('ウニ', 221),
]);
// 123*1.1 + 155*1.1 + 108*1.1 + 2211.1 = 667.7

// Roundableインターフェースを実装するクラスのオブジェクトを引数に渡す
echo $order->totalPrice(new RoundingHalfUp());
// 668

echo $order->totalPrice(new RoundingUp());
// 668

echo $order->totalPrice(new RoundingDown());
// 667

このコードではRoundableインターフェースを実装するクラスのオブジェクトは端数処理を行うround()という共通するメソッド(振る舞い)を持ちますが、中身の実装はクラス毎に異なります。
そのため、totalPrice()メソッドに渡すオブジェクトの種類毎に異なる端数処理を行うことが出来ます。
最初のコードでは条件分岐がありましたが、このコードでは条件分岐が存在しません。

実行ではなく生成で条件分岐

ポリモーフィズムを用いることで、コードから条件分岐が完全に消える訳ではありません。
分岐する場所が処理を実行する場所ではなく、あるデータからオブジェクトを生成する場所で分岐することになります。

<?php declare(strict_types=1);

enum RoundingType
{
    case HALF_UP; // 四捨五入
    case UP; // 切り上げ
    case DOWN; // 切り捨て
}

final class RoundableFactory
{
    public static function create(RoundingType $roundingType): Roundable
    {
        return match ($roundingType) {
            RoundingType::HALF_UP => new RoundingHalfUp(),
            RoundingType::UP => new RoundingUp(),
            RoundingType::DOWN => new RoundingDown(),
        };
    }
}

$order = new Order([
    new Item('まぐろ', 123),
    new Item('中トロ', 155),
    new Item('サーモン', 108),
    new Item('ウニ', 221),
]);

$roundable = RoundableFactory::create(RoundingType::HALF_UP);

echo $order->totalPrice($roundable);

まとめ

今回はポリモーフィズムによる条件分岐を減らす設計テクニックについてまとめました。
サンプルコードのように、ポリモーフィズムを用いることで条件分岐を1つの階層として振る舞いをインターフェースとして抽象化し、実装の詳細を具象クラスとすることで、条件分岐を減らすことが出来ます。

また、if文での条件分岐では、変更や拡張が発生するたびにメソッドの中身のif文を修正することになります。
しかし、それをオブジェクトの階層として表現することで、変更や拡張で影響する範囲を狭め、変更や拡張に強くすることができます。

https://www.oreilly.co.jp/books/9784814400973/

Discussion