First Class Collection

この記事はDeNA Advent Calendar 2016 8日目の記事です。

はじめまして、オープンプラットフォーム事業本部のpospomeです。
普段は GAE/GO の環境でサーバサイドエンジニアとして働いています。
(´・ω・`)

この記事では「First Class Collection」という実装パターンを紹介します。

First Class Collection とは?

First Class Collection は「ThoughtWorksアンソロジー」という書籍で紹介されている「Array, Map をクラスでラップする」という実装パターンです。

以下のようなユーザーのゲームスコアを扱うArrayがあったとします。


$scores = Array(
    Array(
        'user_id' => 1,
        'team_id' => 1,
        'event_id' => 1,
        'score' => 100,
    ),
    Array(
        'user_id' => 2,
        'team_id' => 2,
        'event_id' => 2,
        'score' => 100,
    ),
);

このArrayをクラスでラップします。


class GameScoreList {
    private $scores;
    function __construct($scores) {
        $this->scores = $scores;
    }
}

これが First Class Collection です。
Array を直接触らず GameScoreList を介してArrayを触ります。

ラップすることによって以下の利点が生まれます。
・ArrayとArrayに対する処理を一緒に管理できる
・Arrayをイミュータブルにできる
・Arrayに名前を付けることができる

1つ1つ説明していきます。

ArrayとArrayに対する処理を一緒に管理できる

Arrayのようなデータの集合体は加工処理やフィルタリング処理をすることが多いと思います。
例えば、「特定の userId の score の合計を取得する」「合計 score が一番高いチームに所属する userId を全て取得する」などです。

このような場合、Arrayをそのまま利用してしまうと、Arrayと加工ロジックが分離してしまいます。
似たようなロジックをあちこちに書いてしまったり、
ちょっとしたロジックであれば、Controllerなどにべた書きしてしまうかもしれません。

First Class Collection を利用すると、Arrayに対する処理を一緒に管理できます。
これによってロジックが重複することもないですし、べた書きする必要もありません。


class GameScoreList {
    private $scores;

    function __construct($scores) {
        $this->scores = $scores;
    }

    //特定の user_id の score の合計を取得する
    function getTotalScoreByUserId($userId) {
        //ロジック省略
    }
}

クラスなので、interface を利用して並び替え処理を抽象化することもできます。
以下はイベントの種類によってスコアの並び替え処理が変わるケースを想定しています。


interface ISort {
    public Sort()
}

class xxxEventScoreList implements ISort{
}

class yyyBossEventScoreList implements ISort{
}

ゲームスコアはDBなどで永続化されている事が多いので、コードを書かずSQLで完結することもありますが、
場合によってはコードでフィルタリングすることもあると思います。
そういった場合にとても便利です。

Arrayをイミュータブルにできる

完全コンストラクタにすることで、Arrayをイミュータブルにすることができます。

要素の追加、削除、更新が必要になった場合は以下のように関数を用意してあげることで、
Arrayに対して副作用を与える操作を制限することができます。
追加対象のArrayに対してバリデーションをかけることも可能です。


class GameScoreList {
    //追加はできるが、削除、更新はできない
    function add($score) {
        $this->scores[] = $score;
    }
}

Arrayに名前を付けることができる

単なる Array では「それがArrayであること」しか表現できないので、具体的に「何のArrayなのか」は変数名でしか表現できません。


//単なるArray
$bigBossEventScores = Array();

First Class Collection ではクラス名で表現することができます。
普段チーム内で利用する言葉で命名すれば「それが何なのか」がすぐに分かります。
Array であることを意識する必要もありません。
チーム内で「ビッグボスイベントのスコア一覧」という言葉を利用するのであれば、
以下の BigBossEventScoreList のように言葉をそのまま表現することができます。


$list = new BigBossEventScoreList($arr);

以下のように BigBossEventScoreクラスのArrayである場合、
「ビッグボスイベントのスコアの集合」であることは分かるのですが、
「ビッグボスイベントのスコア一覧」という言葉を表現することはできません。


$bigBossEventScores = Array(new BigBossEventScore(), BigBossEventScore());

引数や戻り値にもクラス名が利用されるので、
単なる Array よりも「それが何なのか」を明確に表現することができます。
ユビキタス言語を利用するDDDでは必須の実装パターンですね。

以上が First Class Collection の利点です。

その他

クラスにラップすることで Array を foreach のようなループで回すことはできなくなりますが、
ループで回す目的はフィルタリングやデータ加工のはずです。
その処理をクラスに持たせることができるので、
Array をループで回せなくて困るというケースは少ないと思います。
ループは可能な限りクラス内に実装しましょう。

Arrayを直接触りたい場合は getter を用意してあげましょう。
ただし、getter で取得した Array に対する処理を外に置かないように気をつけてください。


class GameScoreList {
    private $scores;

    function get() {
        return $this->scores;
    }
}

終わりに

今回は First Class Collection を紹介しました。

Array, Map は int, string よりも保持している情報量が多いので、
それらに紐づく処理が必要になりがちです。
また、言葉で表現できるデータ集合体であることが多いので、
クラスにラップしてあげた方が分かりやすくなるケースもあると思います。

全ての Array, Map をラップするかは迷うところですね。

人によって好みが別れるところだと思うので、
「データに対する処理が必要になったらラップする」
「レイヤをまたぐ場合はラップする」
「ユビキタス言語で自然に表現できるもののみラップする」
など、色々と試行錯誤してみるのがいいかもしれませんね。

ちなみに、golangには primitive type というプリミティブ型に実装を持たせる機能があるので、
以下のようにサラッと実装することができます。
*Array に直接触れてしまいますが・・・


type UserScoreList []int

func (u UserScoreList) GetTotalScore() {
    //ロジック省略
}

長々と説明しましたが、
First Class Collection は簡単に幅広く活用できる実装パターンだと思うので、
知らなかった方は是非試してみて下さい。

明日は yuichi1004 さんです。(`・ω・´)ゞ

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る