171
127

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DDD に入門するなら、まずは ValueObject だけでもいいんじゃない?

Last updated at Posted at 2019-12-10

今日は 『ドメイン駆動設計#1 Advent Calendar 2019』の 11日目 です。

昨日は mejileben さんの 『Laravelでドメイン駆動設計を実践し、Eloquent Model依存の設計から脱却する』 でした。


みなさん初めまして、こんばんは!
C# をこよなく愛する静岡エンジニアの t2-kob です。

  
本日のテーマは、
  「DDD に入門するなら、まずは ValueObject だけでもいいんじゃない?
です。

  

■ なぜこの記事を書いたか?

・理由の1つ目は、勉強のためアウトプットしたいと思ったからです。

最近 DDD コミュニティの DDD-Comunity-jp(Discord) に参加して、色々なオンライン勉強会に参加させて頂いています。
この勉強会へは色々下調べして臨んでいますが、その後の振り返りが出来ていませんでした。
このため、勉強を兼ねてアウトプットをしようと思い立ちました。

  
・理由の2つ目は、DDD の超入門記事って実はそんなにないなぁ、と思ったからです。

領域が広すぎて、どこから初めて良いのか分からない人が多い予感がしています(私もその一人です)。

このため今回は、特に2つ目をテーマにして、どこから始めるといいか考えてみたいと思います。
※ この記事では私の愛してやまない C# をサンプルコードに使用します。


■ 正直、DDD って難しすぎない?

DDD を始めるにあたっては色々な情報があります。
DDD本、IDDD本、素敵な入門記事、Discord の DDD-Comunity-jp(オススメ)、etc...

  
方法はいろいろとありますが、正直、どれも難しいです。
概念も日本語も難しい。調べても調べてもよくわかりません。

個々の要素を見れば、なんとなく言っていることはわかる気がする。
…でも、どうやって自分の触れているプロダクトに組み込むんだろう?

  
もしもDDDが簡単お手軽に導入できて超絶的な効果が得られるのであれば、
DDD はもっともっと普及しているはずです。

そうならないのは、乗り越えるべきハードルがとても高いから。
定義や言葉がフワッとしていて理解するのも大変、身につけるのも大変。
適用するのも大変だし、導入できるタイミングも限られているし、上司・同僚・お客さんを同じバスに乗せるのも大変です。
なんだか実例も少ないし、正直イメージも湧かない―――――。

であれば、まずは DDD の中から、
身近に取り入れられる手法に目を向ける、というのもひとつの方法ではないでしょうか。

  
そう、つまりは軽量DDDです。

  
 

■ 軽量DDD?

こんなことを書くと DDD 界隈の先輩方のマサカリで首と胴が別れてしまうかもしれませんが、
私は「本格的なDDDの敷居が高いのであれば、まずは軽量DDD から始めたらどうだろう?」と考えています。

軽量DDD とは、DDD にまつわる様々な考え方のなかから、
その手法の一部だけを使うアンチパターンです。

...

アンチパターンじゃ嫌だ? 全部身につけてからの導入?

でもそれって、いつになったら出来るんだろう―――。

  
もちろん思想まで完璧に身に着けてから実践できれば最高だけど・・・
初めて DDD を学びたいと思った時点では、とてもとても難しい。

なら、まずは簡単なものから始めてみませんか?
もっと詳しく覚えるのはそのあとでも問題ないんじゃないかな、と思います。

一見は百聞にしかずって言いますし。
小手先の技術でも、きっと DDD の考え方のほんの一端に触れるくらいはできるでしょう。

  
今回はそんな技術の中から、Value Object について取り上げます。
(おそらく、これが取っ掛かりとして最もわかりやすく、導入の難易度も低いです。)

そのうち罠に嵌る日が来ると思いますが、そうしたらまた勉強すれば良いのです。

  
※ そもそも Value Object を導入しただけでは軽量DDDとすら呼べないかもしれませんが、
  その点は今回はスルーします。大切なのは心意気ということでご勘弁願います😲

  
 

■ まずは Value Object から。

Value Object とは、DDD の考え方の中の一種 で、なにか 特定のひとつの概念を表したクラス です。
たとえば UserIdSurName(名字) クラスといった感じです。
このクラスを、今まで使っていた intstring といった プリミティブの代わりに使用 します。

// いままで
int userId; 
string surName;

// これから
UserId userId;
SurName surName;

  
なお ValueObject は、日本語では「値オブジェクト」と呼びます。

Value Object を導入することによるメリット は、

  • 可読性の向上
  • 値の取り違えによるバグの減少(異なる概念が混ざらなくなる)
  • 修正漏れが減る
  • 不正な値が存在しなくなる
  • 初期化していない値によるバグがなくなる
  • 上記の結果、生産性が向上する

などです。

  

バラ色の未来ですね!🌹

  
デメリットは、クラス量とコード量が増えることです。

コード量は少ないほうがいい?
なら1行プログラミングでも目指せば良いんじゃないかな。

正しい理由で増えたのであれば、コード量は多くてもOKです。
それが正しいのであればそうすべきです。
 

■ 【今までの問題点】 要素をプリミティブで管理するコードの悲しみ

レガシーコードでは、hogeIdfugaNameといった様々な値は、
一般的にstringint などの プリミティブで定義されていました。

これらの変数を書き換えたり、インスタンスの変数を操作する、
または複数の値を取り扱うなどの行為によって「やっちまった」り、被害を受けたことはないでしょうか?

こう言いかえることも出来ます

  • 「誰だこの変数書き換えたやつ!」
  • 「いつどこでこの値になったか分からん・・・」
  • 「やべぇ、messageId の引数に userId 渡してバグ出してしもた💀」

 
言いかえるとイメージしやすいですね。
おそらく、多くの方が何らかの形で実体験しているのではないでしょうか?

このあとどうなるかはもうお分かりですよね。
ステップ実行しながら、お目当ての状態になるまで1行1行確認していくわけです。

くそ、どこでどんな条件の時、値が null になるのかマジわからん・・・

  
つらい・・

 

そもそもなんで値を書き換えるんだろう?

値を書き換えるってことは、異なる概念に変わっているはずだよね?
最低でも、変数名は違っているはずだ・・・

でもレガシーコードでは(得てして) そうは書かれていない😇

// C# によるレガシーコードのイメージ
public double CalcTotalPrice() {
    double price = 300;
    price = price * 1.10; // あれ、消費税も価格に含めたのに変数名は price のまま?

    // 中略...

    // 忘れたころにもう一回消費税掛けちゃって不具合に。
    return (price * 1.10);
}

 

ああ、残業確定だ。。

 

■ これらの問題を解決する Value Object とは?

これらの、何かの概念を表すプリミティブな変数や値を、
生まれた瞬間完成している、値を変えられないクラスで表す」のが、
Value Object (値オブジェクト) です。

 
Value Object の 核となる考え方 は、

  1. 値をクラスにすること
  2. 生まれた瞬間完成していること (不正な値を持てないこと)
  3. 値がライフタイムの間、不変であること (副作用がないこと)
  4. 値を変更したくなったら、値オブジェクトごと置き換えること

の4つです。 (※ 一般的には他にも考え方がありますが、特に重要なのはこれらと捉えています。)

  
生まれた瞬間完成しているから null に悩まされることもありません。

クラスが不変な値の変えられないものになっていれば、
そもそも値は書き換わらないから不正な値にならないし、
変数の再代入によって気が付かないうちに異なる概念になっていることもない。

クラスになっているから、もう messageId と userId を取り違えることもないのです。
間違えてもコンパイラが怒ってくれます。 (静的型付け言語バンザイ!)

 
他にも

  • Value Object 同士を比較できるようにすること
  • (できれば) ひとつの値オブジェクトの中に、値は1個とすること
  • Value Object の表す概念と密接に紐づく計算が可能であること

などといった重要な考え方もありますが、
まずは 先に述べた4つを抑えておいてください

  
いずれにしても、『生まれた瞬間完成していて、値が変更できないクラス』 を作ることが大切です。

ValueObject の表す概念に必要なメソッドを生やすのはその後で構いません。
※ 最終的に、Value Object の内包する値に対して Add()Remove() などのメソッドが追加されていきます。

■ Value Object の考え方の詳細について

 
ここからは ValueObjectの核となる4つの考え方の詳細について記載していきます。

□ ValueObject (1) 値をクラスにすること。

□ ValueObject (2) 生まれた瞬間完成していること。

いままでプリミティブで扱っていた『値』をクラス化します。

あとから値を Set すると初期化漏れが発生しやすいですし、
値を書き換えられれると不正な数値にされてしまったり、異なる概念(※) が混ざり込んでしまう可能性があります。
(※ 商品価格として扱っていた値に気がついたら消費税が掛けられていたなど。)

このため、生まれた瞬間に完成していて欲しい かつ 値を変えられたくない ので、
クラスを new して作るときに、必要な値をすべて渡します
また絶対に public な変数やプロパティSetterpublic な Initializeメソッド を公開してはいけません。

// 生まれた瞬間完成している。
public class MessageId {
    private readonly int _value;
    
    public MessageId(int value) {
        _value = value;
    }
}

 
また必要に応じてバリデーションを行い
未初期化の値や、不正な値を持つことを許しません

ここでの「不正な値」とは、その値オブジェクトが成立しなくなってしまうような値 を指します。

たとえば UserId 値オブジェクトであれば、普通は 1 から始まることが多いので、
0以下の値 をコンストラクタにぶち込まれたら不正値として弾いておきたいですよね。
「商品価格」値オブジェクトであれば、一般的にはマイナスの値となることはないでしょう。

一言で言うと null 絶対許さないマン です(初期値も許さないけど)。

// null と初期値、不正値を絶対許さないマン。
public class MessageId {

    // 書き換え不能な変数。
    // C# の場合、readonly や Getter only プロパティで実現。
    // readonly struct で表現するのもよいかも。
    // ※ コメントでご指摘いただきましたが、struct は残念ながら推奨出来ませんでした。
    private Value { get; }
    
    // (※ int? ==> null許容型)
    public MessageId(int? value) {

        // 存在してはいけないインスタンスは、そもそも作れないようにする。
        // null を取り得るなら、null も弾きます。
        if (value <= 0 || value == null) {
            throw new InvalidArgumentException(nameof(value));
        }

        Value = value;
    }
}

 
 
 
では、なぜ null や初期値を許さないのでしょうか?
その答えは、null に代表される初期状態が とてもとてもとてもとても厄介 だからです。

null で死んでくれれば実はまだマシなほうで、
初期値が 0 なので不正な値のままなんとなく動作してしまった というのが、
最高に最低な問題かつ、稀によく起きるという都市伝説として知られています。

null が DB に保存された結果、時限式で爆発する という ボンバーマンの存在 も確認されています。

□□□□
□💣 □ 
□😱□□
□□□□

...

いずれにしても、気がついたときにはデータに不整合が発生していて、残業が確定します。

   
しかしValue Object を使えば、そもそも不正な値オブジェクトは生成すらされないため、
0 や null などの初期状態を考慮しなくてもよくなります

  
もうこれで、ぬるぽしてガッ とされることもなくなります(最近聞きませんね)。
使う側で毎回 if (hoge == null) { ... } しなくていいし、うっかり忘れたり知らずにぬるぽることもありません。
同様に、初期値のままでないことが保証されるので、if (fuga != 0) { ... } も不要になるのです。

 
どうでもいいですが、C# の場合は ぬるり(NullReferenceException) なんですが、
ぬるりってイマイチ聞かないような気がしています。

好きなんだけどな、ぬるり って響き。

ぬるり。

  

□ ValueObject (3) 不変であること

□ ValueObject (4) 値を変更したくなったら、値オブジェクトごと置き換えること

繰り返しになりますが、new したあとに 変数や Setter、メソッドを通じて値を書き換えてはいけません。
それが自身の処理であっても、値の変更は許しません。

したがって、絶対に public な変数やプロパティSetterpublic な Initializeメソッド を公開してはいけません。

  
この不変性が破られると、誰かが間違って値を書き換えてしまって、
クラス名と中身の概念が異なるものになってしまうかもしれませんし、
不正な値をぶち込まれて夜な夜なデバッグすることになるかもしれません。帰りたい。

例えば、外から消費税率のプロパティを 110 に書き換えられられたとします。
しかし実は仕様が「倍率」を入力する仕様だったとしたら・・・?
100円の商品が 110倍 されて、なんと 11000円 となってしまいます
―――訴えられないことを祈りましょう。

  
・・・自分はそんなことしないって?
副作用のある振る舞いは「見える罠」なんです。
あなたはしないかもしれませんが・・・残念ながらいつか誰かが踏み抜きます。忘れた頃に自分で踏むかもしれません。
だって疲れていたらダメと分かっていても、つい使いたくなっちゃうよね。

  
ダメコードの例は以下のような感じです。

// Setter が公開されていることにより予測される悲劇。
public class PriceWithSalesTax {
    private readonly int _value;

    // 例えば Setter が公開されていたとして…
    public double SalesTax { private get; set; }

    public PriceWithSalesTax(int value) {
        _value = value;
    }
    
    // 合計値金額を求めるメソッド。
    public int CalcTotalPrice {    
        // もし SalsesTax に 110 が外から突っ込まれたら、とんでもない税率になる。
        // 100円 * 110 = 11000円!  (本当は 100 * 1.1 だったのに)。
        //
        // また SalesTax の初期化を忘れると、初期値 0 なので 100円 * 0 で無料になる。
        get => _value * SalesTax;
    }
}

 
ちょっとでも油断すると、Setter に変な値を突っ込む輩が出てきます。
あるいは自身を書き換えたため、いつどのタイミングで書き換えられて現在の結果になったのか分からなく&追えなくなります。

このため、何が何でも外からも内からも値を変更させたくありません。
したがって、値を書き換えたくなったら自分のコピーを作って返す ことで不変性を守るようにします。

// 消費税込みの合計金額クラスがあったとき...
public class TotalPriceWithSalesTax {

    private readonly Price _value;
    private readonly Amount _amount;

    // 消費税率(例なので軽減税率には対応しません)
    private const double SalesTax = 1.1;

    public TotalPriceWithSalesTax(Price value, Amount amount) {
        _value = value;
        _amount = amount;
    }

    // 個数を追加する場合、自身の Amount を書き換えるのではなく、
    // 自身をコピーしたものをベースとして作り直すことで、別の値として返す。
    public TotalPriceWithSalesTax Add(Amount addAmount) {
        Amount newAmount = _amount.Add(addAmount);
        return new PriceWithSalesTax(new Price(_price), newAmount);
    }
)
    // 合計金額の数値が欲しければ、例えばこんなふうに計算する。
    public int TotalPrice {
        get => _value.Value * _amount.Value * SalesTax;
    }
}

 
このように新たなオブジェクトを返すことで、
そもそも不正な値はコンストラクタで弾かれる ので、不正な演算をしてしまう余地もなくなります。


少し話は変わりますが、実はそもそも『消費税』という業務に関わる知識(ドメイン知識) を、
外から自由に変更できてしまうこと自体、実はうまく設計ができていない証拠です。

『外から書き換えることができる』ということは、
消費税クラスだけが知っていれば良い『消費税に関するビジネスルール』が外に漏れ出している、
つまり 「消費税クラスではない他のクラスが、消費税に関するビジネスルールを知ってしまっている」 状態であり、
様々な場所に消費税に関するロジックが散らばりやすくなっています。

このような状態では、どこを見たら消費税に関するビジネスルールやロジックが存在するのか類推することが困難 になります。
またやっと見つけたロジックたちを漏れなくすべて把握しつつ、矛盾がないように修正しなくてはなりません。
不具合の確認も修正も、難易度が跳ね上がってしまいます。

また漏れ出たビジネスルールが他のルールと相互作用を持ってしまっている可能性もあります。
すべて確認して修正しようにも、目も当てられない結果になるかもしれません。
THE スパゲッティコード ってやつですね。私はチーズだけで作った本家カルボナーラが好きです。

  
また、「Setter を用意することで消費税が 8% から 10% に変わったときのように、変更にすぐ対応できるようにしている」といった趣旨の言い訳 を聞くこともあります。
一見聞こえは良いのですが・・・ それならもっと他の方法があります。
わざわざリスクをニンニクアブラヤサイマシマシにする方法を取る必要はありません。
(最低限、生成時に選ぶ仕組みにするか、そもそもクラス自体を分けます。)

ふつう買い物中に消費税を10% → 120% → 25% のように自由に変えることはありませんよね?
なぜ消費税に関わるビジネスルールを、外のクラスが Set するのでしょうか?
ビジネスルールが消費税クラスにまとまっていれば、消費税クラスを確認するだけで済むのに・・・。

■ まとめ

さて、時間も迫ってきたので強引にまとめに入ります。

Value Object とは、『生まれた瞬間完成していて、値が変更できないクラス』 のことです。

その核となる考え方 は、

  1. 値をクラスにすること
  2. 生まれた瞬間完成していること (不正な値を持てないこと)
  3. 値がライフタイムの間、不変であること (副作用がないこと)
  4. 値を変更したくなったら、値オブジェクトごと置き換えること

の4点です。

Value Object を導入すると、

  • 可読性の向上
  • 値の取り違えによるバグの減少(異なる概念が混ざらなくなる)
  • 修正漏れが減る
  • 不正な値が存在しなくなる
  • 初期化していない値によるバグがなくなる
  • 上記の結果、生産性が向上する

といった良い効果が見込まれます。

  
どうでしょう、ちょっとだけ使ってみたくなりましたか?

まずは試してみて、行けそうだなと思ったら少しずつトライしてみてはいかがでしょうか。
シンプルなので自分の担当するプロダクトにも取り入れやすいです。
それに 値を間違えないことが保証されるって、本当に良い ですよ。

  
もし慣れてきたら、次の一歩として、
Value Object を束ねる Entity や、その永続化や読み込みなどを行う Repository をおすすめします。

モデリングを試してみるのもよいと思います。
普段やっている設計とは一味違って、とても楽しいですよ!

  
※ 省きまくったのに約11000字になりました(白目)

171
127
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
171
127

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?