PHPの最高機能、配列を”完全に”捨てよう!!
はじめに
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);
}
$categoryCollection = new CategoryCollection(Category::RECOMMENDED, Category::KEEP_FROZEN);
foreach ($categoryCollection as $category) {
// 取り出した値をViewに渡すなど
}
最後に
ファーストクラスコレクションを積極的に導入できれば、プリミティブなarray型を意識して使うことはなくなるため、"完全に"捨てることができますね。
というのは極論ですが、PHPの配列は良くも悪くも便利なのでうまくバランスの取れたところを探して良いアプリケーションを開発していきましょう。
NE株式会社のエンジニアを中心に更新していくPublicationです。 NEでは、「コマースに熱狂を。」をパーパスに掲げ、ECやその周辺領域の事業に取り組んでいます。 Homepage: ne-inc.jp/
Discussion