じぶん対策

日々学んだことをアウトプットして備忘録にしています。

JavaScriptにおけるクラスとTypeScriptにおけるクラスについて

はじめに

今回はES2015から変更されたJavaScriptオブジェクト指向構文についてまとめたいと思います。
また、最近TypeScriptについて個人開発等で導入し始めているので違いを意識しながら理解していきたいと思います。

参考書籍

今回の記事は今読んでいるJavaScriptおよびTypeScriptの書籍を比較しながら書きました。

改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで
プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで

JavaScriptにおけるクラス構文

class Member {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // メソッド
    getName() {
        return this.lastName + this.firstName;
    }
}

let m = new Member('太郎', '山田');
console.log(m.getName());

class命令

他のオブジェクト言語に近い形でclass命令を書くことができます。

class クラス名 {
    // コンストラクターの定義
    // プロパティの定義
    // メソッドの定義
    メソッド名(引数) {
        メソッドのロジック
    }
}

コンストラクターの名前はconstructorで固定です。

他言語との違いとして、public/protected/privateのようなアクセス修飾子は利用できない点に注意が必要です。 JavaScriptにおいてはクラスのすべてのメンバーがpublic、つまりどこからでもアクセスできるようになります。

無名クラス(匿名クラス)について

少し特殊な書き方で、無名クラスと呼ばれる書き方ができます。
リテラルなので、関数リテラルと同じく、式の中で利用できます。

let Member = class {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    getName() {
        this.lastName + this.firstName;
    }
}

let m = new Member('太郎', '山田');
console.log(m.getName());

また、無名クラスを定義して即時newすることもできます。
クラスのインスタンスが生成されるため、変数に代入することも可能です。

new class {
    constructor(name) {
        console.log('hi!', name);
    }
}
('Yamada');

使いみちについてはぱっと思いつきませんが、こういう書き方ができると知っておくだけでいつか役に立つかも、、、?

class命令は内部的には関数である

class命令で定義されたクラスは内部的に「特別な関数」として扱われます。
つまり、ES2015にていわゆるクラスが導入されたわけではなくあくまで「これまでFunctionオブジェクトで表現していたクラス(コンストラクター)」をよりわかりやすく表現できるようになったに過ぎません。
ただし、class命令によって定義されたクラスはFunctionオブジェクトによるクラスと完全には等価ではなく、どの部分で違いがあるのかについてこれから見ていきます。

  1. 関数としての呼び出しはできない

例えばclass命令で定義されたMemberクラスを以下のように呼び出すことができません。

let m = Member('太郎', '山田');

functionでのクラスの表現の場合は呼び出せてしまうため、以下のように対策する必要がありました。

let Member = function(firstName, lastName) {
    if(!(this instanceOf Member)) {
        return new Member(firstName, lastName);
    }
    this.firstName = firstName;
    // 以下略
};

上記は、コンストラクターが関数として呼び出された場合にthisがMemberオブジェクトではなくグローバルオブジェクトになる性質を利用してthisがMemberオブジェクト出ない場合に改めてnew演算子コンストラクターを呼び出しています。

  1. 定義前のクラスを呼び出すことはできない

以下のようなコードを書くことはできません。(function命令の場合は呼び出せます)

let m = new Member('太郎', '山田');
// Member is not defined.
class Member {...中略...}

class命令によるプロパティの定義

classブロックにおいて、get/set構文を使ってプロパティを定義することもできます。

class Member {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // プロパティの定義
    get firstName() {
        return this._firstName;
    }

    set firstName(value) {
        this._firstName = value;
    }

    get lastName() {
        return this._lastName;
    }

    set lastName(value) {
        this._lastName = value;
    }
    // メソッドの定義
    getName() {
        return this.last.Name + this.firstName;
    }
}

let m = new Member('太郎', '山田');
console.log(m.getName()); // 山田太郎

他の言語のプロパティ定義のようにlet firstName = 'hogehoge'のような書き方はできませんが、直感的なプロパティの定義だと思います。

class命令によるその他のオブジェクト指向操作

  1. 静的メソッドの定義
    staticをメソッド定義の頭に付与することで静的メソッドを定義することができます。

  2. クラスの継承
    extendsを利用することで既存クラスを継承したサブクラスをシンプルに定義することができます。

class Member {
    ...中略...
}

class BusinessMember extends Member {
    work() {
        return this.getName() + 'は働いています。';
    }
}

let bm = new businessMember('太郎', '山田');
console.log(bm.getName()); // 山田太郎
console.log(bm.work()); // 山田太郎は働いています。
  1. オーバーライドとsuperキーワード
    基底クラスで定義されたメソッド/コンストラクターは、サブクラスで上書きすることもできます。 これをメソッドのオーバーライドと呼び、JavaScriptにおいてもこの仕組みが用意されています。
class Member {
    ...中略
}
class BusinessMember extends Member {
    // Memberのコンストラクタに役職を追加
    constructor(firstName, lastName, position) {
        super(firstName, lastName);
        this.position = position;
    }
    getName() {
        return super.getName() + '/役職: ' + this.position;
    }
}
let bm = new BusinessMember('太郎', '山田', '課長');
console.log(bm.getName()); // 結果: 山田太郎/役職: 課長

オーバーライドは、基底クラスの機能を完全に書き換えるより、差分の処理を追加する場合が多いかなと思います。
そのような場合にsuperを使用することで基底クラスの処理を利用しつつ新しいクラス定義が可能になります。

TypeScriptにおけるクラス構文

ここまで、JavaScriptにおけるクラスについて復習も兼ねて丁寧に確認してきました。
ここからはクラス構文をTypeScriptでどのように書くのかについてまとめてみたいと思います。
基本的な使い方にあまり差がない部分については省略して、JavaScriptと比較して有用な箇所や使用に注意が必要な箇所についてまとめます。

プロパティの宣言

TypeScriptでは、JavaScriptでは不要だったプロパティの宣言を行う必要があります。
定義していないプロパティアクセスはエラーとなります。
以下の例のように、プロパティ名: 型 = 式;のかたちで書く事ができます。

また、下記の例は初期値を書いていますが、省略することもできます。
ただし、初期値を省略する場合はコンストラクタを必ず書く必要があります。

class User {
    name: string = "";
    age: number = 0;
}
  • オプショナルなプロパティや読み取り専用のプロパティ

以下のような形でオプショナルなプロパティ(任意のプロパティ)や読み取り専用のプロパティを宣言することができます。

class User {
    name?: string;  // オプショナルなプロパティ
    readonly age: int = 0;  // 読み取り専用プロパティ
}
const u = new User();
console.log(u.name); // undefinedが表示される(エラーではない)
console.log(u.age); // 0が表示される
u.age = 2; // エラー

読み取り専用プロパティは基本的に代入不可能ですが、コンストラクタの中では代入が可能です。

3種類のアクセス修飾子

TypeScriptでは、public, protected, privateの3種類のアクセス修飾子をクラス宣言内のプロパティ宣言、及びメソッド宣言に付与することができます。

  • public ... どこからでもアクセス可能
  • private ... クラスの内部からのみアクセス可能
  • protected ... そのクラス自身と子クラスからアクセス可能

省略した場合はpublicと同じになります。
これによって、privateが付与されたプロパティやメソッドは外向きのインターフェースと内部実装とにはっきりと区分されます。

コンストラクタ引数でのプロパティ宣言

アクセス修飾子を用いることで、コンストラクタの引数でのプロパティ宣言が可能になります。

class User {
    name: string;
    private age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

上記のコードをコンストラクタ引数でプロパティ宣言を行うと以下のようになります。

class User {
    constructor(public name: string, private age: number){}
}

コンストラクタの引数名の前にpublic,privateというアクセス修飾子が付きます。
これによってコンストラクタ引数であると同時にプロパティ宣言であるとみなされます。
ただし、この書き方をする場合はpublicの場合でもかならず修飾子が必要になります。

処理は短くなりますが、プロパティ宣言を1箇所にまとめる事ができない点と、JavaScript本来の構文からかなり逸脱しているためこの書き方は好みが分かれますが、こういう書き方ができるということは知っておくといいと思います。

まとめ

  • JavaScriptにおけるclass構文はES2015で追加された比較的新しいもの
  • プロパティの宣言にはコンストラクタを利用したものとget/setを利用したものがある
  • TypeScriptにおけるclass構文ではプロパティの宣言が必要
  • TypeScriptではプロパティとメソッドにアクセス修飾子が付与できる

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes
https://future-architect.github.io/typescript-guide/class.html

所感

今回はJavaScriptのclass構文とTypeScriptのclass構文の違いを主に学習しました。
TypeScriptでは他にも型引数やクラスの型の概念があるので引き続き調べていく必要がありそうです。

2023年になり、年末は帰省していたため、久々に業務以外で一切勉強しない日が続きました。
気分転換あまりできていなかったのでいい休息になったかなと思います。
新年になったのでまた新たに目標を設定して2023年も駆け抜けたいと思います。

2022年の振り返り

はじめに

2022年もあっという間に終わってしまいました。
ここ数年の中で一番人生に大きな変化があった一年間ですし、とてもいい出会いが多かった一年間でもありました。
当初考えていた2022年の目標を踏まえて、一年間を振り返りながら、来年の目標を考える記事にしようかなと思います。
組み込み、かつテスターやツール制作等開発以外の業務がメインだったこれまでのキャリアからWEBエンジニアに変わって努力してきたこと、これからしようと思っていることが誰かの目標設定の参考になれば幸いです。

2022年に達成できたこと

目標設定していたもの

エンジニアとしては3年目に突入する2022年には以前一度受験して落ちていた応用情報技術者試験をいい加減取っておきたいなと考えていました。
春期に受験したのですが実際問題、転職してすぐで業務についていくことが必死な期間の受験となりました。
以前受験した際の知識と、午前問題に絞って徹底して対策して、午後対策なしでぶっつけ本番という賭けに出ましたがなんとか合格することができました。
あまり再現性のない方法ですし人に勧められるものでもありませんが、個人的にはずっと引っかかっていた部分だったのでしっかり合格できて一安心です。
あとは合格したからといって内容を忘れず、特にネットワーク等基礎的な部分で得点の低かった部分を復習していきたいと思います。

  • アウトプットの継続(本ブログや業務以外での開発)

入社してすぐに、業務でのインプットの量が多く自分で調べることが圧倒的に増えました。
自分の理解の整理のためにスマレジで用意されているブログ手当制度を活用して本ブログを開設しました。
そこから週1本のペースで記事を執筆し続ける事ができています。
アウトプットの継続は地味ながらかなり大変でしたが、心が折れそうなときにブログ手当が支えてくれた気がします(適当感ありますが)
特にここ最近は単純にいろんなことが気になってしまって一本あたりの文字量もかなり増えてきました。
インプット、アウトプットともに量が増えていくことでエンジニアとしての成長も加速できればなと思います。

目標設定していなかったもの

  • チームリーダーとしてのプロジェクト管理やチーム文化の醸成に貢献できた

入社当初は研修から始まったWEBエンジニアとしてのキャリアでしたが、気づけばチームの中心となってプロダクトの開発に携わることができるようになっていました。
メンバーの支えあってのものですが、挑戦したい姿勢があれば挑戦できる環境というのはありがたいなと感じています。
知らないことをメンバーやほかのチームの方々含めて質問しまくる一年でしたが対応くださったおかげでいろんな業務知識、技術的な知識を吸収できたと思います。
来年も引き続き能動的に動けるエンジニアでありたいなと思います。

  • そもそもスマレジ入社前はリモートでの勤務を想定していなかったが現在フルリモートでうまく業務を回せている

転職時は特にリモート希望とかではなかったんですが、コロナの情勢もあいまって結果的にほぼほぼリモート勤務となりました。
コミュニケーションや業務効率の面でチームとしてうまく回るような取り組みを実践できているため特にストレスなく働くことができた一年でした。
特に時間を予め確保しての雑談やslackでの個人チャンネルの作成などを通して具体的に固まる前の段階で軽く相談できていることが非同期コミュニケーションのストレスを軽減している要因だと思います。
また、基本的にテキストコミュニケーションが中心となるので言った言わない問題を自然と回避できている点も個人的にはストレスが軽減できているなあと感じます。

  • 技術への好奇心や勉強時間が想定よりかなり大きくなった一年間だった

入社当初は業務についていくために必死で一日1~2時間程度業務外で興味のある分野や業務で使用した技術周辺の深堀り等に時間を使っていました。
そんな生活が続いて今後大丈夫かなと心配していましたが、気づけば今も変わらず、むしろ少し増えたくらいのペースで技術的な好奇心のために時間を使うことができています。
当初の目標よりがんばれてるなあと感じつつ、思っていたよりしんどさを感じずに楽しめている自分に驚いています。
来年以降、私生活含めどうなるかはわかりませんが、変わらず時間を確保していきたいと思います。

  • 思っていたよりいろんな技術書を読んだ

言語の入門書に限らず、色んなジャンルの色んな本を手に取れた一年でした。
結構いろいろと読みましたが、以下の3冊は結構何度も読み返していて自分の糧にできた本です。

初めて学ぶ分野の場合は自分の頭は白紙の状態で知識を詰め込めますが、ある程度知識がある状態で再度読み返すと新しい発見や悩みが生まれてより深いレベルで消化することができるのが技術書の面白いところだと思います。2023年も興味が湧いた本は悩まずに手にとっていきたいと思います。

2022年に達成できなかったこと

応用情報技術者だけでなく、DBスペシャリストも視野に入れていましたが、忙しさや勉強時間不足、また個人開発を優先して受験を見送ってしまいました。

スマレジ入社の少し前から挑戦していた競技プログラミングでしたが、茶色に変わる少し手前で大会への参加を見送るようになってしまいました。
参加自体は楽しかったんですが、なかなか精進の時間を取れなかったことが原因かなと思います。

2023年に達成したいこと

  • 個人開発のスピードを上げる

個人での勉強目的で作成しているアプリを完成させること。
また、他にも複数作りたいものがあるのでフロントエンドの技術をもう少し体系的に学びながらいくつか実際にリリースしてみるのが2023年の目標です。

  • インフラ周りへの理解を深める(AWS等のサービス + Docker)

これも半分個人開発、もう半分は業務の内容という感じですが、インフラ面の理解がまだまだだなと感じていて課題感があります。
インフラ面の理解をもう少し深められればと思います。

2022年に達成できなかったDBスペシャリストへの挑戦を2023年こそはしたいなと考えています。
バックエンドエンジニアとしては必須のDB周りの知識を資格勉強と実務の両方を通して身につける一年にします。

2023年にやらないこと

何をやるのかと同じくらい何をやらないのかを明確にしておかないと有限な時間で成し遂げられる目標を整理できない、というのが持論なのでやらないことも明確にしておきたいと思います。

2023年はアルゴリズム分野の勉強より知識面での勉強を優先する一年間にしたいと思います。
いずれ挑戦するとは思いますが、まだまだ知識面での不足が目立つのでまずはそこを優先してカバーしていきたいと思います。

  • AWS系の資格試験への挑戦

2022年はAWS SAAの取得ができましたが、2023年はひとまずAWS系の資格への挑戦はせずに、実際にAWSのサービスを利用してみたいと思っています。
SAAを取得したものの実際に使用してみないと知識が知識のままで終わってしまっているなと感じているため上位資格への挑戦は見送りたいと思います。

まとめ

2022年は自分的にはかなり努力して、それが良い成果に結びついてきた一年間でした。
2023年も引き続き努力するとともに、もっと手を動かし続けるエンジニアでありたいと思います。
2023年もどうぞよろしくお願いいたします!

TypeScriptに入門する

はじめに

前回の記事同様直近の大きな課題感を感じているのはフロントエンド技術です。
今回はその中でも最近主流となっているTypeScriptに入門してみます。
私のエンジニア人生はC言語から始まっているので静的型付けで書けないJSに違和感をずっと感じていたため、個人開発等で導入し、ゆくゆくは社内のプロダクトもどんどん移行していきたいなと感じています。
今回は備忘録としてJavaScriptの仕様含めたおさらい記事として残しておきます。

TypeScriptとは

  • Microsoftによって開発されているAltJSの一種
  • AltJSという言葉をあまり聞かなくなるくらい主流になっている
  • AltJS=JavaScriptの代替となる言語
  • 静的型システムを備えているのが最大の特徴

歴史的には最初のプレビュー版の公開が2012年の10月。
バージョン1.0の公開、つまり正式リリースが2014年4月。

静的型システムの例

静的型システムとは、ざっくりいうと、変数や式が型をもつということ。

const str: string = "foobar";

変数strはstring型を持っているという型注釈が書かれている

同時にTypeScriptは型推論という機能も充実している。 型推論とは、型注釈を書かなくてもTypeScriptが補って変数などの型を決めてくれる機能。

下記のコードは先程のコードから型注釈を省略したもの。

const str = "foobar";

型があるとなぜ嬉しいのか

  • 型安全性
  • ドキュメント化

型安全性とは、実行前にコンパイラが型チェックして検出してくれるしくみ。 コンパイラとは、プログラミング言語で書かれたコードを機械語に翻訳するための仕組みのこと。

コンパイラは、構文が正しくないエラーと型チェックが失敗したことを表す型エラーの2種類のエラーを主に返すが、特にコンパイラによって型エラーを検出できるのが静的型付け言語の恩恵。

型エラーとは?
型チェックの失敗は型に矛盾が発生した場合に起こる。

function repeatHello(count: number): string {
    return "hello".repeat(count)
}
// countはint型を期待しているが文字列型が入力されるためエラーが発生
console.log(repeatHello("wow"));

ドキュメント化とは型の情報がソースコードに書かれることでプログラムを読解する助けになる、ということ。
例えば、先程の例を見ると1行目を見るだけで関数repeatHelloがnumber型の引数を受け取ってstring型の返り値を返すことがわかる。
適切な関数名、コメントに加えて型の情報があればプログラムの読解時に関数の中身まで読む必要がなく、ある程度中身を推測しながら読み進める事ができる。
これは特に大規模なシステム開発で効果を発揮する。

また、型の情報は人間の助けとなるだけでなくコンピュータにとっても助けとなる。IDEによる入力補完の恩恵をより受けられる。

静的とは、実際にプログラムを実行しなくても行えるチェックのこと。プログラムの文面だけを見て行われるチェック。
反対に動的なチェックとは、テストなど実際にプログラムを実行してその結果を見てプログラムに間違いがないかを確認するもの。

TypeScriptはランタイム(実行時)の挙動が型情報に依存しないため、TypeScriptの持つ役割はあくまで静的なチェックのみ。

ランタイム(実行時)の挙動が型情報に依存しない具体例

function double(value: number) {
    console.log(value * 2);
}

function double(value: string) {
    console.log(value.repeat(2));
}

double(123);
double("hello");

上記の様に、同名関数で引数の取る型が違うものを定義できるプログラミング言語が存在する。
が、TypeScriptでは実行時の挙動は型情報に依存しないため、この機能は存在しない。あくまでTypeScriptはJavaScriptの拡張という立ち位置なので、役割を型チェックに絞ってランタイムの挙動はJavaScriptに従うという思想。  

トランスパイル

TypeScriptコンパイラの型チェック以外の役割として、トランスパイルという仕組みがある。

トランスパイルとは、TypeScriptコードをJavaScriptコードに変換するということ。(機械語ではなく他言語への変換なのでトランスパイルと呼ばれるが、単にコンパイルと呼ぶ場合もある)

TypeScript->JavaScriptへのトランスパイルは単に型注釈を取り除くだけの処理が行われる。つまり、TypeScriptは基本的にJavaScriptに型の概念を導入しただけなのでそれを取り除くだけで変換ができる。

TypeScriptにおけるプリミティブ型

プリミティブとは、「原始的な」という意味ので単語で、プログラミング言語における基本的な値を示す。

TypeScriptには以下のプリミティブ型がある。

  • 文字列
  • 数値(number)
  • 真偽値
  • BigInt
  • null
  • undefined
  • シンボル

以降、最もよく使用される数値、文字列、真偽値についてJavaScriptの復習も兼ねて再確認する。

数値(number)型

小数と整数の区別なく数値を扱える型。
TypeScriptで欠かせない要素がリテラルという概念。
リテラルとは、何らかの値を生み出すための式のこと。

数値リテラルとは、以下の5のようなもの

const value = 5;

他にも2進数や8進数、16進数のリテラルが良く使われる。

const binary = 0b1010; // 2進数リテラル
const octal = 0o755; // 8進数リテラル
const hexadecimal = 0xff; // 16進数リテラル

// 指数表記もリテラルが存在する
const big = 1e8;
const small = 4e-5;

文字列(string)型

文字列リテラルにはダブルクォートとシングルクォートの2種類の書き方があるが、機能上の違いはない。

const str1: string = 'Hello,world!';
const str2 = "Hello,world!";

加えて、テンプレートリテラルろいうリテラルも存在する。

const message: string = `Hello
world!`

const str1: string = "Hello";
const str2: string = "world!";

console.log(`${str1},${str2}`); // "Hello,world!と表示"

普通の文字列リテラルとの違いは、

  • リテラル中で改行が可能である
  • 式を文字列の中に埋め込む事ができる

真偽値(boolean)型

trueとfalseの2つの値からなる型。

const no: boolean = false;
const yes: boolean = true;

console.log(yes, no); // true falseと表示される

オブジェクトとは

TypeScriptにおけるオブジェクトは必ずしもクラスに由来するものではない。
Java等の言語ではクラスとオブジェクトは切っても切り離せない関係にあるので、クラスに触れずにオブジェクトの話ができるTypeScriptはすこしギャップがある。

オブジェクトは連想配列である

オブジェクトはいくつかの値をまとめたデータである。

const obj = {
    foo: 123,
    bar: "Hello,world!"
};

console.log(obj.foo);
console.log(obj.bar);

変数objに代入されている{}をオブジェクトリテラルと呼ぶ。
また、:の後ろには固定された数値や文字列を書くだけでなく、変数の値を用いたり、プロパティの値を直接計算したりすることができる。

const user = {
    name: input ? input : "名無し",
    age: 20,
};

オブジェクトリテラルは、プロパティ名: 変数名という形の場合、かつプロパティ名と変数名が同じである場合は省略記法を使用することができる。

const name = input ? input: "名無し";
const user = {
    name: name,
    age: 20,
};
const name = input ? input: "名無し";
const user = {
    name,
    age: 20,
};

オブジェクトに対するconstの制限について

constで宣言された変数に再代入することはできないが、オブジェクトのプロパティの書き換えはconstによって制限されない。

次の例のように、constで宣言された変数自体に別のオブジェクトを再代入する場合はエラーが発生する。

const user = {
    name: "hoge",
    age: 25,
};

const user = {
    name: "fuga",
    age: 15,
};

スプレッド構文

オブジェクトリテラル中ではスプレッド構文を使用することができる。

オブジェクトの作成時にプロパティを別のオブジェクトからコピーする事ができる。

const obj1 = {
    bar: 456,
    baz: 789
};
const obj2 = {
    foo: 123,
    ...obj1
};

// obj2 = {foo: 123, bar: 456, baz: 789}
console.log(obj2);

既存のオブジェクトを拡張した別のオブジェクトを作りたい場合に有用。あくまでコピーなので、コピー元のオブジェクトのプロパティを変更してもコピー先には影響しない。

オブジェクトの等価性

TypeScriptではオブジェクトが暗黙にコピーされることはなく、複数の変数に同じオブジェクトが入る場合が存在する。

const foo = { num: 1234 };
const bar = foo;
console.log(bar.num); // 1234

bar.num = 0;
console.log(foo.num); // 0

上記の例では、変数foobarには同じオブジェクトが入っている。
変数がそのオブジェクトの実体を専有しているとは限らず、他の場所で書き換えられる可能性がある。

下記の様に、明示的にオブジェクトをコピーすることで別のオブジェクトとして扱う事ができる。 1つめの方法がスプレッド構文を使用して次のように書く。

const foo = { num: 1234 };
const bar = { ...foo };
console.log(bar.num); // 1234
bar.num = 0;
console.log(foo.num); // 1234(書き換えられていない)

ただし、オブジェクトのプロパティの中に更にネストしてオブジェクトが入っている場合は同じオブジェクトのままなので注意が必要。

また、オブジェクトの比較には===を使用することができる。
1つ目の例のように同じオブジェクトの場合はtrueとなり、2つ目の例のように中身が同じでも別々のオブジェクトの場合はfalseになる。

まとめ

今回は名前と概要しか知らなかったTypeScriptに入門してみる記事でした。
いざ勉強し始めるとその大部分はJavaScriptと同じことがわかってきて、よりハードルが下がった気がします。
次回以降、TypeScriptならではの型について勉強していきたいと思います。
また、実際にJavaScriptと比較してメリットを実感するために個人開発にも積極的に導入していこうと思います。

JavaScriptにおける非同期処理について

はじめに

今までの学習内容でフロントエンドにあまり触れてなかったので今回はJavaScriptについて改めて理解をしていこうと思います。
JavaScriptに対する個人的な理解度はある程度文法や言語思想は理解できているもののJavaScriptらしい部分や歴史的な部分、いわゆるその言語の特性を理解できているとは言えないレベルだなと感じているため、今回は非同期処理について改めて調査してみます。

同期処理と非同期処理

多くのプログラミング言語には同期処理(sync)と非同期処理(async)の2つのコードの評価の仕方があります。 特にJavaScriptにおいて非同期処理は重要な概念になります。

同期処理

同期処理ではコードを順番に処理していき、ひとつの処理が終わるまで次の処理は行いません。
同期処理においては実行している処理はひとつだけになり、直感的な動作となります。
一方、同期的にブロックする処理が行われていた場合にはひとつの処理が終わるまで、次の処理へ進むことができないです。

このときに特に問題となるのはブラウザ上でJavaScriptを動作させる場合です。
基本的にJavaScript派ブラウザのメインスレッド(UIスレッドとも呼ばれる)で実行されます。 メインスレッドは表示の更新といったUIに関する処理も行っています。 そのため、メインスレッドがJavaScriptの処理で専有されると、表示が更新されなくなり、見た目上フリーズしたようになります。

非同期処理

非同期処理はコードを順番に処理していきますが、ひとつの非同期処理が終わるのを待たずに次の処理を実行します。 つまり、同時に実行される処理が複数存在します。
これによって解決される問題としては、コストが大きく異なる2つの処理を同期的に順次処理していく効率の悪さを解決できます。

ここまでの非同期処理の説明だけを見ると、完全に別々の処理が同時進行しているように感じます。 基本的にJavaScriptの基本的な非同期処理はメインスレッドで実行されています。 setTimeOutメソッドなどを利用すれば並列処理ができるのではないかと感じますが、実際にはsetTimeoutで指定された作業は一旦脇に置かれているだけで、メインスレッド上で順番に処理されています。

例外としてはWeb Worker APIを使用した場合などです。

スレッドとは

非同期処理について理解を進める前にスレッドについて理解しておく必要があります。
スレッド(thread)はプログラムが連続して順番に何かしらの処理が実行される流れのことです。
英語で「糸」という意味があり、一般的なプログラムはこのスレッド(糸)を複数合わせて一本の丈夫なひも(機能)を実現しているとイメージするとわかりやすいかもしれません。
JavaScriptは基本的にシングルスレッドである、というのはこのスレッドが基本的に1つしかないということです。

JavaScriptにおけるメインスレッド

ブラウザにおいて、JavaScriptは以下の2つのしごとをメインスレッドで行っています。

JavaScriptでは一部の例外を除き、非同期処理は並行処理(concurrent)として扱われます。 並行処理とは、処理を一定の単位ごとに分けて処理を切り替えながら実行することです。 非同期処理を実装すると、メインスレッドに並んでいる処理の流れから一旦外れて次の処理に実行を譲るイメージです。

一方、先程例外の例に上げたWeb Workerにおけるは並列処理です。 並列処理とは、排他的に複数の処理を同時に実行することです。 Web Workerではメインスレッドとは異なるWorkerスレッドで実行されるため、Workerスレッド内で同期的にブロックする処理を実行してもメインスレッドは影響を受けにくくなります。 これによって重たい処理をWorkerスレッドに移動できます。

このように、非同期処理をひとくくりにはできないですが、基本的にはJavaScriptはシングルスレッドで実行されるという性質を知っておくことが大事です。つまり、ここから先紹介する非同期処理の仕組みはほとんど並行処理となります。

非同期処理と例外処理

JavaScriptにおける例外処理(同期処理)

JavaScriptの場合、同期処理ではtry...catch構文を使用することで同期的に発生した例外がキャッチできます。

try {
    throw new Error("同期的なエラー");
} catch (error) {
    console.log("同期的エラーをキャッチ");
}
console.log("この行は実行されます");

JavaScriptにおける例外処理(非同期処理)

非同期処理ではtry...catchによる例外のキャッチができません。

try {
    setTimeout(() => {
        throw new Error("非同期的なエラー");
    }, 10);
} catch (error) {
    console.log("実行されない");
}
console.log("この行は実行されます");

tryブロックはそのブロック内で発生した例外をキャッチする構文です。 しかし、setTimeout関数で登録されたコールバック関数が実際に実行されて例外を投げるのは、すべての同期処理が終わったあととなります。 つまり、tryブロックのマークしている範囲外で例外が発生するため、catchできないという仕組みです。

そのため、コールバック関数内で同期的なエラーとしてキャッチします。

// 非同期処理の外
setTimeout(() => {
    // 非同期処理の中
    try {
        throw new Error("エラー");
    } catch (error) {
        console.log("エラーをキャッチできる");
    }
}, 10);
console.log("この行は実行されます");

上記のようにコールバック関数内でエラーのキャッチは可能ですが、非同期処理の外からは非同期処理の中で例外が発生したかがわかりません。 非同期処理の外から、例外が発生したことを知るためには非同期処理の外へ伝える方法が必要です。

また、JavaScriptでのHTTPリクエストやファイルの読み書きといった処理も非同期処理のAPIとして提供されているため、例外の扱い方は重要になります。

非同期処理で発生した例外の扱い方には様々なパターンがありますが、主流なPromiseについて見ていきます。

Promise

非同期処理がいくつも連なる場合にコールバック関数を利用すると、入れ子が深くなりすぎて1つの関数が肥大化する傾向にあります。

first(function(data) {
    console.log("最初に実行する処理");
    second(function(data) {
        console.log("first関数が成功した場合に実行する処理");
        third(function(data) {
            console.log("second関数が成功したときに実行する処理");
        });
    });
});

このような問題を解決するのがPromiseオブジェクトの役割です。

これまで、jQueryやAngularJSには似たような機能を提供してきましたが、ES2015でPromiseオブジェクトが標準化されたことで外部ライブラリに頼る必要がなくなりました。

非同期処理はPromiseのインスタンスを返し、そのPromiseには状態変化をした際に呼び出されるコールバック関数を登録できます。

function asyncProcess(value) {
    return new Promise((resolve, reject) => {
        // ここで非同期処理を行う
        setTimeout(() => {
            if (value) {
                // 成功した場合はresolveを呼ぶ
                resolve(`入力値: ${value}`);
            } else {
                // 失敗した場合はrejectを呼ぶ
                reject('入力は空です');
            }
        }, 500);
    });
}

asyncProcess('input').then(() => {
    // 非同期処理が成功したときの処理
}).catch(() => {
    // 非同期処理が失敗したときの処理
})

asyncProcess関数はPromiseオブジェクトのインスタンスを返しています。 PromiseインスタンスはasyncProcess関数内で行われた非同期処理が成功したか失敗したかの状態を表すオブジェクトです。
また、このPromiseインスタンス二台したthencatchメソッドで成功時や失敗時に呼び出される処理をコールバック関数として登録することができます。

書き方だけを見るとややこしく見えますが、Promiseは非同期処理の状態や結果を監視するためのオブジェククトです。
同期的な関数では関数を実行するとすぐに結果がわかりますが、非同期な関数では関数を実行してもすぐには結果がわからないため、非同期処理の状態をラップしたオブジェクトを返し、結果が決まったら登録しておいたコールバック関数へ結果を渡す仕組みになっています。

具体的にPromiseインスタンスを理解する

上記のコードで基本的な使用方法は把握できるかと思いますが今回はPromiseについてもう少し丁寧に理解してみます。

まずはPromiseインスタンスの作成します。
thenメソッドでPromiseがresolve、rejectしたときに呼ばれるコールバック関数を登録します。

const promise = new Promise((resolve, reject) => {
    // 非同期の処理が成功したときはresolve()を呼ぶ
    // 非同期の処理が失敗したときはreject()を呼ぶ
});
const onFulfilled = () => {
    console.log("resolve時に実行される");
};
const onRejected = () => {
    console.log("reject時に実行される");
};

promise.then(onFulfilled, onRejected);

Promise.prototype.thenとPromise.prototype.catch

Promiseのthenメソッドは成功(onFulfilled)と失敗(onRejected)の2つのコールバック関数を受け取りますが、どちらの引数も省略できます。

function Process(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/success")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("Not Found"));
            }
        }, 1000 * Math.random());
    });
}

// thenメソッドで成功時と失敗時のコールバック関数を登録
Process("/success/data").then(function onfulfilled(response) {
    console.log(response);
}, function onRejected(error) {
    console.log("実行されない");
});

Process("/failure/data").then(function onFulfilled(response) {
    console.log("実行されない");
}, function onRejected(error) {
    console.error(error); // "Not Found"
});

ここで、失敗時のコールバック関数のみ登録する場合を考えます。 このときcatchメソッドは内部的にthenメソッドを呼び出しています。つまり、catchはthenの失敗時のみを記載するためのエイリアスとして動作します。

参考:
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch

function errorProcess(message) {
    return new Promise((resolve, reject) => {
        reject(new Error(message));
    });
}

// 非推奨。thenで失敗時のコールバック関数のみを渡したい場合は第一引数にはundefinedを渡す。
errorProcess("thenでエラーハンドリング").then(undefined, (error) => {
    console.log(error.message);
});

// 推奨
errorProcess("catchでエラーハンドリング").catch(error => {
    console.log(error.message);
});

resolveとrejectの使い方がわかったところで重要な以下の2点を覚えておいて次に進みます。

  • Promise内のresolveメソッドが実行されるまで、then()の中身は実行されない
  • Promise内のrejectメソッドが実行されるまで、catch()の中身は実行されない

Promiseコンストラクタ内の例外処理

Promiseではコンストラクタの処理で例外が発生したPromiseインスタンスreject関数を呼び出したのと同じように処理されます。
try...catch構文を使用しなくても自動的に例外がキャッチされます。

function throwPromise() {
    return new Promise((resolve, reject) => {
        // Promiseコンストラクタの中で発生した例外は自動的にキャッチされreject関数が呼ばれる
        throw new Error("例外発生");
    });
}

throwPromise().catch(error => {
    console.log(error.message);
});

Promiseの状態について

Promiseインスタンスには以下の3つの状態が存在します。

  • Fulfilled
    resolve(成功)した時の状態。onFulfilledメソッドが呼ばれる

  • Rejected
    reject(失敗)または例外が発生したときの状態。onRejectedが呼ばれる。

  • Pending
    FulfilledまたはRejectedではない状態。インスタンスを作成したときの初期状態。

これらは内部的な状態なのでこの状態を直接扱うことはできませんが、Promiseについて理解するのに役立ちます。

Promiseインスタンスは作成時にPending状態になり、処理の結果によってFulfilledまたはRejectedに変化するとそれ以降変化しなくなります。

つまり、Promiseコンストラクタ内で一度resolveメソッドを呼び出すと、その後、rejectやもう一度resolveを呼び出したとしてもコールバック関数は1度しか呼び出されないことに注意する必要があります。

この1度きりのコールバック関数を登録するのが、thencatchといったメソッドです。

非同期処理の連結

ここまでPromiseオブジェクトについて説明してきました。 ここからはPromiseオブジェクトのありがたみをイメージできるケースを考えます。

単一の非同期処理の場合、Promiseオブジェクトを介する分、記述は冗長になります。 Promiseオブジェクトが真価を発揮するのは、複数の非同期処理を連結するような場合です。

// 初回関数呼び出し
asyncProcess('初回')
.then(
    response => {
        console.log(response);
        // 初回の関数呼び出しに成功した場合、2回目を実行
        return asyncProcess('2回目');
    }
)
.then(
    response => {
        console.log(response);
    }
    error => {
        console.log(`エラー: ${error}`);
    }
);

この仕組みは、thenやcatchといったメソッドが新しいPromiseオブジェクトを返すことで成り立っています。 これによって複数のthenメソッドをドット演算子で列記することができ、非同期処理を同期処理であるかのように書けます。(入れ子を深くせずに書けるという意味です)

非同期処理の並列実行

非同期処理の直列実行の次は、並列実行のメソッドについて見ていきます。

  • Promise.allメソッド
    Promise.allメソッドは複数の非同期処理を並列に実行し、そのすべてが成功した場合に処理を実行します。
Promise.all([
    asyncProcess('1回目');
    asyncProcess('2回目');
    asyncProcess('3回目');
]).then(
    response => {
        console.log(response);
    },
    error => {
        console.log(`エラー: ${error}`);
    }
);

Promise.allでは、配列のかたちで渡された複数のPromiseオブジェクトがすべてresolveした場合にだけthenメソッドの成功時コールバック関数を実行します。 その際の引数(response)にはすべてのPromiseから渡された結果値が配列として渡されます。

Promiseオブジェクトのいずれかがreject(失敗)した場合には失敗コールバックが呼び出されます。

  • Promise.raceメソッド
    Promise.raceメソッドでは並列して実行した非同期処理のいずれか1つが最初に完了したところで成功時コールバック関数を実行します。 例えば、複数のデータベースレプリケーションに対して一斉にクエリを投げて最初に応答があったものを使用する、といった使い方ができます。 関数の命名通り、レースです。

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function

https://jsprimer.net/basic/async/

https://www.amazon.co.jp/%E6%94%B9%E8%A8%82%E6%96%B0%E7%89%88JavaScript%E6%9C%AC%E6%A0%BC%E5%85%A5%E9%96%80-%E3%83%A2%E3%83%80%E3%83%B3%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%81%AB%E3%82%88%E3%82%8B%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E7%8F%BE%E5%A0%B4%E3%81%A7%E3%81%AE%E5%BF%9C%E7%94%A8%E3%81%BE%E3%81%A7-%E5%B1%B1%E7%94%B0-%E7%A5%A5%E5%AF%9B/dp/477418411X

https://qiita.com/ryosuketter/items/dd467f827c1b93a74d76

まとめ

  • 非同期処理にはもともとコールバック関数が使用されていたが、ネストの深さの問題や書きやすさから、ES2015以降はPromiseオブジェクトを使用する方法が主流。
  • Promiseには内部的に3つの状態があり、1度状態変化した後は変化しない。
  • Promiseのthenメソッドやcatchメソッドは新しいPromiseオブジェクトを返すため、thenやcatchメソッドをドット演算子でチェーンすることができ、例外処理がシンプルに書ける。

所感

今回はJavaScriptにおける重要な仕組みである非同期処理について調べてみました。
個人的にJavaScriptはなにか作りたいときに都度調べるという方法で勉強してきましたが、調べて直感的に理解できなかったのが今回調査した非同期処理でした。
知らない技術を調べる際にはその技術が解決する問題を先に知っておきどんなつらみを解消するのかを意識することでより深い理解につながると思っています。
また今回学んだPromiseオブジェクトを利用して個人開発のほうでなにか作ってみたいと思います。

プログラミングにおける「DI」二種類の違いとは。依存性注入と依存性逆転について

はじめに

エンジニアになって初めて人に知識を伝えるという場面がありました。
これまでチーム内ではたいてい自分が一番技術的に拙い部分が多かったので少しうれしくもありますが、正しくわかりやすく伝えるために勉強してきたことを棚卸しするとともに現時点での考えを整理していきたいと思います。

DIとは

プログラミングの文脈で登場するDIという単語には以下の二種類があります。

混乱しやすいのは両者はかなり密接な関係にあります。 この2つのDIを理解するために、まずは依存性逆転の原則について理解を進めます。

依存性逆転の原則

依存性逆転の原則とは、一言でいうと「抽象へ依存させることで依存関係を逆転させる」という原則です。 次の2つが中心的な考え方になります。

  1. 上位のモジュールは下位のモジュールに依存してはならない。双方とも抽象(例としてインターフェース)に依存するべきである。
  2. 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。

理解を進めるにあたって、「依存」について知っておく必要があります。
JSでいうとimport、PHPでいうとuseなどを使用してモジュールを使う側が依存する側です。
反対に、使用される側が依存される側です。 依存先のモジュールがないとモジュールが成り立たない状態を依存先のモジュールに「依存している」といいます。

プログラミングにおいては、例えば重要性が高いビジネスロジックをまとめたドメイン層の部分と表示における関心事をまとめたプレゼンテーション層では変更頻度が異なります。

それぞれの層に求められる性質は以下のようになります。

ドメイン層->安定性が高い、柔軟性は求められない
プレゼンテーション層->安定性が低い、柔軟性が求められる

システムの本質であるドメイン知識(ドメイン層)は最も安定性が高くなければなりません。
モジュール間に依存関係が成立する場合、依存する側は安定性が低く、柔軟性が高くなります。
これはモジュールを使用すればするほどコードが複雑になるからです。

今回はPHPを例に依存性逆転の原則を適用した場合とそうでない場合を比較してみます。
前回の記事にて軽量DDDを紹介しているのでそれに合わせたレイヤ構造を例にしてみます。

従来のレイヤーパターン

依存性逆転の原則のwikipediaに下記の説明が記載されています。過不足なくわかりやすい説明だと思ったので引用しておきます。

伝統的なアプリケーションアーキテクチャにおいては下位レベルのコンポーネントはより複雑なシステムの構築を可能にする上位レベルコンポーネントによって使用される形で設計がおこなわれる。この方法では上位レベルコンポーネントは直接下位レベルコンポーネントに依存する。この低レベルコンポーネントへの依存は、上位レベルコンポーネントの再利用の機会を制限してしまう。

実際にPHPで書いてみると以下のようなコードになります。C言語のような手続き型言語の場合は結構使用されているパターンだと思います。簡単に言うと部品を作ってそれをまとめ上げるイメージです。

(イメージなので実際には動作しないと思いますので注意してください)

class Service
{
    private Repository $repository;

    public function __construct()
    {
        $this->repository = new Repository();
    }

    public function process()
    {
        return $this->repository->findAll();
    }
}

class Repository
{
    public function findAll()
    {
        // 具体的なDBアクセス処理
        $result = 'hoge';
        return $result;
    }
}

このように上位のレイヤから下位のレイヤを呼び出す形がレイヤーパターンと呼ばれていたりします。 注目してほしいのはコンストラクタで直接newをしている部分です。このように直接newをしている場合はRepositoryという具象クラスの実装に依存してしまいます。

この場合、システムの本質であるServiceがRepositoryに依存しているため安定性が低くなってしまいます。
ここでいう安定性が低いというのは他のモジュールの影響を受けやすく、変更に弱いことを意味しています。
安定性を上げるために、ドメイン層であるServiceはモジュールをuse(具象クラスをnew)しないように書くべきです。

依存性注入

さて次はどうやってドメイン層の安定性を上げていくのかについて考えてみます。
ここで登場するのが依存性注入です。
依存性注入とはプログラミングにおける設計思想の一種です。
コードの実行時にインターフェース(または抽象クラス)に対して具象クラスを外部から注入(Inject)するという考え方です。
オブジェクトが他のオブジェクトを利用する際、コードをはじめから結合させるのではなく、オブジェクトの実行時に呼び出して結合するようにしています。

広義的には対象モジュール以外のモジュールから具象クラスが渡される場合はDIと呼びます。

先程のレイヤーパターンの例でDIを使用してみます。

use Repository;
class Service
{
    private Repository $repository;

    public function __construct(Repository $repository)
    {
        $this->repository = $repository;
    }

    public function process()
    {
        return $this->repository->findAll();
    }
}
class Repository
{
    public function findAll()
    {
        // 具体的なDBアクセス処理
        $result = 'hoge';
        return $result;
    }
}

// ここで依存性を注入
$service = new Service(new Repository());
$action = new Action($service);
// 実行
$action->process();

変更点はコンストラクタにて外部からインスタンスを渡すように変更している点です。
外部から具象クラスを渡す事によってクラス単位での単体テストが可能になります。
どのクラスを使用するかは外部から決める事ができるので、モック等を使用しやすくなります。
ここでServiceのコンストラクタにはRepository型のインスタンスを渡す必要がありますが、コンストラクタで渡すように変更したため、Repositoryクラスを継承したクラスを渡すことが可能になリます。

次のステップとして、Repositoryにインターフェースを実装します。

use RepositoryInterface;
class Service
{
    private RepositoryInterface $repository;

    public function __construct(RepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function process()
    {
        return $this->repository->findAll();
    }
}

interface RepositoryInterface
{
    public function findAll();
}
class Repository implements RepositoryInterface
{
    public function findAll()
    {
        // 具体的なDBアクセス処理
        $result = 'hoge';
        return $result;
    }
}

// ここで依存性を注入
$service = new Service(new Repository());
$action = new Action($service);
// 実行
$action->process();

ここでのポイントはドメイン層側にインターフェースを持つことです。
ドメイン層側というのはPHPの場合はnamespaceにおけるドメイン層を指しています。(Javaの場合はパッケージにあたるかなと思います)
明示的にドメイン層とその他の層を分けておくことをおすすめします。

これによってドメイン層はRepositoryInterfaceに依存し、Repositoryの具体的な実装に依存しなくなります。
反対に、Repositoryがnamespace的にドメイン層にあるRepositoryInterfaceに依存します。

最初のレイヤーパターンと比較すると依存性が逆転しています。これを依存性逆転の原則と呼びます。
このパターンを採用することでドメイン層はRepositoryに依存しないため、変更があった場合でも影響を受けにくくなります。

また、依存性注入はフレームワークやライブラリでおこなってくれる場合が多く、DIコンテナと呼ばれたりします。 Laravelにおいてはサービスコンテナ、サービスプロバイダを理解するとイメージが湧くかなと思います。

サービスコンテナ
サービスプロバイダ

アーキテクチャについて

依存性逆転の原則を利用したアーキテクチャとして良く取り入れられているものにクリーンアーキテクチャやオニオンアーキテクチャ、ヘキサゴナルアーキテクチャ等があります。

これらのアーキテクチャはどれも本質的なビジネスロジックをまとめた層に向けて依存させるという依存の方向性を重視しています。依存の方向を内側(ドメイン層)へ向けるための具体的なレイヤ分けが異なるだけで、本質となる考え方は共通していると私は考えています。

どのアーキテクチャもレイヤ分けしたそれぞれの層の責務を明確にして依存関係を明確にするという目的が共通しています。

責務を明確にすることで振る舞いを適切に抽象化してインターフェースを作成します。そのインターフェースに対して依存することで共通の振る舞いを持つ別クラスに交換可能になります。
別のオブジェクトが同じふるまいを持ち、異なるクラスを同じものとみなせる性質をオブジェクト指向の文脈においてはポリモーフィズムと呼びます。

これにより交換可能なコード、つまり単体テストのしやすいコードになります。
私個人の考えですが、良い設計というのは変更容易性を高めるために必然的に交換可能なコードになります。
そのため、良い設計になっているかどうかの指標としてテストがしやすいかどうかは一つのポイントだと思っています。
テストがしにくいなと感じた場合は設計が悪い可能性が高い、といえます。

先程挙げた例のServiceをテストしてみます。
このとき、必要なのはRepositoryInterfaceのみであり、Repositoryの具体的な実装は必要ないため、Mock化することができます。

use RepositoryInterface;
class Service
{
    private RepositoryInterface $repository;

    public function __construct(RepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function process()
    {
        return $this->repository->findAll();
    }
}

interface RepositoryInterface
{
    public function findAll();
}
class ServiceTest
{
    public function testProcess()
    {
        $repositoryMock = new RepositoryMock();
        $result = new Service($repositoryMock);
    }
}

class RepositoryMock implements RepositoryInterface
{
    public function findAll()
    {
        // テストのための値を返す
        return 'test';
    }
}

まとめ

  • DIには、依存性の注入(Dependency Injection)依存性逆転の原則(Dependency inversion principle)の2種類がある。

  • 依存性逆転の原則とは、一言でいうと「抽象へ依存させることで依存関係を逆転させる」という原則。

  • 上位のモジュールは下位のモジュールに依存してはならない。双方とも抽象(例としてインターフェース)に依存するべきである。
  • 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。

  • 依存性の注入とは、コードの実行時にインターフェース(または抽象クラス)に対して具象クラスを外部から注入(Inject)するという考え方です、

    所感

今回はDIについてメンバーに説明する機会があったのでまとめてみました。
自分でサンプルコードを書いてみたり、人に説明することで自分自身の理解もより高まるなと感じています。
ここ最近は自分の知識を吐き出すことが多かったのですが、さまざまな技術ブログでアドベントカレンダーイベントが始まっているので毎日いろんな記事を参考にできて最近はインプットも増えて嬉しいです。
またなにか新しい考えを身に着けられれば記事にしたいと思います。

参考

https://qiita.com/okazuki/items/a0f2fb0a63ca88340ff6
https://zenn.dev/chida/articles/e46a66cd9d89d1

「ドメイン駆動設計入門」を読む その2 ユースケースを組み立てるためのパターン編

はじめに

前回の記事にて、ドメイン駆動設計への入門として、ドメインオブジェクトを紹介しました。 今回は実際にドメインオブジェクトを利用してユースケースを組み立てていきます。
そのためのステップとして、先にいくつか便利なパターンを紹介しておきます。

今回紹介するパターンは

  • Repository
  • Factory
  • Service

の3つです。

ちなみに、混乱を避けるために説明のスコープを絞り、クリーンアーキテクチャとは分けて書いたつもりですが、一部クリーンアーキテクチャの思想がサンプルに含まれている点がありますのでご注意ください。

対象書籍

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

今回も前回同様、上記の書籍を参考に進めていきますが、今回の記事はより個人的な見解(ほぼ個人的見解)が含まれます。 書籍にて解説されている内容を知りたい方はぜひ購入してみてください。(kindleだとセールで結構安くなっていたりします。)

Repository

Repositoryという単語の意味は、「保管庫」です。

ソフトウェアにおいてはドメインオブジェクトを作成したとしても、メモリ上に展開されるので、プログラムが終了した場合は消えてしまいます。 消えてしまっても困るデータについてはデータストアに永続化する必要があります。
リポジトリはデータを永続化するという責務を凝集するためのオブジェクトといえます。

Repositoryパターンを採用した場合、オブジェクトのインスタンスを永続化したい場合はデータストアに直接書き込むのではなく、リポジトリインスタンスの永続化を依頼します。 また、データストアに永続化したデータからインスタンスを再構築したい場合にもリポジトリにデータの再構築を依頼します。

リポジトリドメインオブジェクトではありませんが、ドメインを表現したコードを実現するために、データストアへの具体的なアクセス処理をすべて隠蔽する事ができます。ドメインを表現するための構成要素として欠かせないものです。

リポジトリは具体的にどういった内容を隠蔽するのかについて考えてみます。
リポジトリの責務はドメインオブジェクトの永続化再構築を行うことです。
また、保存先であるデータストアに基づく具体的な処理を隠蔽します。
データストアといえばMySQLなどのRDBを想像するかもしれませんが、他のDBMSに変わったり、単純にファイルに保存したり、スプレッドシートのようなサービスに保存したりと保存先はいろいろ考えられます。保存先が変更となった場合でもドメインコードに変更がないように、つまり永続化に関連する処理にドメインコードが依存しないように隠蔽します。

では具体的にリポジトリがどういう処理を担うのか、インターフェースから考えてみます。 今回は例として、前回の記事同様システムを利用するユーザーというドメインについて考えてみます。

interface RepositoryInterface
{
    /**
     * 再構築を担う処理
     */
    public function findById(UserIdentifier $id): User;

    /**
     * 永続化を担う処理
     */
    public function save(User $user): void;
}

ここで永続化したいユーザーのエンティティは以下のようなものを想定しています。

class User
{
    // UserIdentifierというValueObjectを保持するためのプロパティ
    private UserIdentifier $id;

    // UserNameというValueObjectを保持するためのプロパティ
    private UserName $name;

    public function __construct(
        UserIdentifier $id,
        UserName $name
    ){
        $this->id = $id;
        $this->name = $name;
    }

    public function id(): UserIdentifier
    {
        return $this->id;
    }

    public function name(): UserName
    {
        return $this->name;
    }
}

続いて、Repositoryを利用する側を考えてみます。
今回はユーザー情報の更新について考えてみます。
例えば、下記のコードでユーザー名を変更します。

class UpdateUser
{
    private RepositoryInterface $repository;

    // Laravelのサービスコンテナを用いたDIを行う。
    // 今回の説明において重要な箇所ではないのでDIに馴染みが無い方は一旦読み飛ばしても構いません。
    public function __construct(RepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function process(UserIdentifier $id, UserName $name): User
    {
        // データストアからインスタンスの再構築を行う。
        $user = $this->repository->findById();

        // エンティティのもつ名前変更の振る舞いを呼び出して名前を変更する。
        $user->changeName($name);

        // データストアへの永続化を行う。
        $this->repository->save($user);
    }
}

UserIdentifier,UserNameは必要なバリデーションを実装した、値の性質を持ったValueObjectです。
Userエンティティは名前を変更するための振る舞い「changeName」を持っています。
Repositoryには再構築のための振る舞い「findById」と永続化のための振る舞い「save」を定義します。

こうすることで、サンプルコードの様に具体的なデータストアの操作をすべてRepositoryに隠蔽し、ドメインを表現したコードにすることができます。

ドメインのコードにDBアクセスに関連するコードが氾濫しないことを確認できたので具体的なDBに対する操作はどう書くのか考えてみます。

今回はLaravelのORMであるEloquentを例にしますが、ORMがなんであれ、接続先DBがなんであろうと永続化と再構築という処理がドメインコードから隠蔽されていれば問題ないです。

以下サンプルではLaravelのEloquentモデルとしてUserを使用し、同じUserという名前でエンティティを作成しているため違いに注意してください。

class Repository extends RepositoryInterface
{
    /**
     * Eloquentモデル
     */
    private \App\Models\User $user;


    public function __construct(\App\Models\User $user)
    {
        $this->user = $user;
    }

    /**
     * IDに紐づくデータをUserエンティティに詰めて返す。
     */
    public function findById(UserIdentifier $id): User
    {
        $query = $this->user->newQuery()
            ->where('id', '=', (string)$id)
            ->first();
        return new User(
            new UserIdentifier($query->getAttribute('id')),
            new UserName($query->getAttribute('name'))
        );
    }

    /**
     * エンティティを永続化する。
     */
    public function save(User $user): void
    {
        $query = $this->user
            ->fill([
                'id' => (string)$user->id(),
                'name' => (string)$user->name()
            ])
            ->save();
    }
}

このような形で、利用するフレームワークの機能を使用したりしてDBへのアクセスロジックをRepository内に記載します。
重要なのはフレームワークの機能やDBアクセスの複雑なロジックをドメインを表現している層に漏洩させないことです。
開発初期にDBがまだ選定されていない場合や運用中に変更となった場合でもその変更はドメインロジックには影響しなくなります。
また、テストを行う際にはリポジトリをモック化することでドメインロジックのテストがより容易になります。

Factory

次に紹介するパターンは「Factory」パターンです。直訳すると工場という意味があります。

前回の記事で作成したようなドメインオブジェクトはドメインモデルを反映させるが故にときに複雑なものとなります。 複雑なドメインオブジェクトの生成に関する知識をまとめたのが「Factory」です。

これまでと同じくユーザーというドメインモデルについて考えます。ユーザーエンティティの生成処理を担うUserFactoryのもつ振る舞いを考えてみます。
ユーザーエンティティにはIDを持ちますがこのIDの採番処理はドメインを操作する上では直接関係なく、生成時に行う処理なのでFactoryでもつようにしてみましょう。

インターフェースは以下のようユーザーエンティティの作成処理を定義しておきます。

interface UserFactoryInterface
{
    public function createUser(UserName $name): User
}

今回はIDの採番にULID形式を採用します。 IDの生成にはLaravelに含まれるSymfonyというライブラリを使用します。

use Symfony\Component\Uid\Ulid;

class UserFactory implements UserFactoryInterface
{
    public function createUser(UserName $name): User
    {
        $id = Ulid::generate();
        $user = new User(
            $id,
            $name
        );
    }
}

今回はフレームワークの機能に頼ったのでシンプルな処理になりましたが、他にもエンティティの生成時に発生する処理をFactoryに隠蔽できます。
例えばエンティティの生成時刻をエンティティ自身がもつような場合、現在時刻を取得してエンティティに設定するのはFactoryに書くべきです。

RepositoryのときはDBに依存しないように処理を記載しました。Factoryを採用することでIDの採番や作成時刻等をDBに依存せずに記述できます。 ドメイン駆動設計においては具体的なDBMSに依存することを避けることがドメインロジックに集中して取り組むために必要です。

ここで考えられる疑問点として、インスタンスの生成はコンストラクタで行うのでFactoryを使用せずにコンストラクタに書けばよいのでは?というものです。 この疑問に対する答えは、処理が複雑なものはFactoryに記載するべき、です。
コンストラクタ内で他のオブジェクトを生成しているような場合はまずFactoryの利用を検討します。
特に、外部のフレームワークやライブラリに依存する場合は依存関係が発生するのでFactoryを使用しない場合にドメインルールがフレームワーク依存になります。つまりフレームワークを変更する場合にドメインオブジェクトのコードに変更が発生してしまいます。
このような自体を避けるためにFactoryを利用しましょう。

大切なのは思考停止するのではなく根拠を持ってFactoryを利用するかどうか判断することです。

Service

ドメイン駆動設計という文脈においてServiceと呼ばれるものには以下の二種類があります。

  • ドメインサービス
  • アプリケーションサービス

名前は似ていますが、この2つは明確な違いがあります。違いを意識しながら読み進めてもらえればと思います。

ドメインサービス

ValueObjectやエンティティには振る舞いが記述されます。
例えばユーザー名の最大文字数の制限等がそれにあたります。
しかし、ValueObjectやエンティティに記述すると不自然になってしまう振る舞いが存在します。

例えば、ユーザーの重複チェックです。
ユーザーエンティティが重複チェックを行うという振る舞いを持つ場合、重複の有無を自身に対して問い合わせることは不自然です。値の重複チェックは値を利用した別のオブジェクトが持つほうが自然になります。

この際に利用するのがドメインサービスになります。

interface UserServiceInterface
{
    /**
     * 重複をチェックする処理
     */
    public function isExists(User $user): bool
}

具体的な処理については割愛しますが、重複の確認をUserServiceというドメインサービス内で行います。 このサービスを用いてユーザーの作成処理を書いてみます。また、先述したFactoryやRepositoryも使用してみましょう。

class CreateUser
{
    private UserRepositoryInterface $repository;

    private UserFactoryInterface $factory;

    private UserServiceInterface $service;

    // Laravelのサービスコンテナを用いたDIを行う。
    // 今回の説明において重要な箇所ではないのでDIに馴染みが無い方は一旦読み飛ばしても構いません。
    public function __construct(
        UserRepositoryInterface $repository,
        UserFactoryInterface $factory,
        UserServiceInterface $service
        )
    {
        $this->repository = $repository;
        $this->factory = $factory;
        $this->service = $service;
    }

    public function process(UserName $name): User
    {
        // IDはFactoryにて生成されるためユーザー名を渡すとエンティティが返される。
        $user = $this->factory->createUser($name);

        // ドメインサービスを用いて例外チェックを行い、重複している場合は例外をスローする。
        if(!$this->service->isExists())
        {
            throw new NotFoundException();
        }

        // データストアへの永続化を行う。
        $repository->save($user);

    }
}

ドメインモデルを実装する際にはドメインオブジェクトに実装すると不自然になる振る舞いが必ず存在します。
これは特に複数のドメインオブジェクトを横断するような操作に多く見られます。
そんなときにはドメインサービスの利用を検討してください。

アプリケーションサービス

アプリケーションサービスを一言でいうと、ユースケースを実現するオブジェクトです。
実際に私の所属しているチームではこのアプリケーションサービスをユースケースと呼ぶ事が多いです。

ここで「ドメイン」と「アプリケーション」という命名について考えてみます。
「アプリケーション」とは一般的には利用者の目的を達成するためのプログラムのことを指します。
ValueObjectやエンティティといったドメインオブジェクトは「ドメイン」を表現するためのものです。 ドメインを表現してもそれだけでは利用者の目的は達成されません。
ドメインオブジェクトを目的に沿って操作する必要があり、それはまさしく利用者の目的を達成するための「アプリケーション」といえます。

先程から例に上げているユーザー機能においては、「ユーザーを作成する」、「ユーザー情報を更新する」、「ユーザーを削除する」等がユースケースにあたります。
先程紹介した以下のコードはまさしくアプリケーションサービスそのものです。

class CreateUser
{
    private UserRepositoryInterface $repository;

    private UserFactoryInterface $factory;

    private UserServiceInterface $service;

    // Laravelのサービスコンテナを用いたDIを行う。
    // 今回の説明において重要な箇所ではないのでDIに馴染みが無い方は一旦読み飛ばしても構いません。
    public function __construct(
        UserRepositoryInterface $repository,
        UserFactoryInterface $factory,
        UserServiceInterface $service
        )
    {
        $this->repository = $repository;
        $this->factory = $factory;
        $this->service = $service;
    }

    public function process(UserName $name): User
    {
        // IDはFactoryにて生成されるためユーザー名を渡すとエンティティが返される。
        $user = $this->factory->createUser($name);

        // ドメインサービスを用いて例外チェックを行い、重複している場合は例外をスローする。
        if(!$this->service->isExists())
        {
            throw new NotFoundException();
        }

        // データストアへの永続化を行う。
        $repository->save($user);

    }
}

ドメインサービスとアプリケーションサービスは対象となる領域が「ドメイン」なのか「利用者の目的を達成すること」なのかという点が異なる以外は本質的には同じものです。ただし、その領域をきちんと分け、ドメインのルールがアプリケーションサービスに流出しないように実装することでドメインの変更をドメインオブジェクトのみに反映すれば良くなります。変更容易性の確保のためにも意識して振る舞いを実装しましょう。

まとめ

Repository ... データアクセスに関するロジックをまとめるためのパターン
Factory ... ドメインオブジェクトの生成に関するロジックをまとめるためのパターン
Service ... ドメインオブジェクトに実装するのが不自然なものをドメインサービス、アプリケーションの目的を達成するためのロジックをアプリケーションサービスに実装するためのパターン

所感

前回の記事と合わせて、ドメイン駆動設計におけるドメインモデルをコードに反映するためのパターンをいくつか紹介しました。 これらは軽量DDDと呼ばれます。
ドメイン駆動設計において大事な要素は、ドメインモデルの継続的な改善です。そこに着手するための準備として、今回の記事がお役に立てばと思います。 より詳しいパターンの説明や抱くであろう疑問の多くは書籍で解説されている部分も多くありますのでよければ参考にしてください。

書籍のない内容に沿っていますが、自分なりの解釈がほとんどです。まだまだ理解しきれていない部分もありますが、こうしてアウトプットすることと日々の業務に向き合っていくことで引き続き設計について身につけて行ければと思います。

実際の業務ではクリーンアーキテクチャという考え方とドメイン駆動設計の要素をハイブリットして取り入れていますが、改めてドメイン駆動設計における考え方を整理しておくことと、目的を再認識できるいい記事になったなと感じています。

次回は今回の記事では紹介しきれなかった部分をTips的に紹介しようと思っています。

「ドメイン駆動設計入門」を読む その1 ドメインオブジェクト編

はじめに

スマレジにて私が担当しているプロジェクトではドメイン駆動設計を取り入れて日々開発をしています。
ジョインしてからドメイン駆動設計について説明を受けたものの体系的に学べていないので書籍を一つ読みながら足りていない箇所を補っていきます。
今回の記事は書籍の内容に沿っているつもりですが、多分に個人的な見解が含まれているため、書籍の著者の考えをきちんと把握したい場合はぜひ書籍を読んでみてください。 ちなみにスマレジに入社してすぐの頃にドメイン駆動設計について調べて記事にしました。 現在はこの頃より理解は進んでいて、人に教えるというイベントが発生する立場になり新しくジョインしたメンバーにうまく伝えるためにも語彙を増やしておきたいです。
今回はこれからドメイン駆動設計に入門する人を対象に、理解をすすめる順番を意識して記事にしてみます。

対象書籍

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

ドメイン駆動設計とは

ドメイン駆動設計は、ソフトウェアの開発手法の一つです。
まずはそもそもソフトウェアを開発する目的を考えるところから始めてみましょう。
一般的にソフトウェアは、現実世界に存在するある領域において何らかの問題を解決するために開発されます。
この「ある領域」のことを指してドメインと呼びます。(ドメインは「領域」という意味をもつ単語です)

このドメインをコード上に表現するためにモデルを作成します。

モデルという言葉自体はソフトウェア開発に携わっている方なら頻繁に耳にするでしょうし、関連する文献にも頻出します。
ではこのモデルドメイン駆動設計という文脈で何を表しているのかについて考えてみましょう。

モデルとは、現実の事象あるいは概念を抽象化した概念です。 ドメイン駆動設計とは、ドメインモデリングによってソフトウェアの価値を高めるというアプローチの開発手法です。

よく挙げられる例として、例えば日報を書くシステムを考えてみます。紙とペンがあります。 ペンを使用して紙に文字を書いて記録するという一連の流れの中にも現実世界には非常に多くの情報があります。

ペンの値段、形、重さ、インクの色
紙の材質、大きさ、色、形
日報を記入する人の名前、所属、年齢
日報の内容、日付、長さ...etc

こうした無数の情報の中から、日報システムに必要な情報を定義します。

例えば、

  • 日報記入者名
  • 日付
  • 内容

といったものを抽出します。これがモデルとよばれるものです。 現実世界においてドメインという概念は非常に多くの情報を含み、複雑なものです。そのドメインをコード上に表現するために、必要な情報のみを抽出し表現しやすくします。

モデルについて理解したところで、ドメイン駆動開発という手法について着目します。

まず、ドメイン駆動設計は以下の3つの概念を中心に考えます。

  • ドメイン(現実世界に存在するある領域)
  • モデル(ドメインから必要な情報を抽出)
  • ソフトウェア

先程、

ドメイン駆動設計とは、ドメインモデリングによってソフトウェアの価値を高めるというアプローチの開発手法です。

と書きましたが、ドメイン駆動設計は次の2つのステップによって成り立っています。

  1. ドメインモデルを継続的に改善する
  2. モデルを継続的にソフトウェアに反映する

このうち、1を実現するために「ユビキタス言語」「境界づけられたコンテキスト」「ドメインエキスパート」といったキーワードとその手法が存在しますが、今回は割愛します。 ざっくりいうと、ドメインに詳しい「ドメインエキスパート」と一緒に共通の言語として「ユビキタス言語」を用いてモデルを改善し続けるのが1つ目のステップです。

今回のゴール

今回は2のモデルを継続的にソフトウェアに反映するについて先に触れたいと思います。
今回参考にしている書籍「ドメイン駆動入門」の中心となるのもモデルをどうソフトウェアに反映するのか、という点です。
ドメイン駆動設計の本質は難しく、なかなか実践に取り入れることのできない概念が多く存在します。まずは具体的なドメイン駆動設計の実装パターンを取り入れることでドメイン駆動設計の本質を理解するための準備をする、というのが今回のゴールです。
ドメインモデルを定義した状態から、具体的にコードに反映するためのパターンについて理解していきます。

ValueObject

さあ、いよいよ具体的なコードの話に入ります。
書籍はC#で記載されていますが、本記事では毎度のことながらPHPでサンプルを書いてみます。

まずは値オブジェクト(ValueObject)パターンです。
プログラミング言語にはプリミティブな値が用意されています。(例: int, string)ちなみにプリミティブには原始初期のような意味があります。

ValueObjectの例として、システムのユーザー名を例にコードで表現すると以下のようになります。

class UserName
{
    public const MAX_LENGTH = 20;

    private string $name;

    public function __construct(string $name){
        $this->validate($name);
        $this->name = $name;
    }

    public function __toString(): string
    {
        return $this->name;
    }

    protected function validate(string $name): void
    {
        if (mb_strlen($value) > self::MAX_LENGTH) {
            throw new InvalidArgumentException('ユーザー名は20文字以下でなければならない');
        }
    }
}

このようにクラスを用いてシステム固有の値を表現したものを値オブジェクト(ValueObject)とよびます。
例えば、システムにおける 「ユーザー名」という値の長さドメインにおける知識です。 適切なモデルをコードに落とし込む際に最適な値は必ずしもプリミティブな値であるとは限らないということになります。
こういった「ドメインモデル」を実装したオブジェクトをドメインオブジェクトと呼びます。

ValueObjectを使用するメリットは、主に以下の3つです。

  • 表現力を増す(クラス名による表現が可能になる)
  • 不正な値を存在させない(バリデーションロジックをコンストラクタで実行することで不正なインスタンスを生成させない)
  • ロジックの散在を防ぐ(関連するバリデーション等のロジックをValueObject内に凝集できる)

ここで発生するであろう疑問として、ValueObjectと他のclassの違いはどこにあるのか?という疑問が考えられます。
その答えに近づくために、そもそも「値」とはどういうものなのかということについて見直す必要があります。
書籍でも紹介されている代表的な「値」の性質には3つあります。

  • 不変である
  • 交換可能である
  • 等価性によって比較される

以下、一つずつ確認していきます。

値の不変性

「値」は不変の性質を持ちます。

private string $greet = 'こんにちは';
$greet = 'Hello';

// Helloが出力される
echo $greet;

$greetという変数が変更されています。どういうことでしょう。

変数というのは中身を変更する際に代入をします。
代入というのは、変数の中身を変更することであり、値自体の変更ではありません。 プログラミング言語の入門書によくある「変数は箱」という例を思い出してみるとわかると思います。
代入という行為は箱の中身を新しい値によって上書きする行為です。 決して値そのものが変更されているわけではありません。

この不変性はソフトウェア開発において大きなメリットになります。生成したインスタンスを知らないところで変更され、意図しない挙動となりバグを引き起こすということは日常的に発生します。変更が原因のバグを発生させないもっともシンプルな対策は不変にすることです。ソフトウェアの世界は複雑で困難なので様々な方法で制約を実現して人間がわかりやすい形にすることが好まれます。(例えば型システム)

デメリットとしては値を変更するたびにインスタンスを生成して代入をしなければならないため、パフォーマンスの面では劣ります(C言語等メモリを意識する言語を触ってみるとわかりやすいかもしれません)。が、現代の特にWeb開発においては明らかにメリットのほうが大きいはずです。

値は交換可能

値というのは「変更」はできない不変性をもっていますが、値の変更自体は必要です。矛盾しているように聞こえますが、プログラミングにおいては私達は常に代入を用いて値の交換を行い、変更を表現しています。

private UserName $name;
$name = new UserName('わたし');
$name = new UserName('あなた');

// あなたが出力される
echo $name;

このとき、$nameは代入によって変更されています。つまり、どちらもUserName型の値ではありますが、インスタンスは全くの別物であり、最初に代入された値オブジェクト自体が変更されているわけではありません。

値は等価性によって比較される

まずはプリミティブ型の値の比較について確認します。

echo (0 == 0); // true
echo (0 == 1); // false

1つ目の式の左辺の0と右辺の0は別のインスタンスですが、同じものとして扱われています。 これは、インスタンス自身ではなく、属性によって比較されているということです。

では、ValueObjectの場合にはどう表現すればよいでしょうか。 1つ目の方法は、値の属性を取り出して比較する方法です。

private UserName $name;
$name1 = new UserName('わたし');
$name2 = new UserName('あなた');

$compareResult = $name1.value == $name2.value

もちろん上記のコードは動作するので一見正しくみえます。 ただ、「ValueObjectは値」なので「値の値」にアクセスしているのは不自然な記述になります。 数値を比較する際に

1.value === 2.value

という書き方をしないことからも不自然だということがわかります。

どうすればいいのかというとValueObject同士が比較できるようなメソッドを用意するのが自然な記述となります。

private UserName $name;
$name1 = new UserName('わたし');
$name2 = new UserName('あなた');

$compareResult = $name1.equals($name2);

この比較用メソッドを用意することの利点

  • 記述が自然になる
  • 新たに属性が追加されても利用側に比較処理を追加しなくてもいい(ValueObject内に比較処理を隠蔽できるため)

ではどういった値をValueObjectとして表現して、どういった値をプリミティブ型のまま扱うのか、判断する基準が欲しくなります。

まず前提として、設計段階でドメインモデルとして抽出したものはValueObjectにするべきです。ドメインモデルに存在する属性の場合は頻出する概念であり、関連するロジックを散在させないためにもValueObjectに凝集しましょう。 次の判断基準として、書籍で紹介されているのが

  • そこにルールが存在しているか
  • それ単体で扱いたいか

の2つです。システム上でルールが存在する場合はルールのチェックロジックを散在させないためにもValueObjectの使用を検討しましょう。 ドメインモデルに存在しない属性で、単体で取り扱いたい概念を発見した場合は、ドメインモデルに追加することを検討し、ValueObject化を検討します。こうした実装中の気づきをドメインモデルに反映することもドメイン駆動設計を支える一つのポイントになります。

エンティティ

こちらもValueObject同様、ドメインモデルを実装したドメインオブジェクトです。

ValueObjectとエンティティの差は同一性によって識別されるか否かです。
ValueObjectはその属性によって識別されるオブジェクトです。
エンティティは同一性(識別子)によって区別されます。

ValueObjectとの違いを意識しながら、エンティティについて理解を進めるとつかみやすい概念かなと思います。

エンティティの性質は次の3つです。

  • 可変である
  • 同じ属性であっても区別される
  • 同一性によって区別される

エンティティの可変性

ValueObjectは不変なオブジェクトでした。変更を表現するためには代入を使用していました。
エンティティは可変なオブジェクトです。エンティティの属性は変化することが許容されています。

例えば、ValueObjectの際にも例に挙げたようにユーザーという概念について考えてみます。

class User
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function changeName(string $name)
    {
        $this->name = $name;
    }
}

上記のように「ユーザー名を変更する」という振る舞いをエンティティに持たせることができます。
ValueObjectと違い、属性が変わったとしてもインスタンスは同じです。
このようにエンティティの属性は変更することが許容されています。

また、書籍ではユーザー名としてのルールをchangeNameというメソッドの中にガード節として書けば良いと記載されていますが、個人的にはせっかくValueObjectを学んだので$name自体をValueObjectにするべきだと思っています。そうすることでコンストラクタからの入力の際にも同じバリデーションを実行できますし、利用する側でも型でどういう意味の値かわかりやすくなります。

エンティティは同じ属性であっても区別される

ドメイン駆動設計のエンティティの同一性の説明でもっともありふれた例は人間です。
ここでは人間のドメインモデルとして氏名(性と名から成る名前)をもつモデルを考えてみます。

同姓同名の人間が二人いる、つまり氏名という属性が一致している場合に同じ人物を指しているといえるでしょうか。 ドメインモデルにほかのあらゆる属性を定義したとして、つまりクローンを作成したとしても人間というのは区別されます。 属性だけでは区別されないのです。
人間において何をもって区別するのか、というのは哲学の領域になってしまいますが、エンティティはこの区別に識別子を使用して区別します。

つまり、エンティティとは識別子(identifier)と属性を持つオブジェクトだといえます。

エンティティは同一性をもつ

先程、同じ属性であっても識別子によって区別されるのがエンティティと書きました。この識別子を持つことでエンティティは同一性という性質も持つことになります。
先程の人間と氏名を例にすると、氏名を変更した場合、氏名の変更前後で別人になってしまうのか?というのが同一性です。
エンティティにおいて、そのエンティティをエンティティたらしめるものは識別子です。 つまり、エンティティのもつすべての属性が変更されようとも識別子が同じであればエンティティは同一のものとなります。

この同一性の比較を行うための最も典型的な実装が、ValueObjectのときと同じく、比較用の振る舞いをもつことです。

class User
{
    private string $identifier;

    private string $name;

    public function __construct(
        string $identifier,
        string $name)
    {
        $this->identifier = $identifier;
        $this->name = $name;
    }

    public function equals(self $user): bool
    {
        return (string)$this->identifier === (string)$user->identifier;
    }
}

ValueObjectと決定的に違う点はValueObjectではすべての属性を比較の対象としていましたが、エンティティの場合は比較処理の対象が識別子のみである点です。

ドメインモデルから実装する際のValueObjectとエンティティの判断基準

ValueObjectもエンティティもドメインモデルを表現するためのドメインオブジェクトであり、非常に似通っています。
ドメインモデルの概念のうち、何をValueObjectにして何をエンティティにすればいいのでしょうか。

たしかに自分自身も普段の業務において無意識にValueObjectとエンティティを区別しているのかうまく言語化できていないなと感じました。 個人的には、概念として異なる複数の属性を持っているものがエンティティかなと思っていましたが、書籍ではより良い基準が紹介されています。

その基準はライフサイクルです。
例えば、ユーザーの場合、サービスに登録し作成された時点で生を受け、退会処理時に死を迎えます。
まさにライフサイクルを持つ概念です。こういった明確にライフサイクルを持つものはエンティティとして表現しましょう。
反対にライフサイクルを持たない、もしくは持つ意味がないオブジェクトはValueObjectとして扱いましょう。

プログラミングにおいて可変性はできる限り避け、不変な値のみを扱うほうがシンプルになります。
このことからも迷った場合もひとまずValueObjectとして表現しておくべきです。

まとめ

ドメインモデルを実装に反映したオブジェクトをドメインオブジェクトといい、ValueObjectやエンティティもドメインオブジェクトの一種です。

ValueObject

  • 不変である
  • 交換可能である
  • 等価性によって比較される

エンティティ

  • 可変である
  • 同じ属性であっても区別される
  • 同一性によって区別される

ドメインオブジェクトを作成し、ドメインの知識をコードにすると、たちまちコードはドキュメントとして機能し始めます。
コードとは別にドキュメントを作成して仕様を表現するというのが一般的ですが、コード上で表現できるのであればより仕様が理解しやすくなります。
また、コードからドキュメントを生成するDoxygenやphpDocumentorをはじめとしたツールを使用する場合にはより効果的に使用することができます。

また、ドメインモデルの変更が発生した場合に、コードへの実装がドメインモデルに沿ったものになっていると、その変更は容易になります。
ソフトウェアは作って終わりにはならないものです。人の営みが移ろうのにあわせてソフトウェアに求められる仕様も変わっていきます。こうした変化への対応を容易にするための一つのアプローチがドメイン駆動設計だと考えています。

所感

今回紹介した書籍はタイトルどおり、ドメイン駆動設計の入門としてかなり有効に使用できるものでした。 特に具体的にコードに落とし込む方法について見当もついていない状態では非常に頼れる道標となりそうです。

ドメイン駆動設計に限らず、プログラミングに向き合うとモデリングという壁にぶつかると思っていて、この本の内容を理解した次のステップはモデリングだと考えています。

今後の流れとしては

  • Repository
  • Factory
  • Service

について紹介しようと思います。

今回参考にさせて頂いた書籍には他にも「集約」や「仕様」等まだまだドメイン駆動設計についての紹介がありますので一度手にとって読んで頂くことをおすすめしたいです。

今回の記事がこれからドメイン駆動設計に入門しようとする人の助けになれば幸いです。

参考

https://qiita.com/little_hand_s/items/721afcbc555444663247