ドメイン知識を表すモデルでアジャイルな開発を支える

この記事は Retty Advent Calendar 2019 14日目の記事です。

qiita.com

アジャイル開発を支えるコード

こんにちは、バックエンドエンジニアの池田です。
最近は積極的にVue.jsのコードを書いています。バックエンドエンジニアとは概念。

そんな話はさておき、Rettyではアジャイル型開発を採用しています。
アジャイルな開発では短いサイクルで機能をリリースすることで変化に柔軟に対応し、プロダクト価値の最大化を目指していきます。
そのサイクルをどれだけ円滑に回せるかは、コードの質に大きく左右されるといっても過言ではありません。

そこで、開発速度を維持してアジャイルな開発を支えるためにドメイン知識を表すモデルがどのように役立つのかを書いてみたいと思います。

仕様をどうやって表現するか

システムを作る際には何かしらの仕様が決まっています。
仕様と一致していないものはバグとして扱われ、修正の対象になります。
バグはシステムを利用してくれている方々にも迷惑をかけることになるため、出来る限り避けたいものです。

ですので、この仕様をどうやってコード上で表現するかというのは重要なポイントです。
もちろん仕様はコード以外でも表現できますが、コード上でも適切に表現されていることが望ましいでしょう。

例として以下の仕様のカードゲームを作るとします。

  • カードは1~13のいずれかの数字を持つ
  • 山札は1~13の数字を各1枚ずつ計13枚
  • カードを先頭から1枚引く

この仕様をモデルを見るだけでわかるようになっている、というのがドメイン知識を表すモデルとして目指している状態です。

ドメイン知識を表さないモデルの問題点

まずはドメイン知識を表さないモデルをもつコードを書いてみます。
(※以降サンプルコードはC#で書きますが、なるべく言語依存の構文は避けています)

public class Card
{
    private int number;

    public Card() { }

    // コンストラクタで何もせずgetter, setterを用意する極端な例
    public int GetNumber() { return number; }
    public void SetNumber(int number) { this.number = number; }
}
public class PlayingCardService
{
    public class PlayingCardService() { }

    public Card Play()
    {
        // 山札を作る
        List<Card> deck = new List<Card>();
        for (int i = 1; i <= 13; i++)
        {
            Card card = new Card();
            card.SetNumber(i);
            deck.Add(card);
        }

        // シャッフル(JavaのCollections.shuffleやPHPのshuffle等とやりたい事は同じ)
        deck= deck.OrderBy(x => Guid.NewGuid()).ToList();

        Card drawed = deck[0];
        return drawed;
    }
}

見るからにひどいコードですが要求された仕様を満たしていますのでOKです。リリースしましょう!

モデルがドメイン知識を表さないとどうなるか

このコードはPlayingCardServiceクラスのPlayメソッドの中でほとんどの仕様を表現しています。
Cardクラスはsetterを公開してどんな数字でも受け付けているため、いまいち役割がぱっとしません。
一見するとPlayメソッドに仕様がまとまっていてわかりやすいよう思えますが、開発が進むにつれて問題を起こすようになります。

仕様を破壊するコードが入り込む

このコードの問題は、前提になっている仕様をコードから読み解くことが難しいという点です。
例えば for (int i = 1; i <= 13; i++) の部分を見たとき、以下のどちらの解釈も可能です。

  • カードの数字は1~13までしか使ってはいけない
  • ここで1~13までのカードを作っているだけで別の数字を使ってもよい

そのため、仕様を把握しきれずに以下のような仕様を破壊するコードを後から書かれてしまう可能性があります。

public Card Play()
{
    List<Card> deck = new List<Card>();
    for (int i = 1; i <= 13; i++)
    {
        Card card = new Card();
        card.SetNumber(i);
        deck.Add(card);
    }

    Card extraCard = new Card();
    // 数字は13までという仕様なのに14のカードが追加されている!!!
    extraCard.SetNumber(14);
    deck.Add(extraCard);

    deck = deck.OrderBy(x => Guid.NewGuid()).ToList();

    // 先頭からカードを引くはずなのに途中から引いている!!!
    Card drawed = deck[3];
    // カードの数字を勝手に変更!!!
    drawed.SetNumber(7);
    return drawed;
}

チームでの開発であればコードレビューで止めることは出来ますが、見逃してしまったらお終いです。
また、コードレビューやテストなど開発の後半で問題に気が付くと、前半で問題に気が付くよりも手戻りのコストが余計にかかってしまいます。

ドメイン知識を表すモデルのメリット

ドメイン知識を表さないモデルがあちこちに存在する状態で開発を続けると、徐々に開発スピードが落ちサービスの成長速度が低下してしまいます。

そこで以下のようにモデルがドメイン知識を表すように書きかえてみます。(細かいバリデーションは省略)

public class Card
{
    public const int MinValue = 1;
    public const int MaxValue = 13;

    private int number;

    public Card(int number)
    {
        // カードは1~13のいずれかの数字という仕様
        if (number < MinValue || number > MaxValue)
        {
            string message = "number must be between " + MinValue.ToString() + " and " + MaxValue.ToString();
            throw new ArgumentException(message);
        }
        this.number = number;
    }

    // getterのみを持たせる
    public int GetNumber() { return number; }
}
public class Deck
{
    private int topIndex;
    private List<Card> cards;

    public Deck()
    {
        topIndex = 0;

        cards = new List<Card>();
        // 数字の仕様の範囲内でカードを作成する
        for (int num = Card.MinValue; num <= Card.MaxValue; num++)
        {
            Card card = new Card(num);
            cards.Add(card);
        }
        cards = cards.OrderBy(x => Guid.NewGuid()).ToList();
    }

    public Card Draw()
    {
        // 先頭のカードを取得する仕様
        Card card = cards[topIndex];
        topIndex++;
        return card;
    }
}
public class PlayingCardService
{
    public class PlayingCardService() { }

    public Card Play()
    {
        Deck deck = new Deck();
        Card drawed = deck.Draw();
        return drawed;
    }
}

仕様が散らばったように見えてしまうかもしれませんが、モデルの表現力が上がり各クラスの責務も明確になりました!
今回の例では省略しますが、これに加えてユニットテストを書くことで、コードの品質と仕様の担保という点でより良い状態になるでしょう。

また、setterを公開するのをやめてコンストラクタのみで値を設定できるようにしたことで、不正な値のカードを作ってしまうような仕様を破壊するコードが入り込みにくくなりました。

public Card Play()
{
    // コンパイルは通るが実行時に例外が発生
    Card card = new Card(14);

    Deck deck = new Deck();
    Card drawed = deck.Draw();
    // setterは無いのでコンパイルエラー
    drawed.SetNumber(7);
    return drawed;
}

特に嬉しいのはコードのコメントやコードレビューだけでこの状態を担保するのではなく、機械的にも担保できている点です。
これであれば実装中に気が付くことが出来るため、手戻りのコストが少ない段階で修正できる可能性があがります。

業務仕様を変更するときのメリット

しばらくしてカードの数字を1~15にするという仕様変更の要望が挙がりました。
この仕様はCardクラスで表現しているので、修正箇所はCardクラス、影響範囲はCardクラスを利用している箇所となります。修正箇所や難易度は大体把握できそうです!

ところでこの仕様は変更しても良いものなのでしょうか?
Cardクラスのコンストラクタでは、1~13以外の数字が渡されると例外を投げています。
つまり、このコードが書かれた時点ではそれ以外の数字のカードを認めていなかったということがわかります。
ですので、開発に入る前に「今まで前提としていた仕様を変えても問題ないのか?」という観点から、仕様変更の背景を確認したり対応の要否を判断したりすることが出来ます。

こういったやりとりは要望が挙がった段階で行われるのが理想的ですが、少なくともリリース直前やリリース後に発覚するよりかは手戻りのコストを少なく抑えています。
モデルがドメイン知識を表すことで、このようなメリットも生まれます!

柔軟に素早く、かつ堅牢に

アジャイルな開発といえど、スピードのためにコードの質を犠牲にしても良いということはありません。
むしろ質の悪いコードが開発サイクルを回す速度を低下させる原因になってしまいます。

ドメイン知識を表すモデルを作ることで素早く仕様を把握できるようになるだけではなく、仕様を破壊する変更からコードを守り、質を担保した堅牢かつ柔軟な開発を支えることができます!