30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

『WEB+DB PRESS Vol.113』の「体験 ドメイン駆動設計 モデリングから実装までを一気に制覇」を読んだ

ドメイン駆動設計(以下 DDD)に関心があるので読んでみた。
私のような初心者にも分かりやすい内容だったので、DDD に興味を持ったけど挫折した、という人は読んでみるといいと思う。

gihyo.jp

DDD に関心を持ったキッカケはよく覚えていて、今年の2月。
SPA のフルリニューアルを一人でやることになり、設計や技術選定について考えていた。
既存の SPA の出来があまりにもひどくて、毎日がとにかく苦痛で不愉快だった。その体験があったため、自分はちゃんとしたモノを作ろうという気持ちが強かった。
そこらへんの話は以下の記事にも書いた。

numb86-tech.hatenablog.com

その時期に出会った記事のひとつが、これ。

medium.com

この記事の内容そのものも参考になったが、この記事で触れられている DDD にも関心を持った。
詳しくは分からないが、DDD というものを使えば、もっといいものを作れるのではないか。そう感じた。

その後、ネット上の記事を読んだり、「わかる!ドメイン駆動設計 ~もちこちゃんの大冒険~」を読んだりした。
どれも有益ではあったのだが、断片的な用語や考え方しか得られていないような感覚があった。かといって、原典は難解な本として有名であり、今の私が読んで理解できるとは思えない。
上記の Vuex の記事のように、断片的、部分的に DDD の知見を使うだけでも十分に価値はあると思う。だが、DDD の全体像を頭のなかで上手く描けておらず、消化不良のような感覚を抱いたままだった。
「よく分からない」という意識をずっと抱えており、何より、実際のプロダクトにどう活かしていけばいいのか、イメージできなかった。

この特集を読んで初めて、プロダクトに活かせる、実践できるかもしれない、という感覚を抱けた。
少しは頭のなかに地図を描けたような気がする。知識や用語同士のつながりや関連性を自分なりに持つことが出来た。

説明や例えが分かりやすいし、具体的なソフトウェアを題材にした実践的な内容になっている。
何より、短くて、かつ、「一連の流れ」が描かれている。モデリングだけ説明されてもよく分からないし、アーキテクチャの話だけされてもそれを使う意図を掴めない。かといって全てを説明しようとすると難易度が急激に上昇する。
本特集は DDD を実践していく流れをコンパクトに説明しており、丁度よい分量になっていると思う。

以下、自分なりのまとめ。

ドメイン、モデリング、モデル

ソフトウェアは、何らかの問題を解決するために作られる。 その、解決しようとしている対象、それを取り巻く領域を、ドメインという。その領域に存在する事象や概念、物体などが、ドメインに含まれる。

だが、ドメインに含まれる全てが、ソフトウェアに必要なわけではない。ソフトウェアにとって重要かつ十分な知識や概念を抽出する行為をモデリングと呼び、成果物をモデルという。
以下の定義が分かりやすかった。

ここでは、「モデルとは、問題解決のために物事の特定の側面を抽象化したもの」と定義します。

ドメインの概念からモデリングして作ったモデルを、ドメインモデルという。

モデルは、問題を解決するために作るもの。だから、問題を解決できるのがよいモデル。理解しやすいモデルでも、問題解決に役に立たないなら価値がない。

DDD にはモデルファーストという考えがあり、まずコードではなくモデルを作ることから始める。

モデリングのやり方

モデリングには決まった方法はない。
本特集では、ドメインモデル図を作るという方法を紹介している。ドメインモデル図とは、簡易化したクラス図のようなもの。

モデリングをやる前にまず、スコープを定めて、モデリングの対象を限定しておく。

モデリングの説明で「オブジェクト」という言葉が出てきたが、これが何を指すのか分からなかった。プログラミング言語という文脈での「オブジェクト」は知っているが、モデリングという文脈ではそれは何を意味するのだろうか。
取り敢えず、「概念」とか「ドメインモデルの構成要素」のような意味で捉えた。
そして、モデリングとは、オブジェクトそのものやオブジェクト同士の関係性について、ルールや成約を定義していく行為、と解釈した。

モデリングの作業は一度で終わりではなく、何度も繰り返して、より適切なものにしていく。
オブジェクト同士の関係性を見直したり、オブジェクトを分割したり名前を変えたりすることで。

モデリングの過程で発見した言葉や概念は、コード、ミーティング、ドキュメントなど、プロジェクト内の全ての場所で使うことを目指す。
この言葉をユビキタス言語という。

ドメインモデルがある程度出来たら次は、ドメインモデルに対して集約の範囲を定義する。
集約とはオブジェクトのグループ分けのようなもので、全てのオブジェクトはいずれかの集約に所属する。
どういう基準でグループ分けするかというと、整合性を強く確保したいオブジェクトを、一つの集約にまとめる。そして、集約毎に、「親」となるオブジェクトをひとつ決める。そのオブジェクトを「集約ルート」という。
同じ集約に所属するオブジェクトは、必ず集約単位で取得や処理を行う。言い換えれば、ひとつのトランザクションにまとめる。集約ルートに状態管理の責務を負わせ、子を直接操作できないようにする。
そうすることで、集約内のオブジェクトの整合性を保つことが可能になる。

ドメインモデルをコードに落とし込む

モデリングのあとは、ドメインモデルをコードに落とし込む。

そのときにありがちな失敗が「ドメインモデル貧血症」。
これは、ドメインモデルを実装するためのオブジェクト(ドメインオブジェクト)が、ドメインに関する知識を持っていない状態になってしまうこと。
より具体的には、ドメインオブジェクトが単なるデータ構造体になってしまっており、それとは別に、処理を行うための機能を持ったオブジェクトが作られ、そちらにドメイン知識が書かれてしまっている状態。

DDD について積極的に情報発信している増田亨さんのこのブログ記事が、分かりやすい。

新訳版『テスト駆動開発』に学ぶオブジェクト指向設計 | システム設計日記

ドメインモデル貧血症について書かれた記事ではないが、この記事でいう「手続き型」の設計が、ドメインモデル貧血症に近いと思う。DDD なのにこのような状態になってしまっている場合は、見直す必要がある。

データを持つクラスと処理を持つクラスを分け、トランザクションスクリプトを開発するスタイル

そして以下のように、ドメインオブジェクトを単なるデータ構造体にはせずに、ドメインに関する知識を持たせるのが、在るべき姿。

「換算」という機能よりも、もっと基本的な部品である「ドル」や「スイスフラン」をどうオブジェクトとして表現するかからスタートしています。問題領域の関心事をオブジェクトで表現するという典型的なドメインモデルの開発スタイルです。

そして「処理を行うための機能を持ったオブジェクト」は、ドメインに関する知識を持たず、ドメインオブジェクトを使ったユースケースが記述されたものにしていく。

ドメインオブジェクトを実装する際は、正しい状態のインスタンスしか存在させないようにすることを意識する。
そのために、インスタンスを作成するメソッドや、インスタンスが持っている状態を変更するメソッドは、その原則が守られるように実装する。それと同時に、余計なメソッドを露出させずプライベートメソッドにしておき、インスタンスに意図しない変更が加えられることを予防しておく。

また、ドメインオブジェクトを適切に分割していき、本来持つべきビジネス知識のみを、持つようにする。
そうすることでオブジェクトの肥大化を防ぎ、責務の適切な分離が進むことで、可読性や保守性が高まる。

アプリケーションアーキテクチャ

上記のようなコードを実現するための指針として、アプリケーションアーキテクチャがある。
アプリケーションアーキテクチャとは、アプリケーション全体にまたがる設計のこと。アプリケーション全体をレイヤに分割し、レイヤ毎に責務を定義していく。

DDD は、特定のアプリケーションアーキテクチャと結びついてるわけではない。この特集では一例としてオニオンアーキテクチャを紹介している。
サンプルコードのリポジトリに、オニオンアーキテクチャの図が掲載されている。

github.com

依存関係は、下方向への一方通行になっている。そのため、ドメイン層はどこにも依存しない。
両端にある「コントローラ」や「リポジトリ」が、各レイヤーの実装。

DDD では、ドメインモデルを表現するためのパターンとして、以下の4つを定義している。

  • エンティティ
  • 値オブジェクト
  • ドメインサービス
  • ドメインイベント

ドメインモデルをオブジェクトとして表現するのは、エンティティと値オブジェクトの2つ。
両者の違いは以下を参照。本特集の執筆者の一人である松岡幸一郎さんによって書かれたもので、本特集にもほぼ同じ内容の説明が書かれているのだが、非常に分かりやすい。
DDD基礎解説:Entity、ValueObjectってなんなんだ - little hands' lab

ドメインサービスは、オブジェクトで表現するのが難しいものを表現するために使われる。

アプリケーションサービスは、ドメインモデル(が公開している操作)を使ってユースケースを実現する。

リポジトリは、永続化のためのインターフェイスを提供するもの。
インターフェイスの定義はドメイン層で行い、実装はインフラストラクチャ層で行う。
こうすることで、ドメイン層が、ドメイン知識以外の知識(DBやテーブル構造などに関する知識)について意識せずに済む。

とにかく、レイヤ毎の責務を守ることが大切。
例えばドメインに関する知識については、ドメイン層から外に漏れ出さないようにする。もしアプリケーションサービスにドメイン知識が書かれてあれば、その実装はおかしい。ドメイン層以外がドメイン知識について知っていてはいけない。アプリケーションサービスには、ドメインオブジェクトを使って何をするかという、抽象的な記述しか書かれていないはず。

先程のリポジトリの例もそうだが、それぞれのレイヤの責務や役割を意識し、コードが、あるべきところにある状態にすることが大切。
そうすることで、コードを理解するのが楽になるし、コードの変更も安全に行えるようになる。

重要なのは、最初から完璧な設計を目指すのではなく、継続的に改善を繰り返していくこと。

DDD が目指すもの

ドメインを適切にコード(ソフトウェア)に反映させることで、よりよく問題解決できるようにすること。ならびに、そういう状態を維持できるようにすること。
これが DDD の目指すものだと理解した。

ドメインからモデルを抽出して、それをコードに落とし込む。そのため、モデルを媒介としてドメインとコードは連動している。

しかし、ドメインは様々な理由で変化していく。コードは、その変化に対応しないといけない。
そのため、コードを変更するコストを下げることが大切になる。変更のコストが高いコードは、ドメインの変化に対応することが出来ず、ドメインとコードが乖離していってしまう。

そのために、アーキテクチャがある。変更に強い実装にするための指針として、アーキテクチャがある。
そしてそれは、アーキテクチャ自体には意味がないということでもある。モデルを適切にコードで表現するための指針としてアーキテクチャがあるのであり、アーキテクチャを遵守することそのものには意味がない。
アーキテクチャを活用して上述の「コードがあるべきところにある状態」を実現できれば、ドメインを正しく表現しており、かつ、ドメインの変化にも追従していきやすい、そんなコードになっているはず。

また、DDD は変化を前提にしており、そして継続的な改善が重要であるため、テストコードの存在は不可欠と言える。テストコードがなければ、それだけで変更コストが非常に高いものになってしまう。

なぜ useEffect では無限ループが起こり得るのか

React のuseEffectは、その仕組み上、書き方によっては無限ループが発生してしまう。
それはなぜ発生するのか、そしてどう対処すればいいのか。一度理解してしまえば大した話でもないのだが、自分の理解を整理するために書いておく。

動作確認に使った React のバージョンは16.10.2

エフェクトが実行されるタイミング

原則として、関数コンポーネントが呼び出される度に、その関数に書かれてあるuseEffectが実行される。
実行のタイミングは、その関数コンポーネントが React element を返し、それによる DOM の変更が行われたあとになる。

関数コンポーネントが呼び出される条件については、以下に書いた。端的に言えば、stateに新しい値が渡されるか親コンポーネントが再呼び出しされるかすると、関数コンポーネントは再呼び出しされる。

numb86-tech.hatenablog.com

なので以下の例では、ボタンを押す度にuseEffectが実行され、ログが流れる。

import React, {useState, useEffect} from 'react';

function App() {
  const [state, setState] = useState(0);

  useEffect(() => {
    console.log('effect');
  });

  const onClick = () => {
    setState(s => s + 1);
  };

  return (
    <div>
      <p>{state}</p>
      <button type="button" onClick={onClick}>
        Click me
      </button>
    </div>
  );
}
export default App;

状態の更新とエフェクトの実行の無限ループ

stateに新しい値がセットされると関数コンポーネントが呼び出されてその都度useEffectが実行される、という仕組み上、もしuseEffectのなかで常にstateに新しい値を渡すようになっていた場合、以下のような無限ループが発生してしまう。

  1. コンポーネントがマウントされる
  2. useEffectが実行され、stateを更新する
  3. stateが更新されたので、コンポーネントが再び実行される
  4. useEffectが実行され、stateを更新する
  5. 以下、この繰り返し

以下のサンプルはこのパターンで、Profile関数コンポーネントが実行される度にuserProfileに(値は同じだが)参照が異なるオブジェクトをセットするので、関数呼び出しが止まらなくなる。

import React, {useState, useEffect} from 'react';

function Profile() {
  const [userId, setUserId] = useState(1);
  const [userProfile, setUserProfile] = useState(null);

  useEffect(() => {
    setUserProfile(
      userId === 1 ? {name: 'Alice', age: 20} : {name: 'Bob', age: 25}
    );
  });

  const onClick = () => {
    setUserId(currentId => (currentId === 1 ? 2 : 1));
  };

  return (
    <div>
      <p>{userId}</p>
      {userProfile && (
        <>
          <p>name: {userProfile.name}</p>
          <p>age: {userProfile.age}</p>
        </>
      )}
      <button type="button" onClick={onClick}>
        toggle user
      </button>
    </div>
  );
}
export default Profile;

エフェクトの実行に条件をつける

これを防ぐには、useEffectを常に実行するのではなく、条件を満たしたときにだけ実行するようにする必要がある。
useEffectの第二引数に配列を渡すことで、これが可能になる。
この第二引数の配列(以下、「依存配列」と呼ぶ)に値を入れておくと、その値が前回の関数呼び出し時から変化したときにのみ、useEffectが実行されるようになる。

先程のProfileuseEffectを以下のように書き換えると、userIdに更新があったときにのみuseEffectが実行される。
そのため、無限ループは発生しなくなり、マウントした時とボタンを押してuserIdが変更された時にのみ、useEffectが実行されるようになる。

  useEffect(() => {
    setUserProfile(
      userId === 1 ? {name: 'Alice', age: 20} : {name: 'Bob', age: 25}
    );
  }, [userId]);

依存配列の要素が変更されたかどうかの判定にはSameValueアルゴリズムを使っている。このアルゴリズムについても、以下の記事に書いている。

numb86-tech.hatenablog.com

SameValueアルゴリズムでは0-0を区別するため、以下の例ではボタンを押下する度にuseEffectが実行される。

import React, {useState, useEffect} from 'react';

function App() {
  const [state, setState] = useState(0);

  useEffect(() => {
    console.log('effect');
  }, [state]);

  const onClick = () => {
    setState(s => {
      const currentState = s;
      const nextState = 1 / currentState === Infinity ? -0 : 0;
      console.log(currentState);
      console.log(nextState);
      console.log(currentState === nextState); // === では常に true になる
      return nextState;
    });
  };

  return (
    <div>
      <p>{state}</p>
      <button type="button" onClick={onClick}>
        click
      </button>
    </div>
  );
}
export default App;

予期せぬ無限ループやバグを防ぐためにも、その関数スコープ内の値(statepropsなど)のうちuseEffectで使っているものについては、その値を依存配列に含めるべき。

eslint-plugin-react-hooksを入れてreact-hooks/exhaustive-depsを有効にしておけば、漏れがあった際にそれを検知してくれる。
setUserProfileによる無限ループの例も、正しく検知される。

f:id:numb_86:20191023115644g:plain

関数を依存配列に入れることで発生する無限ループ

しかし、とにかく依存配列さえ使えばよいという訳ではない。正しく理解して使わないと、やはり無限ループが発生してしまう。

依存配列に関数を入れる場合は、よく注意しないといけない。
ProfileコンポーネントのuseEffectを書き換え、プロフィール取得のロジックをgetUserProfileという別の関数に切り出した。
それをuseEffect内で使っているので、依存配列に含めた。react-hooks/exhaustive-depsも、そうするように指摘してくる。

  const getUserProfile = () =>
    userId === 1 ? {name: 'Alice', age: 20} : {name: 'Bob', age: 25};

  useEffect(() => {
    setUserProfile(getUserProfile());
  }, [getUserProfile]);

だがこれは、無限ループを生む。
なぜなら、getUserProfileProfile関数コンポーネントの呼び出し毎に新しく作られるため。
そのため、依存配列が更新されたと見做され、このuseEffectはコンポーネントが再呼び出しされる毎に実行されてしまう。
useEffectのなかでstateを更新していなければ取り敢えず無限ループは発生しないが、今回は更新してしまっている。

関数に限らず、関数コンポーネントのスコープ内の値は、関数コンポーネントが呼び出される度に新しく作られる。

以下の例はそれを示している。
ボタンを押してAppが呼び出される度に新たにsampleFuncが作られる。

import React, {useState} from 'react';

const funcArray = [];

const App = () => {
  const [state, setState] = useState(0);
  const sampleFunc = () => {};
  funcArray.push(sampleFunc);

  if (funcArray.length >= 2) {
    console.log(
      Object.is(
        funcArray[funcArray.length - 2],
        funcArray[funcArray.length - 1]
      )
    ); // false
  }

  return (
    <>
      {state}
      <button
        type="button"
        onClick={() => {
          setState(s => s + 1);
        }}
      >
        click
      </button>
    </>
  );
};

export default App;

これを回避する方法はいくつかある。

まず、sampleFuncを関数コンポーネントの外で定義する。そうすると、sampleFuncは同じ参照を指し続けることになる。

 import React, {useState} from 'react';

 const funcArray = [];
+const sampleFunc = () => {};

 const App = () => {
   const [state, setState] = useState(0);
-  const sampleFunc = () => {};
   funcArray.push(sampleFunc);

   if (funcArray.length >= 2) {
@@ -13,7 +13,7 @@ const App = () => {
         funcArray[funcArray.length - 2],
         funcArray[funcArray.length - 1]
       )
-    ); // false
+    ); // true
   }

   return (

あるいは、useCallbackを使うことでも対応できる。

-import React, {useState} from 'react';
+import React, {useState, useCallback} from 'react';

 const funcArray = [];

 const App = () => {
   const [state, setState] = useState(0);
-  const sampleFunc = () => {};
+  const sampleFunc = useCallback(() => {}, []);
   funcArray.push(sampleFunc);

   if (funcArray.length >= 2) {
@@ -13,7 +13,7 @@ const App = () => {
         funcArray[funcArray.length - 2],
         funcArray[funcArray.length - 1]
       )
-    ); // false
+    ); // true
   }

   return (

Profileも同じ要領で対応できる。
以下は、useCallbackを使った例。

  const getUserProfile = useCallback(
    () => (userId === 1 ? {name: 'Alice', age: 20} : {name: 'Bob', age: 25}),
    [userId]
  );

あるいは今回のケースだと、getUserProfileuseEffectのなかで定義すれば、getUserProfileを依存配列に含める必要がなくなる。

  useEffect(() => {
    const getUserProfile = () =>
      userId === 1 ? {name: 'Alice', age: 20} : {name: 'Bob', age: 25};

    setUserProfile(getUserProfile());
  }, [userId]);

ちなみに、useState()[1]useReducer()[1]、いわゆるsetStatedispatchは、同一性が維持されることが React によって保証されている。

import React, {useState} from 'react';

const funcArray = [];

const App = () => {
  const [state, setState] = useState(0);
  funcArray.push(setState);

  if (funcArray.length >= 2) {
    console.log(
      Object.is(
        funcArray[funcArray.length - 2],
        funcArray[funcArray.length - 1]
      )
    ); // true
  }

  return (
    <>
      {state}
      <button
        type="button"
        onClick={() => {
          setState(s => s + 1);
        }}
      >
        click
      </button>
    </>
  );
};

export default App;

参考資料