業務中ワイ
ワイ「こないだ見た」
ワイ「ドメイン駆動設計 ボトムアップでわかる! ドメイン駆動設計の基本」
ワイ「っていう本おもろかったな~」
ワイ「ワイもドメイン駆動設計やってみたいわ~」
ワイ「まずは、なんか簡単な案件で値オブジェクトとか導入してみたいもんやな~」
そんなとき社長に呼ばれて
社長「おーい、やめ太郎くん」
ワイ「なんでっか?」
社長「新しいお仕事持ってきたで」
社長「以前、うちで作ったECサイトのリファクタリング案件や」
社長「なんでも、注文クラスの注文数を値オブジェクトにしたいちゅうことらしいわ」
ハスケル子「(!??)」
ワイ「おっ、ほんまでっか!!」
ワイ「ワイに任しておいて下さいやで」
ちょうど良い案件が舞い込んできた
ワイ「いや〜」
ワイ「値オブジェクトの練習にお誂え向きな」
ワイ「シンプルな案件が舞い込んでくるもんやな〜」
ハスケル子「(ちょっと案件の設定が雑過ぎるんじゃ・・・)」
ワイ「ほな、リファクタリングしていこか〜」
注文クラスを見てみる
ワイ「まずは、注文クラスを見ていくで↓」
/**
* 注文
*/
class Order {
private quantity: number; // 注文数
// ~ 省略 ~
}
ワイ「ん~、ほんまやな~」
ワイ「な○せさんの言う通りやで」
ワイ「単純なプリミティブ型やと」
ワイ「かろうじて数値ってことしかわからへんもん」
ワイ「注文数の仕様(業務ルール)を知りたくなったら」
ワイ「どこでどんな使われ方してるか把握する旅に出なアカンわ」
注文数の仕様(業務ルール)を把握する旅に出る
ワイ「ほな GoTo Travel していこか」
ワイ「えーっと、注文数の仕様(業務ルール)がどんなんかって言うと」
ワイ「まず...」
// 1~99個であれば処理を続けるで
if (1 <= order.getQuantity() && order.getQuantity() <= 99) {
}
// 1~99個であればおkやで
if (0 < this.quantity && this.quantity < 100) {
}
ワイ「↑こんなんが書いてあるから」
ワイ「注文数は、1~99個まで」
ワイ「ちゅうことがわかる」
ワイ「そんで...」
const newQuantity = order.getQuantity() + addQuantity;
order.setQuantity(newQuantity);
ワイ「↑こんなコードがあるから」
ワイ「注文数同士は、加算することができる」
ワイ「ってことやな」
ワイ「あとは...」
const newQuantity = order.getQuantity() - subQuantity;
order.setQuantity(newQuantity);
ワイ「こんな部分もあるから」
ワイ「注文数同士は、減算することができる」
ワイ「ってこともわかったで」
ワイ「まとめると」
ワイ「こんな感じの仕様(業務ルール)ってことやな↓」
注文数の仕様(業務ルール)のチェックリスト
- 注文数は、1~99個まで
- 注文数同士は、加算することができる
- 注文数同士は、減算することができる
ワイ「まあー普通に考えて」
ワイ「注文(コンテキスト)における注文数(の概念)もこんな感じやな」
ワイ「注文数の上限はそれぞれやと思うけど」
ワイ「1個からやないと注文できんのが当たり前やし」
ワイ「足したり、引いたりできなアカンわな」
ワイ「にしても酷いコードやな~」
ワイ「注文数に関することがあっちこっちに書いてあるせいで」
ワイ「色んなとこ見なアカンかったわ」
ワイ「誰やこんなクソコード書いたの」
ワイ「こんなん GoTo Travel やなくて GoTo Hell やないかいっ!」
ハスケル子「それ、先輩が書いたコードですけどね」
ワイ「」
注文数を値オブジェクトにしていく
ワイ「そ、それはさておき」
ワイ「さっそく注文数を値オブジェクトにしていくで」
ワイ「まずは、注文数クラスを定義して」
ワイ「そこに注文数を保持するためのメンバ変数を用意する↓」
/**
* 注文数
*/
class OrderQuantity {
// 注文数を保持するためのメンバ変数を用意
private value: number;
}
ワイ「そんで、コンストラクタで注文数の初期値を与えて」
ワイ「メンバ変数を初期化するんやったな↓」
/**
* 注文数
*/
class OrderQuantity {
private value: number;
// コンストラクタで注文数の初期値を与えて
constructor(value: number) {
// メンバ変数を初期化
this.value = value
}
}
ワイ「ほんで、中身を確認するためのアクセサを定義して↓」
/**
* 注文数
*/
class OrderQuantity {
private value: number;
// アクセサを定義
public get Value(): number { return this.value; }
public set Value(value: number) { this.value = value; }
constructor(value: number) {
this.value = value
}
}
ワイ「おっと!たしか、」
ワイ「setterは内部の変更を容易にするから」
ワイ「安易に定義したらアカンて偉い人が言うてたな」
ワイ「せやから、setterは削除するで↓」
/**
* 注文数
*/
class OrderQuantity {
private value: number;
// getterのみを定義
public get Value(): number { return this.value; }
constructor(value: number) {
this.value = value
}
}
ワイ「で、使うときはこんな感じやな↓」
// 注文数が10個
const quantity = new OrderQuantity(10);
console.log(quantity.Value); // 10が出力される
ワイ「うんうん」
ワイ「ええ感じやで」
不正値が存在できる構造になっていた
ハスケル子「やめ太郎さん」
ワイ「なんや、ハスケル子ちゃん」
ハスケル子「これだと不正な注文数が存在できる構造になってますよ」
ハスケル子「例えば、こんな感じです↓」
// 不正な注文数を生成できてしまう
const quantity = new OrderQuantity(-10);
注文数の仕様(業務ルール)のチェックリスト
- 注文数は、1~99個まで ← コレに違反しやすい構造になっている
- 注文数同士は、加算することができる
- 注文数同士は、減算することができる
ワイ「ほんまやで...」
ハスケル子「なので」
ハスケル子「完全コンストラクタパターンを適用しないとです」
完全コンストラクタパターンを適用する
ワイ「おお、せやった!せやった!」
ワイ「完全感覚パターンを適用するんやったな」
ワイ「で、完全感覚パターンってなんやったっけ?」
ハスケル子「完全コンストラクタパターンです」
ハスケル子「newした時点で正しく利用できる完全な状態になるよう」
ハスケル子「適切な初期化ロジックをコンストラクタに実装するパターンです」
ハスケル子「まぁーたしかに、やめ太郎さんって」
ハスケル子「何となくでコーディングしてる節がありますけどね」
ワイ「ふぁ!?」
ワイ「何となく感覚だけでコーディングできるワイって」
ワイ「もしかして、天才と違うか?」
ワイ「違うな」
ハスケル子「やめ太郎さん」
ハスケル子「今回の注文数における正しく利用できる完全な状態って何だと思いますか?」
ワイ「えーっと」
ワイ「さっき調べた注文数の仕様(業務ルール)がこんなんやから↓」
注文数の仕様(業務ルール)のチェックリスト
- 注文数は、1~99個まで
- 注文数同士は、加算することができる
- 注文数同士は、減算することができる
ワイ「注文数が1~99個まで」
ワイ「であることやな」
ハスケル子「そうですね」
ハスケル子「なので、こんな感じになるかと思います↓」
/**
* 注文数
*/
class OrderQuantity {
// コンストラクタ以降は再代入を禁止にしてより安全に
// =不変(イミュータブル)にする
private readonly value: number;
public get Value(): number { return this.value; }
constructor(value: number) {
// 不正値は例外をスローして排除
if (!isValid(value)) {
throw new RangeError("不正: 範囲外");
}
this.value = value;
}
// 注文数のバリデーションを用意
private static isValid(value: number): boolean {
// 1~99個までという仕様(業務ルール)を表現
return (1 <= value) && (value <= 99);
}
}
ワイ「えーっと...」
/**
* 注文数
*/
class OrderQuantity {
// コンストラクタ以降は再代入を禁止にしてより安全に
// =不変(イミュータブル)にする
private readonly value: number;
// ~ 省略 ~
}
ワイ「↑ここを見ると」
ワイ「メンバ変数のvalue
をreadonly
にして」
ワイ「コンストラクタ以降の変更ができんよう**不変(イミュータブル)**にしたんやな」
ワイ「そんで...」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
// 注文数のバリデーションを用意
private static isValid(value: number): boolean {
// 1~99個までという仕様(業務ルール)を表現
return (1 <= value) && (value <= 99);
}
}
ワイ「↑この部分では」
ワイ「注文数が1~99個までっていう仕様(業務ルール)を表現する」
ワイ「バリデーションを用意して」
ワイ「ほんで...」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
constructor(value: number) {
// 不正値は例外をスローして排除
if (!isValid(value)) {
throw new RangeError("不正: 範囲外");
}
this.value = value;
}
// ~ 省略 ~
}
ワイ「↑コンストラクタを見ると」
ワイ「さっきのisVaild()
で引数のvalue
を判定して」
ワイ「1~99個までであれば、メンバ変数に格納」
ワイ「そうでなければ、例外をスローするわけやな」
ワイ「なるほどな」
ワイ「これで、さっきみたいに不正値が渡されると例外をスローするわけやから」
ワイ「このアプリケーション内で不正な注文数のインスタンスが存在できなくなるんやな」
// 1~99個であれば処理を続けるで
if (1 <= order.getQuantity() && order.getQuantity() <= 99) {
}
ワイ「↑こんな感じで不正値かどうかを気にしながらコーディングするんやなくて」
ワイ「そもそも、不正値が存在できない仕組みを作り出したっちゅうわけか!」
ワイ「ちゅうことで」
ワイ「進捗としてはこんな感じやな↓」
注文数の仕様(業務ルール)のチェックリスト
- 注文数は、1~99個まで
- 注文数同士は、加算することができる
- 注文数同士は、減算することができる
残りを実装していく
ワイ「ほな、残りを実装していくで↓」
注文数の仕様(業務ルール)のチェックリスト
- 注文数は、1~99個まで
- 注文数同士は、加算することができる ← コレと
- 注文数同士は、減算することができる ← コレ
ワイ「まずは、注文数同士を加算するためのメソッドを作っていこか」
ワイ「メソッド名は加算するんやからadd
やな」
ワイ「ほんで、加算する注文数を引数で受け取って、自身と引数のメンバ変数同士を加算する↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
// 加算する注文数を引数で受け取る
public add(other: OrderQuantity) {
// 自身と引数のメンバ変数同士を加算
const addValue: number = this.value + other.value;
}
}
ワイ「そんで、その結果を持つ新しい注文数(のインスタンス)を生成して返す↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
public add(other: OrderQuantity): OrderQuantity {
const addValue: number = this.value + other.value;
// 新しい注文数(のインスタンス)を生成して返す
return new OrderQuantity(addValue);
}
}
ワイ「メンバ変数のvalue
はreadonly
で不変(イミュータブル)やから」
ワイ「新しく注文数のインスタンスを作り直すことで」
ワイ「状態を不変(イミュータブル)に保ちつつ、値を変更(交換)できるようにするっちゅうわけや」
ワイ「ほんで、加算した結果がうっかり99個を超えてしまわへんように」
ワイ「ここでもちゃんとバリデーションしておくで↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
public add(other: OrderQuantity): OrderQuantity {
const addValue: number = this.value + other.value;
// 加算した結果が不正値である場合は例外をスローして排除
if (!isValid(addValue)) {
throw new RangeError("不正: 加算した結果が範囲外");
}
return new OrderQuantity(addValue);
}
}
ワイ「で、使うときはこうやな↓」
const quantity1 = new OrderQuantity(10);
const quantity2 = new OrderQuantity(20);
const result = quantity1.add(quantity2);
console.log(result.Value); // 30が出力される
ワイ「進捗としてはこんな感じや↓」
注文数の仕様(業務ルール)のチェックリスト
- 注文数は、1~99個まで
- 注文数同士は、加算することができる
- 注文数同士は、減算することができる
ワイ「ほな、注文数同士を減算するためのメソッドも同じように実装してくで」
ワイ「メソッド名は減算するんやからsub
やな」
ワイ「ほんで、減算する注文数を引数で受け取って、自身と引数のメンバ変数を減算する↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
// 減算する注文数を引数で受け取る
public sub(other: OrderQuantity) {
// 自身と引数のメンバ変数同士を減算
const subValue: number = this.value - other.value;
}
}
ワイ「そんで、その結果を持つ新しい注文数(のインスタンス)を生成して返す↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
public sub(other: OrderQuantity): OrderQuantity {
const subValue: number = this.value - other.value;
// 新しい注文数(のインスタンス)を生成して返す
return new OrderQuantity(subValue);
}
}
ワイ「ほんで、減算した結果がうっかり1個未満にならんようにバリデーションする↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
public sub(other: OrderQuantity): OrderQuantity {
const subValue: number = this.value - other.value;
// 減算した結果が不正値である場合は例外をスローして排除
if (!isValid(subValue)) {
throw new RangeError("不正: 減算した結果が範囲外");
}
return new OrderQuantity(subValue);
}
}
ワイ「で、使うときはこうや↓」
const quantity1 = new OrderQuantity(10);
const quantity2 = new OrderQuantity(20);
const result = quantity2.sub(quantity1);
console.log(result.Value); // 10が出力される
ワイ「よしゃ、これで注文数の値オブジェクトの完成やで↓」
注文数の仕様(業務ルール)のチェックリスト
- 注文数は、1~99個まで
- 注文数同士は、加算することができる
- 注文数同士は、減算することができる
概念的にありえる操作のみを定義する
ワイ「って思ったんやけど」
ワイ「注文数同士を加算、減算するメソッドがあるんやから」
ワイ「注文数同士を乗算、除算するメソッドとかあってもええんやないか?」
ワイ「な、ハスケル子ちゃん」
ハスケル子「いえ、必要ないと思いますよ」
ハスケル子「注文数同士の加算、減算は用途上考えられますが」
ハスケル子「乗算、除算はありえないですよね?」
ワイ「たしかに、注文数同士を掛けたり、割ったりすることってないわなあ」
ハスケル子「概念的にありえる操作のみを定義することで」
ハスケル子「不正な操作を許さない堅牢なアプリケーションになるんです」
ワイ「なるほどな~」
ワイ「値オブジェクトにすることで」
ワイ「不正な操作を許さない構造にもなるんやな」
ハスケル子「そうですね」
ハスケル子「単純なプリミティブ型だと汎用的過ぎて」
ハスケル子「概念的にありえない操作(乗算/除算)も許可することになりますからね」
なにがどう良くなった?
ハスケル子「ところで、やめ太郎さん」
ハスケル子「値オブジェクトは完成したみたいですけど」
ハスケル子「値オブジェクトを導入して、なにがどう良くなったか」
ハスケル子「つまりは、値オブジェクトを導入するメリットはちゃんと理解してますか?」
ワイ「そんなん、当たり前やで」
ワイ「まずは」
ワイ「不正値が存在できなくなることやな」
値オブジェクトを導入するメリット
- 不正値が存在できなくなる
ワイ「それから」
ワイ「不正な操作を許さない構造になる」
値オブジェクトを導入するメリット
- 不正値が存在できなくなる
- 不正な操作を許さない構造になる
ワイ「あとは...」
ワイ「ま、そんなもんやな」
ハスケル子「そうですか...」
ハスケル子「やめ太郎さんにしては頑張りましたね」
ワイ「せやろ!!」
ハスケル子「他にも」
- 表現力が増す
- 誤った代入を防ぐ
- ロジックの散在を防ぐ
ハスケル子「などがあげられます」
ハスケル子「まとめると、こんな感じです↓」
値オブジェクトを導入するメリット
- 不正値が存在できなくなる
- 不正な操作を許さない構造になる
- 表現力が増す
- 誤った代入を防ぐ
- ロジックの散在を防ぐ
表現力が増す
ハスケル子「まず、表現力が増すというのは」
ハスケル子「自己文書化を推し進めるということです」
ハスケル子「例えば」
ハスケル子「最初のコードで注文数の仕様(業務ルール)を知ろうとしたとき」
ハスケル子「やめ太郎さんは、どうしましたか?」
ワイ「えーっと...」
ワイ「quantity
っていう文字列をgrepして」
ワイ「色んなクラス渡り歩いて注文数の仕様(業務ルール)調べたで」
ワイ「ホンマにしんどかったわ」
ハスケル子「そうですよね」
ハスケル子「でも、今みたいに注文数が値オブジェクトになっていたらどうですか?」
ワイ「まあー注文数クラスを見にいくなあ」
ハスケル子「ですよね」
ハスケル子「つまり、注文数の仕様(業務ルール)について知りたければ」
ハスケル子「注文数のことが書かれている注文数クラスだけを見れば良いんです」
ハスケル子「もう一度、注文数クラスを見てみましょう↓」
/**
* 注文数
*/
class OrderQuantity {
private readonly value: number;
public get Value(): number { return this.value; }
constructor(value: number) {
if (!isValid(value)) {
throw new RangeError("不正: 範囲外");
}
this.value = value;
}
// 注文数は、1~99個まで
private isValid(value: number): boolean {
return (1 <= value) && (value <= 99);
}
// 注文数同士は、加算することができる
public add(other: OrderQuantity): OrderQuantity {
// ~ 省略 ~
}
// 注文数同士は、減算することができる
public sub(other: OrderQuantity): OrderQuantity {
// ~ 省略 ~
}
// ★★★【重要】ここに書いていない操作はできない★★★
}
ワイ「ほんまやな」
ワイ「ワイが最初に苦労して調べ上げた注文数の仕様(業務ルール)が」
ワイ「全部、注文数クラスに書いてあるわ」
ワイ「ワイは、こういう者(注文数クラス)やで」
ワイ「こんな制約(1~99個)があって」
ワイ「こんな操作(足したり、引いたり)ができますわ」
ワイ「って感じで自己を文書化してるみたいやから」
ワイ「自己文書化っちゅうわけやな」
ワイ「これやったら GoTo Hell やなくて GoTo Travel できそうやわ!!」
ワイ「旅行代金ちゅう時間がワイに還元されるわけやからな」
ハスケル子「ちょっと何言ってるかわかんないです」
ワイ「せやな」
誤った代入を防ぐ
ハスケル子「次に、誤った代入を防ぐということについてです」
ハスケル子「最初のコードみたいに注文数が単純なプリミティブ型である場合」
ハスケル子「全く関係のない値が代入されてしまうという危険が潜んでいます」
ハスケル子「例えば、こんな感じです↓」
/**
* 注文
*/
class Order {
private orderId: number;
private orderProductId: number;
private orderQuantity: number;
constructor(
orderId: number,
orderProductId: number,
orderQuantity: number) {
this.orderId = orderId;
this.orderProductId = orderProductId;
// 注文数に注文商品IDを指定しているが型は同じなので代入できてしまう
this.orderQuantity = orderProductId;
}
}
ハスケル子「あたりまえですが」
ハスケル子「プリミティブ型であるnumber
型は」
ハスケル子「注文数の仕様(業務ルール)なんて知るわけがないので」
ハスケル子「業務ルールに違反した代入操作を未然に防ぐことはできませんよね」
ワイ「なるほどな~」
ワイ「注文数が値オブジェクトとして定義されていれば」
ワイ「OrderQuantity
型にnumber
型の変数を代入することになるわけやから」
ワイ「タイプ不一致で、そもそもコンパイル(トランスパイル)すらできないっちゅうことか!」
ワイ「こんな感じに↓」
/**
* 注文
*/
class Order {
private orderId: number;
private orderProductId: number;
private orderQuantity: OrderQuantity;
constructor(
orderId: number,
orderProductId: number,
orderQuantity: OrderQuantity) {
this.orderId = orderId;
this.orderProductId = orderProductId;
// 注文数に注文商品IDを指定しているが
// タイプ不一致でコンパイル(トランスパイル)すらできない
this.orderQuantity = orderProductId;
}
}
ハスケル子「そういうことですね」
ハスケル子「実行してから初めて気が付くのと、実行する前に気が付くのでは」
ハスケル子「圧倒的に後者の方が安心ですよね」
ワイ「せやな」
ワイ「最悪、気が付かずにそのままリリースってこともあるかもしれんしな」
ハスケル子「値オブジェクトにしておくことで」
ハスケル子「こういったつまらないミスによる不具合を防ぐこともできるんです」
ロジックの散在を防ぐ
ハスケル子「そして、ロジックの散在を防ぐということについてですが」
ハスケル子「最初のコードにこんなのがあったのを覚えてますか?↓」
// 1~99個であれば処理を続けるで
if (1 <= order.getQuantity() && order.getQuantity() <= 99) {
}
// 1~99個であればおkやで
if (0 < this.quantity && this.quantity < 100) {
}
ワイ「おぼえてるで」
ワイ「他にも同じようなのがあっちこっちにあったで」
ハスケル子「つまり、注文数の仕様(業務ルール)である1~99個まで」
ハスケル子「を表現するロジックがあらゆるクラスに散在しているわけです」
ハスケル子「仮に1~99個までという仕様(業務ルール)に変更があったら」
ハスケル子「あらゆるクラスに散在している全てのロジックを修正しないといけませんよね?」
ハスケル子「それに、修正漏れがあると注文数に関する不具合が発生します」
ワイ「せやな~」
ワイ「一つ一つの修正は簡単かもしれんけど、」
ワイ「それが100箇所とかあったら大変やで」
ワイ「値オブジェクトにしておけば」
ワイ「注文数のことは全て注文数クラスに書かれることになるわけやから」
ワイ「注文数の仕様(業務ルール)に変更があっても」
ワイ「注文数クラスだけを修正すれば良いちゅうわけやな」
さっそくの仕様(業務ルール)変更
社長「おーい、やめ太郎くん」
ワイ「なんでっか?」
社長「さっきのリファクタリング案件なんやけど」
社長「先方から仕様変更したい言われててな」
社長「なんでも、注文数を1~999999999個にしたいらしんやわ」
ワイ「ふぁ!?」
ワイ「どんだけ注文できるようにすんねん」
社長「まあーそういうことやから頼むで」
ワイ「お、おう...」
仕様(業務ルール)変更に対応する
ワイ「えーっと...」
ワイ「注文数の仕様(業務ルール)を変更するわけやから」
ワイ「注文数クラスを見ればええんやったな↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
private static isValid(value: number): boolean {
return (1 <= value) && (value <= 99);
}
// ~ 省略 ~
}
ワイ「そんで、注文数の上限を999999999個に修正すればええんやから」
ワイ「こうやな↓」
/**
* 注文数
*/
class OrderQuantity {
// ~ 省略 ~
private static isValid(value: number): boolean {
// 1~999999999個に修正
return (1 <= value) && (value <= 999999999);
}
// ~ 省略 ~
}
ワイ「よっしゃ、これで仕様(業務ルール)変更の対応おわりやで」
ワイ「値オブジェクトのおかげで修正めっちゃ楽やったわ」
ワイ「これが、最初のコードやったら」
ワイ「こうは簡単にいかへんかったやろうなー」
ワイ「値オブジェクトさまさまやで~」
まとめ
ワイ「いや~」
ワイ「値オブジェクトって良いことだらけやで」
ワイ「不正な値は存在できへんし」
ワイ「不正な操作も許さへん」
ワイ「表現力も増すから、ある概念の理解が容易になるし」
ワイ「誤った代入も実行前にわかるようになって、思わぬ不具合もなくなる」
ワイ「ロジックの散在も防いでくれて、修正も楽ときたもんや」
ワイ「これからは、積極的に値オブジェクト使っていくで!」
ワイ「ほな、会社のみんなにも教えたろー」
ハスケル子「やめ太郎さん」
ハスケル子「うちで実践してないの、やめ太郎さんだけですよ?」
ワイ「ぴえん」
~おしまい~
参考文献
ワイ「もっとちゃんと学びたい人は以下をみるとええで↓」
ワイ「ほな」