Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
125
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

ドメインモデル、ドメインロジックとは何かをコードを交えて考えてみる

この記事について

オブジェクト指向設計の文脈で度々目にする「ドメインモデル」や「ドメインロジック」というものが具体的に何を指しているのか、ドメイン以外のモデルってあるの?とか、じゃあアプリケーションロジックってなんだろう、というようなことをコードを交えて定義してみようという試みです。

はじめに

コード例は、マルチパラダイムなプログラミング言語 PHP で書きます。

対象読者は「ドメインが何を指すのかよく分からない」と思っている方ですが、オブジェクト指向の猛者の方にも間違いがないか見てもらいたいので読んでほしいです。

そもそも「ドメイン」とは

語義

MACMILLAN DICTIONARY によると

a particular area of activity or life
domain (noun) American English definition and synonyms | Macmillan Dictionary

と定義されています(「活動や生活における特定の領域」)。

ひとまず「領域」と定義してよさそうです。

ソフトウェア開発以外でのドメイン

本題に入る前に、ソフトウェア開発以外でドメインという言葉がどんな意味や用法で使われているかを見てみます。

インターネットドメイン

ネットワーク上の複数のコンピューターを管理するためのグループや組織を表す言葉。
ドメインとは - コトバンク

.jp とか qiita.com みたいなやつですね、ネットワーク上にあるコンピュータの識別情報として使われます。元々は www.qiita.com などとドメインの前にホスト名をつけてウェブサーバーを明示していましたが、最近ではホスト名を省略するケースが多いようです。

「グループや組織」ということで、こちらも「領域」と捉えることができそうです。

事業ドメイン

企業の持続的な成長を可能とする自社特有の事業活動領域のこと
ドメイン | 用語解説 | 野村総合研究所(NRI)

会社に所属しているひとであれば、あなたの会社の事業ドメインはなんですか、という問いに答えらたほうがいいかもしれません。
ようするにどんな活動で収益を上げていますか、ということですかね。

すでに上の定義に「事業活動領域」とあるので、こちらも「領域」を指しています。

数学におけるドメイン

数学における写像の定義域(ていぎいき、英: domain of definition)あるいは始域(しいき、英: domain; 域, 領域)とは、写像の値の定義される引数(「入力」)の取り得る値全体からなる集合である。
定義域 - Wikipedia

集合という言葉が登場しました。

詳しく知りたい方は「定義域」などでぐぐっていただくとして、ここでは「写像」についてのみ補足します。

写像は関数の一種で、入力値の集合 X に対して、何らかの関数 f を用いて変換したとき、X のそれぞれの要素に対応した出力値の集合 Y の要素が定まるようなとき、関数 f は写像である、といいます。

以下は Elixir で表現した写像の例です(最もシンプルにわかりやすく書けそうだったので選びました)。

Enum.map([1, 2, 3], fn x -> x * 2 end)
# [2, 4, 6]

始域([1,2,3])に対して終域([2,4,6])が定まるような関数 fn x -> x * 2 end (入力パラメータを2倍して出力する)は写像です。
(入力値の集合を「始域 (=domain) 」というのに対し、出力値の集合を「終域 (=codomain)」といいます)

つまり、数学におけるドメインとは、なんらかの関数に与える入力値の集合である、といえそうです。

(ここらへん理解が曖昧なので間違っていたらどなたか補足をお願いします)

ソフトウェア開発におけるドメイン

さて、ようやく本題です。

ソフトウェア開発におけるドメインという言葉の初出は寡聞にして知らないんですが、けっこう昔から使われている用語な気がします(少なくとも私が最初に読んだオブジェクト指向について書かれた本「Multi-Paradigm Design for C++ - James O. Coplien」では使われてたと記憶しています。1998年出版)。

本記事ではひとまず有名な Eric Evans の「ドメイン駆動設計」から抜粋します。

A sphere of knowledge, influence, or activity.
Evans, Eric. Domain-Driven Design

日本語では「知識、影響、または活動の領域」となります。

Evans は "sphere" という単語を使っているのでついでにこちらの意味も調べてみましょう。

a particular area of interest, activity, work etc that is one of many parts of life
sphere (noun) American English definition and synonyms | Macmillan Dictionary

「生活の一部となっているような関心事、活動、仕事などの特定の領域」みたいなかんじでしょうか。

"sphere" の定義自体にも "activity" が含まれていますが、"domain" の定義にも含まれていて、なにやら "activity"(= 活動)という概念ががとても重要な気がしてきました。

ここまでのまとめ

  • 「ドメイン」とは、なんらかの「領域」であり、その範囲は使われる文脈によって異なる。
  • ソフトウェアにおけるドメインは、事業ドメインと数学におけるドメインになんか近そう
  • activity (活動)っていう単語/概念がなんか重要そう

といったところでしょうか。

個人的な理解

ソフトウェア開発におけるドメインは、そのソフトウェアがなにをするためのものなのか、という定義のうち、ウェブとかデータベースとかメールとか、そういう外部のソフトウェアや決まりごと(HTTPとかSQLとかSMTPとか)の無関係な部分、なのかな、というのがざっくりした私の理解です。

2019年5月11日 12:35 追記
@takumi-n さんからご要望がありましたので、下記の点について補足します。

「ウェブとかデータベースとかメールとか、そういう外部のソフトウェアや決まりごと(HTTPとかSQLとかSMTPとか)の無関係な部分」のドメインモデル・ドメインロジックを抽出して考えると何が良いのか、

一義的には、たとえば、ウェブの API がエントリポイントになっているケースと、CLI がエントリポイントになっているような複数のユースケースに対応しやすい、というのがあると思うんですが、個人的にはそうしたケースは稀で、どちらかというと、処理の分割の際の指標にしやすい、というのが利点だと思っています。

よくあるのが、HTTPセッションに依存したクラスの状態管理が複雑化するとか、外部APIに依存したクラスのテストが書けないとか、そういうのをなくすために、アプリケーションの外にある状態から切り離すことで得られるメリットは大きいと思います(そういうシチュエーションは、上の複数クライアントのケースよりも頻度が高いという認識があります)。

追記ここまで

コードを使って表現してみる

さて、「ドメイン」の定義がなんとなくわかったところで、それを表現してみることにします。

おそらくこれらの定義を何回読み返しても、概念的なことを完全に理解するのは難しいんじゃないかっていう印象で、実際に用例を用いて補ってやる必要があるんじゃないかな、と思います。

本来ならダイアグラムなどを用いて、概念モデルで表現するべきなんでしょうけど、図を書くのがめんどくさいのですっ飛ばしてコードで表現することにします。

お題

複数の異なる用例があったほうがいいと思うので、2つの毛色の異なるサービスをお題にあげようと思います。

  1. メディア(Qiita)
  2. 転職支援サービス(Qiita Jobs)

(注:Qiita Jobs は一部の機能しか使ったことないので、データや振る舞いの部分を想像で補っている部分があります。Qiita 本体に関しても言えることですが、実際のモデルと異なることをご了承ください)

ドメインモデル

オブジェクト指向におけるモデルとは何か、という説明には以下のページに簡潔に記述がありましたので引用します。

モデルとは、何らかの実体があって、それを縮小(あるいは拡大)して細かい部分は省いて本質的なところだけを抽出して形成した形のことである。
オブジェクト指向モデル

「何らかの実体」というのがややミスリーディングなかんじもしますが、ある概念の中から、アプリケーションの中で扱うデータや処理を必要なところだけ抜き出して表現する、という解釈でよさそうです。

(他によい定義や解釈があれば補足をお願いします :bow:

後述しますが、たとえば、「記事」という概念(この場合は実体も存在しうるが、実体がなくても概念として成立すればモデルはつくれるでしょう)を扱うアプリケーションをつくろうとしたとき、その「記事」という概念にはどんな属性があれば成立するでしょうか。

また、それがウェブアプリケーションであろうが、CUI プログラムであろうが変更されない部分、インタフェースやデータ形式に依存しない部分はどこでしょうか。

そういった点に注意しながら、ドメインモデルを探索してみましょう。

個人的には、たとえドメインモデルであってもアプリケーションとして実装されることが前提であり、データベースやフレームワークと完全に切り離して考えることは意味がないとは思っていますが、モデリングの訓練や探求自体には意味はあると思っていて、概念を抽出して名前をつけたり、処理をどの単位で区切るかを考えて名前をつけたり、といったことは役に立つと思っています。

メディアにおけるドメインモデル

メディア(Qiita)におけるドメインモデルには、以下のようなものがありそうです。

  • 記事
  • 執筆者
  • タグ
  • いいね
  • ストック
  • コメント
  • 編集リクエスト

記事を例にとってみます。

Qiita の記事の特徴は、

  • 特定の執筆者にひもづいている(匿名では投稿できない)
  • タイトル、本文、タグ、などの属性がある
  • 編集、削除ができる
  • 限定共有投稿、公開を切り替えられる(ただし、一度公開した記事を非公開にはできない)
  • ページビュー、いいね、ストックの数がひもづいている
  • 最終更新日時から1年以上経過すると「この記事は最終更新日から○年以上が経過しています。」と表示される

といったところでしょうか。

いくつかデータと振る舞いをピックアップして、モデルを表現してみます。

上記のリストから単語を拾って、プロパティおよびメソッドにしてみます。

class Article {
    private $author;
    private $title;
    private $body;
    private $tags;
    private $lastUpdatedAt;

    public function __construct(
        Author $author,
        string $title,
        string $body,
        array $tags
    ) {}

    public function edit(string $title, string $body, string $tags) {}
    public function delete() {}
    public function sharePrivately() {}
    public function publish() {}
}

記事は、執筆者とタイトル、本文、タグがないと公開できないので、日時系のデータを除き、すべてをコンストラクタに渡すようにしました。

編集できるのはタイトルと本文、およびタグなので、 edit メソッドにはその3つを渡すようにしています。

それ以外のメソッドはいずれも引数なしです。

private と public という単語があるので、どうやら公開か非公開か、みたいな状態もプロパティとして持っておいたほうがよさそうなかんじがします。


...といったようなかんじでドメインモデルを表現してみました。

ここまで、URLとかユーザーとか、ウェブに関する単語や、データベースに保存する、みたいな概念を入れずにクラス定義をしました。

簡単ではありますが、ドメイン(Qiita というメディア)において、記事というモデルが、どんなデータを持ち、どんな振る舞いを持つのか、という点だけにフォーカスして表現可能である、ということは分かっていただけたのではないかと思います。

のちほどドメインロジックのところで、以下の2点を Qiita のドメインにおける特徴的な振る舞いとして取り上げようと思います。

  • 限定共有投稿、公開を切り替えられる(ただし、一度公開した記事を非公開(=限定共有)にはできない)
  • 最終更新日時から1年以上経過すると「この記事は最終更新日から○年以上が経過しています。」と表示される

転職支援サービスにおけるドメインモデル

続いて転職支援サービス(Qiita Jobs)では、こんなかんじです。

  • 開発チーム
  • 求職者
  • 募集職種
  • チャット

こちらのドメインでは、開発チームと求職者の関係性(協調)についてモデル化してみようと思います。

まずはデータ部分です。

class DeveloperTeam {
    /** Member[] */
    private $members;
}

class Member {
    private $name;
    private $teamId;
}

class JobSeeker {
    private $name;
}

class Candidate {
    /** JobSeeker */
    private $seeker;
    private $teamId;
}

class Chat {
    /** Message[] */
    private $messages;
}

class Message {
    private $content;
}

これらのモデルには以下の特徴があります。

  • JobSeeker は DeveloperTeam との Chat を開始することができる
  • JobSeeker は 特定の DeveloperTeam と Chat を始めると、Candidate となり、Team とのひもづけが行われる
  • Chat は、JobSeeker 側からしか開始できない
  • Candidate および DeveloperTeam の Member は任意のタイミングで Chat に投稿できる

では、これらの特徴をモデルに落とし込んでみます(実装は省略)。

class JobSeeker {
    private $name;

    public function startChatWith(DeveloperTeam $team): Chat {}
}

class Chat {
    /** Candidate */
    private $candidate;
    /** DeveloperTeam */
    private $team;
    /** Message[] */
    private $messages;

    public function post(Message $message): void {}
}

上のコードで表現できてないことで、ひとつ大事なことを忘れていました。

メッセージを書き込むアクターをどう表現するか、です。

Candidate および Member は任意のタイミングで Chat に投稿できる

とありますので、Candidate と Member 両方が Message の作成者になれそうです。

interface を使って表現してみます。

interface ChatParticipant {
    public function id(): int;
    public function name(): string;
}

class JobSeeker implements ChatParticipant
{
}

class Member implements ChatParticipant
{
}

class Message {
    private $content;
    /** ChatParticipant.id */
    private $creatorId;
}

Chat::post() のインタフェースもちょっと変更します。

class Chat {
    public function post(string $message, ChatParticipant $from): void {
        // メッセージを追加する処理はこんなかんじになりそう
        // $this->addMessage(new Message($message, $from->id()));
    }
}

Message の生成をだれにやらせるかというのを悩みますが、ひとまず Chat 内で生成するようにします。

お気づきと思いますが、JobSeeker と Member という異なるクラスで Message::createId に入る値を ChatParticipant で一意にしなければならないのが問題で、ここらへんはデータベースが絡んでくることになりそうですが、ChatParticipant の実態がアプリケーションのユーザーになったとしても型自体は変わらないので、上のコードを変える必要はなさそうです。

このへんは他にもいくつかやり方ありそうなので、こんな構造にしたほうがよさそう、みたいなご意見があれば、コメントいただけると助かります。

ここまでのまとめ

メディア(Qiita)、転職支援サービス(Qiita Jobs)という2つのドメインについて、それら固有のデータや振る舞いをそれぞれプロパティやメソッドとしてモデル化してみました。

対象のアプリケーションでやりたいことや扱いたい情報を言語化し、その中から名詞や動詞を抜き出してプロパティやメソッドにしていく作業を何度も行って、洗練されたモデルが手に入るんだと思いますが、もしやったことなければ自分が携わっているサービスでもこういった活動をやってみるといいのではないでしょうか。

ドメインロジック

ドメインロジックとはなにか、を考える前に、ドメインモデルの中で表現したデータ以外の部分を振り返ってみます。

たとえば、メディアのモデルにあった「限定共有投稿」という概念を sharePrivately というメソッド名で表現しました。

これはあくまでも名前なので、実際の振る舞いが「限定共有」になっていなければ偽りの名前になってしまいます。

また、転職支援サービスのモデルにあった「Chat は、JobSeeker 側からしか開始できない」という制約を JobSeeker::startChatWith() というメソッドで表現しましたが、 new Chat() を呼んでしまえば、どのクラスからでも生成できてしまいます。

さて、上記を踏まえた上で、ドメインロジックとは何か、を考えてみます。

プログラムにおける処理の内容、手順、方法のこと。
ロジック - Wikipedia

MVC なウェブアプリケーションにおける標準的な「処理の内容」と「手順」は、以下のような感じになると思います。

  1. Controller が Request から入力を受け取る
  2. Model が入力を元になんらかのデータを構築したり変更したりする
  3. Controller が Model の振る舞いを元に View を構築する
  4. View のデータ(HTML)を Response として返す

この手順はウェブアプリケーション固有のものなので、ドメインロジックではありませんが、その他にもデータベースやメールといった外部のソフトウェア(やプロトコル、データ形式など)に関連するもの以外はドメインロジックと呼んでしまってよさそうです。

(こちらも他によい定義があれば補足をお願いします :bow:

メディアにおけるドメインロジック

以下の2つについて、ロジックを考えてみます。

  • 限定共有投稿、公開を切り替えられる(ただし、一度公開した記事を非公開(=限定共有)にはできない)
  • 最終更新日時から1年以上経過すると「この記事は最終更新日から○年以上が経過しています。」と表示される

まずは「一度公開した記事を非公開(=限定共有)にはできない」の部分からです。

呼び出し側では、こんなかんじで使うことになると思います。

$article = $articles->find($articleId);
$article->sharePrivately();

このとき、すでに公開されている記事に対して呼び出された場合には例外を送出するとします。

public function sharePrivately() {
    if ($this->isPublished()) {
        throw new InvalidStateException('すでに公開されている記事に対しては限定共有に戻すことはできません。');
    }
}

InvalidStateException クラスはドメインで定義した例外クラスです)

  • 処理の内容は「限定共有状態にする」
  • 処理の手順は「公開状態かチェックする → 公開状態であれば状態の変更ができない旨クライアントに知らせる」
  • 処理の方法は「if 文で分岐し、例外を投げる」

となります。

続いて「最終更新日時から1年以上経過すると『この記事は最終更新日から○年以上が経過しています。』と表示される」の部分です。

これは Article クラスに処理をさせるかどうか悩むところですが、ここでは別クラスに切り出してみます。

記事の状態によって警告文を出す、という処理なので、 ArticleStateWarning とでもしておきます。

class ArticleStateWarning {
    private $article;

    public function __construct(Article $article) {}
    public function __toString() {
        $diffInYear = Carbon::today()->diffInYear($this->article->lastUpdatedAt();
        if ($diffInYear < 1) {
            return '';
        }
        return "この記事は最終更新日から{$diffInYear}年以上が経過しています。";
    }
}
  • 処理の内容は「警告文を生成する」
  • 処理の手順は「本日と記事の最終更新日との差分を年単位で計算する → 1未満であれば空文字、1以上であれば警告文を生成する」
  • 処理の方法は「Carbon ライブラリを使って差分を計算し、if 文で文字列の生成を分岐させる」

となります。

これらの条件文に書かれたルールはドメイン固有のものであり、他のメディアでもし同様の処理があったとしても、必ずしもルールが一致するとは限りません。

また、クライアントのクラスにどのような形で結果を返すか、というのも、ロジックの一部といっていいでしょう。

転職支援サービスにおけるドメインロジック

同じような要領で転職支援サービスにおけるドメインロジックも考えてみます。

  • Chat は、JobSeeker 側からしか開始できない

前述の

「Chat は、JobSeeker 側からしか開始できない」という制約を JobSeeker::startChatWith() というメソッドで表現しましたが、 new Chat() を呼んでしまえば、どのクラスからでも生成できてしまいます。

という部分について、もうちょっと深掘りしてみましょう。

おそらく中身はこんなイメージになると思います。

// JobSeeker
public function startChatWith(DeveloperTeam $team): Chat {
    $candidate = new Candidate($this, $team);
    $chat = new Chat($candidate, $team);
    // 他にも初期化処理があればここで
    return $chat;    
}

処理の流れを単純化すれば「 Chat を生成しそれを返す」だけですが、いちおうこれまでと同様に3つの属性を書いておきます。

  • 処理の内容は「Chat を生成する」
  • 処理の手順は「JobSeeker から Candidate を生成する → Candidate と DeveloperTeam から Chat を生成する」
  • 処理の方法は「それぞれのオブジェクトは new で生成する」

となります。

では、さらにこの処理を呼ぶであろうクライアント、Controller はどうなるでしょう。

// CreateChatController
// POST /chats
public function __invoke(Request $request) {
    // session か token か分からないがログインしているユーザーを取得する
    $user = Auth::user();
    // User にひもづいている JobSeeker を探す
    $jobSeeker = JobSeeker::ofUser($user)->findOrFail();
    // DeveloperTeam.id はリクエストから渡ってくる
    $team = Team::findOrFail($request->team_id);
    // Chat を生成する
    $chat = $jobSeeker->startWithChat($team);
    // レスポンスを返す(JSON形式を想定)
    return new Response($chat);
}

(ちなみに loadUser() やリクエストやらレスポンスやらが絡んでいる部分はアプリケーションロジックです)

これを見ると、JobSeeker のインスタンスはユーザーから取得できるようになっているので、やろうと思えば開発チームのコンテキスト(開発チーム専用のアプリケーションまたは Controller クラスがあるはずです)からでも Chat の生成ができてしまうでしょう。

これを阻止するためにはどうすればいいでしょう?

ひとつ思いついたのは、完全に有効な手段ではないんですが、コンテキストを名前空間で分けて、求職者のコンテキストでは生成可能とし、開発チームのコンテキストでは生成不可とする、というやり方です。

namespace Domain\Models;

class Chat {
    protected function __construct(Candidate $candidate, DeveloperTeam $team) {}
}

namespace JobSeeker\Models;

class Chat extends BaseChat {
    public function __construct(Candidate $candidate, DeveloperTeam $team) {}
}

namespace Member\Models;

class Chat extends BaseChat {
}

PHP ではパッケージ(名前空間)でアクセスを制限したりはできないので、 Member コンテキストから JobSeeker コンテキストの Chat クラスを使われたらこの目論見は突破されてしまいます。

まぁ、下手するとせっかく protected で宣言していても、意図がちゃんと伝わってないと public に変更されてしまったりするので、何事も完璧というわけにはいかないですね、そのへんはチームでいいかんじにバランスを取ればいいのではないかと思います。

ここまでのまとめ

ドメインロジックは「処理の内容、手順、方法」を説明できるものであるということと、使用する言語によって取れる手段が限られるので、その中からより良い選択ができるかどうか、というのがキモ、という気がします。

つくろうとしている/つくっているアプリケーション固有の概念のうち、セッションやデータベース、HTTPの世界の概念などと関係ない部分がドメインのロジックである、という説明はもうちょっと洗練できるんじゃないかという予感はありつつ、現時点では私の精一杯のものなので、ひとまずこれでご容赦いただければと思います。

おわりに

いかがでしたでしょうか?

「ドメイン」てのがよく分からん、という方

「ドメイン」「ドメインモデル」「ドメインロジック」という言葉/概念の指すものがなんとなく分かったでしょうか?

まだ分からん、ということであれば直接補足しますので、Twitter にてメンションください、よろしくお願いします(その結果腹落ちしてもらえたら、この記事に反映できればいいですね)。

他にも「ドメイン」とはこういうものだ、という意見をお持ちの方

ぜひコメント欄にてご意見お聞かせください。

参考書籍

  • ドメイン駆動設計 エリック・エヴァンス
  • オブジェクト指向設計実践ガイド Sandi Metz
  • ユースケース駆動開発実践ガイド ダグ・ローゼンバーグ, マット・ステファン
  • 現場で役立つシステム設計の原則 増田 亨
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
125
Help us understand the problem. What are the problem?