のんラボ

ユビキタス言語を重視する

2020/07/13 2020/07/13 ユビキタス言語を重視する

プログラマが知るべき97のこと

こんにちは。Nonです。本日はプログラマが知るべき97のことを読んでいてこれ「いいな」と思った箇所について書いてみようと思います。

こちらの書籍はとても有名なので、既読の方もいらっしゃるかもしれません。
そういう方は復習というか、内容を思い出したり、私の意見と違うところや共感できるところを探していただければ幸いです。

ドメインの言葉を使ったコード

突然ですが、DDDではユビキタス言語という業務上の言語を反映することが大事だよ。ということが言われています。

ユビキタス言語ってなに?

ユビキタス言語というのは、プログラミング設計をする上で、開発者だけでなくその他のステークホルダーとの共有言語のことを指します。

例えば、荷物の運送管理システムを使用する時、開発者はよくユーザーという言葉を利用しますが、ステークホルダーは運転手という言葉を利用するとき、やはりシステムでも運転手という言葉を利用する方がいいでしょう。

そして、開発者が運転手という言葉を利用するというのはコードにも反映するということになります。

例えばユーザーが荷物を運ぶ処理を持っていたとすると、

copied.<?php
class Driver
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $name;

    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    /**
     * @param Luggage $luggage
     * @return void
     */
    public function transferLuggage(Luggage $luggage)
    {
        // 荷物を運ぶ処理
    }
}

となります。クラス名はUserではなくDriverとなりますね。

ではログイン処理ではどうなるでしょうか。

境界付けられたコンテキストの観点からみると、ログイン処理は業務知識とはまた違うドメインになりますので、この場合はDriver->login()ではなく、User->login()の方がいいでしょう。

このように、業務知識内の言葉を利用することで、プログラムコードと実際にミーティングなどで利用される言葉の乖離を防ぐことで、ユースケースの齟齬、つまりユーザーとの誤解を避けるために作成され、使用する言葉のことを指します。

プログラムコードに仕様を書き込むとは?

こちらもしばしば見かける内容です。
仕様書を作成しないという意味ではありませんが、できるだけプログラムコードに仕様を反映するということは結構前から言われていたように思います。

キーワードは私の知る限り3点。

  • ビジネスロジック
  • 抽象クラス
  • ユビキタス言語

でしょうか。

ビジネスロジックで仕様をコードに表す

ビジネスロジックで仕様をコードに表すとは何か?2020年に突入した今なら、すでにご存知の方もたくさんいらっしゃるでしょう。

私はこれまで色々なプロジェクトを経験してきましたが、とにかく、コントローラーだけで実装を終了していたり、プログラムコードの示す目的がコードに反映されていないコードをたくさん見ました。

だから、「コードの仕様を先輩に聞いたり」、「なぜ、このコードが必要なのかを議論する」必要があったのです。

試しに例を作成してみました。

copied.class TransactionController
{
    public function store(Request $request)
    {
        // バリデーション処理など

        $user = Auth::user();
        Transaction::create([
            'user_id' => $user->id(),
            'product_name' => $request->get('name'),
            'product_price' => $request->get('price'),
        ]);

        // 処理
    }
}

これが示す仕様、ユースケースをこのコードから読み取れるでしょうか?簡単な例なので、もしやわかる方もいらっしゃるかもしれません。しかし、なんとなくTransaction取引を作成する処理だとわかるくらいでしょうか?

このコードは改善が必要に思います。

copied.class TransactionController
{
    /**
     * @var BuyProductInterface
     */
    private $useCase;

    public function __construct(BuyProductInterface $useCase)
    {
        $this->useCase = $useCase;
    }

    public function create(Request $request)
    {
        $this->useCase->process();
    }
}

class BuyProduct
{
    /**
     * @var CustomerService
     */
    private $customerService;

    public function __construct(CustomerService $customerService)
    {
        $this->customerService = $customerService;
    }

    public function process()
    {
        // $customer, $productの定義
        $this->customerService->buy($customer, $product);
    }
}

class CustomerService
{
    /**
     * @var TransactionRepositoryInterface
     */
    private $transaction;

    public function __construct(TransactionRepositoryInterface $transaction)
    {
        $this->transaction = $transaction;
    }

    /**
     * @param Product $luggage
     * @return void
     */
    public function buy(Customer $customer, Product $product)
    {
        // 商品を購入する処理
    }
}

ここまで書けば先程より伝わるでしょうか?
このプロジェクトの示す取引を作成するというのはお客様が商品を購入することを示していたのです。

このようにビジネスロジックそのものをコントローラーに直接記述してはいけません。とても無機質なコードになり、コードを業務に置き換える作業がとても大変で開発者にとってストレスがかかります。

このような現象をプログラムコードを業務へ翻訳するとか言ったりしますね。

その翻訳作業を無くすために、翻訳してある状態でコードにしておくことが重要です。業務に関する知識をビジネスロジックとして別に切り出し、そこでは業務に関する言葉などを利用することで、コードに仕様を表す事ができます。

抽象クラス

以前、ドメイン駆動設計の読書会についての記事で、「抽象に依存せよ」といった旨について記事にしたことがありますが、抽象クラスの役割は依存関係の解消だけではありません。仕様をコードに表すという観点からも有用です。

もしかしたら誤解を生んでしまうかもしれませんが、抽象クラスはそのオブジェクト(つまり業務での登場人物)のできることリストと言い換えることも出来ます。

1番はじめに使用した運転手の例で作成してみましょう。

copied.interface Driver
{
    public function transferLuggage(Luggage $luggage): void;

    public function startRest(DateTimeInterface $from): void;

    public function endRest(DateTimeInterface $to): void;

    public function moveBase(Base $base): self;
}

(実際の業務を無視して、)とにかく運転手ができそうなことをリストにしてみました。
運転手ができそうなことがこの抽象クラスからわかるかと思います。

開発者はこのクラスから、「このドメイン内の運転手はこのようなことができるのか」と大まかな仕様を理解することができるはずです。また、今後のミーティングなどで運転手にできることが増えたなら、この抽象クラスに処理を追加すればいいこともわかります。

入力値と出力値もわかるので、コーディングレベルでも伝えられることは多そうです。

ユビキタス言語

最後にユビキタス言語でコードに仕様を表してみましょう。

実はここまでのサンプルコードで出てきているのですが、プログラム処理的な言葉を排除し、業務的な言葉でコードを書くことを示します。(大体)

copied.class CustomerService
{
    // 省略
    
    /**
     * @param Product $luggage
     * @return void
     */
    public function createTransaction(Customer $customer, Product $product)
    {
        // 取引を作成する処理
    }
}

このようなコードだとcreateTransactionがどのような業務フローを示すのかがよくわかりません。
コードの内容をそのまま受け取るとお客様が取引を作成する?
うーんちょっとわかりません。

copied.class CustomerService
{
    // 省略
    
    /**
     * @param Product $luggage
     * @return void
     */
    public function buy(Customer $customer, Product $product)
    {
        // 取引を作成する処理
    }
}

このようにすればわかりますね。関数名がbuyですし、Product型の変数を渡していることから商品を購入する処理ということは想像できそうです。

このように変数名 / 関数名を少し業務に寄せるだけでも全然違います。

とても簡単に見えますが、結構難しいです。プログラミングをしているときは頭がプログラムの味方をしてしまうのです。

もう少し極端にしてみる

商品登録をする処理を作成してみましょう。

copied.class Staff
{
    public function createProduct(Product $product)
    {
        // 商品登録処理
    }
}

あっているようにも見えます。createはデータストアにデータを登録する処理として名付けられることもしばしばあります。

でもこれで満足してはいけません。本来ならこうなるはずです。

copied.class Staff
{
    public function registerProduct(Product $product)
    {
        // 商品登録処理
    }
}

登録なのですからregisterですよね。

商品更新ならどうでしょうか?updateProductが正解っぽいです。でもミーティングでは商品編集という言葉で進んでいました。ではeditProductが正解でしょう。

いやいや、ウチのミーティングでは商品保存で統一されているんだよ。

ならば、saveProductでUPSERT的な処理を記述するべきでしょう。

このように細かいところまで使用する言葉に気をつける必要があります。

こうすることでコードに仕様を表すことができるのです。

最後に

コードに仕様を書くことの大切さはプログラマが知るべき97のことでも書かれていましたね。

ここまでするの面倒だからしないという方もいるかも知れませんが、どうせコメントに仕様書くならコードに書きませんか?

次回もプログラマが知るべき97のことを読んで記事にしようかなと思っています。

その時はよしなに。

.