🐕

PHPの最高機能、配列を”完全に”捨てよう!!

2023/11/27に公開

はじめに

NE株でPHPを書いている谷口(@taniguhey)です。
社内ではいい感じの実装テクニックなどを布教したり、設計の相談にのってあーだこーだ言ったりしています。

この記事タイトルは、社内でも視聴率の高かったPHPerKaigi2023のuzullaさんのセッションのオマージュです。
PHPの最高機能、配列を捨てよう!! by uzulla | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp

オブジェクト指向とか難しいことはわからないよ〜という人でも理解しやすいお話で、とりあえずこういうことに気をつけようということが簡潔にまとまっているので、おすすめの発表です。


上記の発表では、連想配列は基本的に撲滅しlist<T>は許容してもよいといったような提案がされています。
この記事では、「配列に旅をさせない」がある程度身についた人に向けて、このlist<T>にも一工夫できるファーストクラスコレクションというデザインパターンを紹介します。

ファーストクラスコレクション

ファーストクラスコレクションとは、list<T>のような配列をクラスでラップしたものだと考えてOKです。

例えば、ECで販売する商品ごとの分類についてを考えてみます。「おすすめ」「セール中」「要冷凍」「要冷蔵」などがあるとします。
まずはこれらをenum(使い慣れていない人はクラス定数だと読み替えてOK)で表現しましょう。

enum Category
{
    case RECOMMENDED;
    case DISCOUNTED;
    case KEEP_FROZEN;
    case KEEP_REFRIGERATED;
}

商品を表すクラスをProductと呼ぶとして、
ある商品にとっては複数の分類を持ちうるので、配列でこれらを保持したいとするとこのようなコードになると思います。

class Product
{
    public function __construct(
        private string $name,
        private array $categories,
    ){
    }
}

$product = new Product('手作りアイスクリーム', [Category::RECOMMENDED, Category::KEEP_FROZEN]);

このときの[Category::DISCOUNTED, Category::KEEP_FROZEN]はarrayですが、これを任意のCategoryが集まったクラスとしてラップしてしまおうというのがファーストクラスコレクションで、

この記事の主役
class CategoryCollection
{
    private array $categories;

    /**
     * @param Category[] $categories
     */
    public function __construct(array $categories)
    {
        $this->categories = $categories;
    }
}

このようにプロパティにarrayを持つクラスのことです。

ここを置き換える
class Product
{
    public function __construct(
        private string $name,
-       private array $categories,
+       private CategoryCollection $categories,
    ){
    }
}

メソッドを生やしてみる

では、これがあると何ができるようになるか見てみましょう。

クラスになったことで、利用側から実際の配列の値には直接アクセスできないためメソッドを通すことになります。
例えば、商品を配送することを考え、要冷凍の商品かどうかを確認しなければならないとします。その判定を行うメソッドをCategoryCollectionに実装するとこんな感じです。

冷凍商品か
public function requiredFrozen(): bool
{
    return in_array(Category::KEEP_FROZEN, $this->categories, true);
}

プリミティブな配列list<T>を使っていた頃と明確に違うのは、このような「何をもって要冷凍か」のような判断処理をlist<T>自身に集約させることができ、配送や商品に関わるクラスから独立させることができます。

同様のことを別の角度から言い換えると、配列に対して行える操作を限定することができるということでもあります。
ファーストクラスコレクションにせずプリミティブな配列で扱った場合、その中の1要素を取り出したり変更したり要素を追加すること自体に制限をかけることが難しいです。
クラスでラップすることによって、配列全体の制御(例えば重複を許すかや並び順など)や、各要素をイミュータブルに保つことなどもできるようになりました。

集合としての制御
// 例えば
// 追加はできるが
public function add(): self

// 削除はできない
// public function remove(): self

// など

「静的解析が効く」や「エディタが補完してくれる」という文脈でも、行える操作をメソッドとして表現することは十分に効果があります。

実際に使うにあたって

list<T>に対してクラスが1つ新たに生まれるので、ファーストクラスコレクションにすることのメリットを感じられない場合は、プリミティブな配列のまま扱うことをお勧めします。単純にコード量・クラス数が増えるので要はバランスです。

実際に導入する場合にできる工夫をもう少し書いておきます。

コンストラクタ

先程の例では、コンストラクタの引数の型自体はarrayなので、中身に複数の型が混ざっていても代入できてしまうため、実際に参照されるまでエラーに気付けない可能性があります。

よりフェイルセーフにする場合、以下のように書くことで他の型の値を渡そうとした際にエラーを吐くようになります。

class CategoryCollection
{
    /**
     * @param Category[] $categories
     */
-   public function __construct(array $categories)
+   public function __construct(Category ...$categories)
    {
        $this->categories = $categories;
    }
}

IteratorAggregate

コレクションの中身を全て読み取って何かしたいなどの使われ方が多い場合、特にforeachに渡す機会が多い場合は、IteratorAggregateを実装すると良いでしょう。

PHP: IteratorAggregate - Manual

public function getIterator(): Traversable
{
    return new ArrayIterator($this->categories);
}
getterが不要
$categoryCollection = new CategoryCollection(Category::RECOMMENDED, Category::KEEP_FROZEN);
foreach ($categoryCollection as $category) {
    // 取り出した値をViewに渡すなど
}

最後に

ファーストクラスコレクションを積極的に導入できれば、プリミティブなarray型を意識して使うことはなくなるため、"完全に"捨てることができますね。
というのは極論ですが、PHPの配列は良くも悪くも便利なのでうまくバランスの取れたところを探して良いアプリケーションを開発していきましょう。

NE株式会社の開発ブログ

Discussion