コマンドメソッドとクエリメソッドについて

ドメイン駆動設計
The view from the deck of a cruise ship

この記事の目的

エヴァンス本を読んでいる時に、「コマンドメソッド」「クエリメソッド」という聞き馴染みのない言葉がありました。今回はこの二つのメソッドについて、どのように使うのか、どんなメリットがあるのかについて書いていこうと思います。

コマンドメソッド、クエリメソッドとは?

コマンドメソッドとクエリメソッドの概念は、CQRS (Command Query Responsibility Segregation) の原則からきています。簡単に言うと、システムの操作(コマンド)と問い合わせ(クエリ)は別々の責任を持ち、それらは分離されるべきという考え方です。

  1. クエリメソッド: データの状態を返すが、状態を変更しない。
  2. コマンドメソッド: データの状態を変更するが、結果を返さない。

データを取得しようと思っていたコードが思いがけずデータを変更が起きてしまう、そんなコードは保守性が低くなりそうですね。。

では実際にどんなコードがあり得るのか具体的なコードを考えてみます。

分離されてない例① 複数の責任のあるメソッド

class UserController extends Controller
{
    // この関数は、ユーザー情報を取得すると同時に、最後のアクセス日時を更新してしまいます。
    public function getUserData($id)
    {
        $user = User::find($id);
        
        // クエリ部分: ユーザー情報を取得
        $userData = [
            'name' => $user->name,
            'email' => $user->email,
        ];
        
        // コマンド部分: ユーザーの最後のアクセス日時を更新
        $user->last_accessed_at = now();
        $user->save();

        return $userData;
    }
}

これは命名にも問題はありますが、「getUserData」メソッドでユーザーのデータを取得できるなと思い、使用したらこのメソッドを呼ぶとユーザーデータを返す&ユーザーのアクセス日時の更新が走ってしまいます。

この原因は、ひとつのメソッドで複数の機能を持たせることで、単一原則の責任に反しているコードになってしまっているからです。

分離されてない例① 複数の責任のあるメソッド 【改修】

class UserController extends Controller
{
    public function getUserData($id)
    {
        $user = User::find($id);
        return [
            'name' => $user->name,
            'email' => $user->email,
        ];
    }

    public function markUserAsActive($id)
    {
        $user = User::find($id);
        $user->last_accessed_at = now();
        $user->save();
    }
}

今回の原因は、getUserDataメソッドに2つの機能(ユーザー取得、ユーザーの最後のアクセスの記録)があることが問題でした。
なので、メソッドを2つに分けてユーザーの最後のアクセス記録は別にしました。

分離されてない例② 副作用をもつクエリ

class Cart
{
    private $items;

    public function __construct()
    {
        $this->items = [];
    }

    // このメソッドはカートの中身を取得すると同時に、商品の価格を10%オフにする。
    public function getItemsWithDiscount()
    {
        foreach ($this->items as &$item) {
            // コマンド部分: アイテムの価格を10%オフにする
            $item['price'] *= 0.9;
        }

        // クエリ部分: カートのアイテムリストを返す
        return $this->items;
    }
}

上記の updatePrice メソッドは、価格を更新するコマンドの役割と、更新後の製品情報を返すクエリの役割の両方を果たしています。なので、クエリメソッドとコマンドメソッドが混在している形になります。この動作が他の人から見てまぎわらしいコードになっています。なぜなら、getItemsWithDiscountでカートの中身を取得できると思っているからです。

分離した例② 副作用をもつクエリ

class Cart
{
    private $items;

    public function __construct()
    {
        $this->items = [];
    }

    public function getItems()
    {
        return $this->items;
    }

    public function applyDiscountToAllItems()
    {
        foreach ($this->items as &$item) {
            $item['price'] *= 0.9;
        }
    }
}

今回も対処としては先程のものと一緒で新たにカートの中身を割引するメソッドを離してgetItemsメソッドの単一性を確保しました。

まとめ

今回はエヴァンス本に書かれているコマンドメソッドとクエリメソッドについてLaravelで具体的なコードを用いて勉強しました。
自分自身コードを急いで書いていたりすると陥る可能性があるなと思ってるので、メソッドを書く際はどこに影響を与えるのか、他人から見ても分かりやすいコードなのかなど考えて書いていかなきゃなと思いました。

コメント

タイトルとURLをコピーしました