DDDとコードとしての正しさ

ドメイン駆動設計 #1 Advent Calendar 2018の14日目を担当する@pospomeです。

今回はDDDとコードとしての正しさについて書いてみようと思います。

DDDは設計手法である

DDD = ドメイン駆動設計 ですよね。
"設計"という単語が付いていることから分かる通り、DDDはあくまでソフトウェアにおける設計手法です。
そのためDDDの成果物は"コード"であり、DDDの目的は"コードの可読性を上げること"であると自分は考えています。
DDDが持つ"ビジネスを理解する", "ドメインを理解する"という側面も最終的にそれらをコードに落とし込み、可読性を上げるためのプロセスに過ぎません。

コードとしての正しさ

一般的にコードには"正しさ"が存在します。
例えば"SOLID原則"なんかがそれにあたります。

さらにコードはソフトウェアが採用する技術に依存します。

  • RDBを使うのか?NoSQLを使うのか?
  • Goを使うのか?Rubyを使うのか?

採用する技術によって最適なコードは変わるでしょう。

DDDは設計手法です。
そのためDDDを採用した結果、コードとしての正しさを見失った設計になってしまっては意味がありません。

コードとしての正しさを見失う

コードとしての正しさを見失うであろう実装例を紹介しましょう。

ユースケースの日本語を"そのまま"コードに落とし込もうとする

DDDにはユビキタス言語という概念が存在します。
この概念によって言葉の大切さを実感した方も多いのではないでしょうか。
しかし、それを飛躍させてユースケースの日本語を"そのまま"コードに落とし込もうとするとコードとしての正しさを失う可能性があります。

例えば自分の近況を投稿できるSNSがあったとしましょう。
近況投稿のユースケースは"ユーザーは自分の近況を投稿する"という感じになります。
そして、このユースケースをコードに落とし込んだ例が以下です。

u := NewUser(100, "pospome")
p := u.NewPost("message") //ユーザーは自分の近況を投稿する
repository.InsertPost(p)



User,Postの実装は以下です。

type User struct {
  ID   int
  Name string
}

func NewUser(id int, name string) *User {
  return &User{
    id,
    name,
  }
}

func (u *User) NewPost(body string) *Post {
  return NewPost(u.ID, body)
}

type Post struct {
  UserID int
  Body   string
}

func NewPost(userID int, body string) *Post {
  return &Post{
    userID,
    body,
  }
}

一見問題なさそうですが、以下の2点に問題があります。

  1. User.NewPost()によってUserがPostに依存しており、PostもUser.IDをフィールドに持っているので、UserとPostが暗黙的に相互依存している。
  2. SNSの操作は基本的にユーザーが行うものなので、ユースケースは"ユーザーはxxx"のようにユーザーを主語とすることが多い。そのためUser.NewPost()と同じようなUser.NewXxx()というファクトリが多くなる。その結果、ユーザーは多くのモデルに対して依存する可能性がある。

1点目の暗黙的な依存に関しては以下のようにUser.IDが独自型であり、
User,Postが別パッケージである場合はパッケージ間の循環参照が発生します。
Goではコンパイルすら通りません。

type UserID int

type User struct {
  ID   UserID
  Name string
}



これらの問題を解決した実装が以下です。

u := NewUser(100, "pospome")
p := NewPost(u.ID, "message")
repository.InsertPost(p)



User,Postの実装は以下です。

type User struct {
  ID   int
  Name string
}

func NewUser(id int, name string) *User {
  return &User{
    id,
    name,
  }
}

type Post struct {
  UserID int
  Body   string
}

func NewPost(userID int, body string) *Post {
  return &Post{
    userID,
    body,
  }
}

User.NewPost()を削除し、クライアントコードで明示的にNewPost()しています。
これにより、UserがPostに依存することがなくなり、
UserにNewXxx()のようなファクトリが多くなることもありません。
UserはPostの存在を知る必要はないのです。

プログラミング言語は日本語を落とし込むことに注力して開発されているわけではありません。
そのためユースケースの日本語をそのままコードに落とし込もうとすると、コードの正しさを失ってしまう可能性があります。
自分はユースケースの日本語とコードとしての具体的なメリット、デメリットを考えて適切なものを選択するようにしています。

ちなみにオブジェクトを生成するパターンは様々です。
そのためUser.NewXxx()のようなファクトリが常にNGということではありません。

無駄にオブジェクト同士の結合度を上げる

ユースケースの日本語を"そのまま"コードに落とし込もうとして失敗する例をもう1つ挙げましょう。
先程の"ユーザーは自分の近況を投稿できる"というユースケースを例に説明します。

先程の実装は以下です。
NewPost()の引数にUser.IDが指定されています。

u := NewUser(100, "pospome")
p := NewPost(u.ID, "message")
repository.InsertPost(p)



しかし、以下のようにintのUser.IDの代わりにUserを指定するようなコードを見かけることがあります。

u := NewUser(100, "pospome")
p := NewPost(u, "message")
repository.InsertPost(p)



しかし、実際にNewPost()内で依存しているのはUser.IDのみです。

type Post struct {
  UserID int
  Body   string
}

func NewPost(u User, body string) *Post {
  return &Post{
    u.ID,
    body,
  }
}

このような実装になる理由は"ユーザーは自分の近況を投稿できる"というユースケースをそのまま表現しようとしたからです。
ユースケースの中にユーザーIDという単語が存在しないので、
ユースケース中に存在する"ユーザー(User)"という単語をそのままコードに落とし込もうとしているのです。

しかし、これはデータ結合からスタンプ結合へと結合度を上げていることになります。
プログラミングにおいて意味もなくオブジェクト同士の結合度を上げる必要はありません。
最初の例のようにNewPost()はUser.IDにのみ引数に指定すべきでしょう。

RubyActiveRecordの正しさ

RubyActiveRecordはDBのテーブルと1対1に対応するモデルを利用します。
そのためActiveRecordを素直に使うとドメインを表現する適切なモデルを利用できるとは限りません。
これは皆さんご存知かと思います。

自分は過去にActiveRecordを利用していた時期があったのですが、
その際にActiveRecordを単なるDAOとして利用し、
モデル層に別途純粋なRubyのオブジェクトとしてモデルを定義しようとしました。
そうすることによって、ドメインを表現する適切なモデルを手に入れようとしたのです。
しかし、実際にチャレンジしてみるとActiveRecordの手軽さを失った感じがしました。
さらにActiveRecordに慣れているエンジニアにこの思想を理解してもらうことが難しいとも感じました。

今回はActiveRecordを例にしましたが、
フレームワークやORMなどの技術としての正しさを優先することで結果的に良いコードになることがあります。
ドメインを表現する適切なモデルが効果を発揮するケースもあると思いますが、常にそうとは限りません。
技術としての正しさの中で可能な限りDDDの良い部分を適用していくという戦略も引き出しとして持っていると良いでしょう。

最初から将来必要になるであろう設計を予想するのは難しいので、
辛くなったときにリカバリできるような仕組みを構築した方がよいと思います。
現状に最適な選択肢を選び、辛くなったらリファクタリングしましょう。

コードとしてのメリット、デメリットを具体的に考えて解決する

"DDDではリポジトリパターンを使うべきなので使いましょう"というのは論理的な決断ではありません。

  • なぜリポジトリパターンが必要なのでしょうか?
  • 採用しなかった場合、具体的にどのようなメリット、デメリットがあるのでしょうか?

常にコードとしての具体的なメリット、デメリットを考えましょう。
非論理的な決断はチームメンバーが理解できない可能性が高く、良し悪しの判断が属人化します。
中長期的に考えるとコードの品質を保証することは難しくなるでしょう。
それどころかチームメンバーにDDDに対する変な印象を与えてしまいます。
そもそも、そのような発言をしている本人が具体的なメリットを理解していない可能性すらあります。

コードとしての正しさを抑えておくとメリットが明確になるため、
チームメンバーからの理解を得やすいですし、
人の入れ替えがあっても意図が伝わるコードになっているはずです。

まとめ

DDDは設計手法なのでコードとしての正しさを考える必要があるというお話をしました。
自分は今も今までもたくさん失敗しています。
そのたびに"コードとしての正しさ"と"具体的なメリット、デメリット"を考えるべきだと痛感しています。
コードとしての正しさを守った上でDDDの実装パターンや概念を追加すると、よりコードの可読性が高くなるはずです。
"DDDだから"という考え方は捨てて、コードとしての正しさを考慮し、具体的な問題を考え、より良いコードを目指していきましょう。