はじめに:アーキテクチャは難しい!
世の中には様々なアーキテクチャがありますが、ものすごく複雑だったり、実装するには難しかったりするものばかりです。特にDDDやクリーンアーキテクチャは概念がややこしく、自分も理解するのに苦しんでいます。
そこで、色々と試していくうちにできた、自分なりに納得のいくアーキテクチャと考え方を紹介します。
(参考)
STEP1:関心を分離する
まず、アーキテクチャで一番大事なのは関心の分離です。つまり、どう役割分担するかです。ここではドメイン層、プレゼンテーション層、リポジトリ層、アプリケーション層に分けていきます。
ドメイン層~知識の管理~
まずドメイン層というところに、業務に関する知識を集めてきます。どういうことかというと、今からやりたい業務のルールや知識をまとて、専門家みたいなのをつくるイメージです。
例えば買い物アプリだと、
- 支払い料金は買い物かごに入っている商品の合計
- 割引券を持っている場合、最大5000円まで割引できる
- 消費税は10%
といった、知識を管理するクラスや関数をつくります。
このドメイン層を利用している限り、間違って消費税を7%で計算してしまったり、5000円以上割引してしまったりということがありません。つまり、ドメイン層は正しい計算をしていることを保証する場所ともいえます。
具体的な実装では「エンティティ」、「バリューオブジェクト」、「ドメインサービス」というものを作ってきます(後述)
プレゼンテーション層~見た目と入力の管理~
プレゼンテーション層ではアプリやサイトの表示を管理します。ポイントはどう表示すればいいかだけ気にしていればいいというところです。それまでにどんな計算がされたか、どういった処理が必要だったかは気にせず、受け取った結果を表示することだけを考えます。
さっきの買い物の例だと
- 合計金額を「○,○○○円」という形式で表示する
ということに専念します。ちょうどディスプレイみたいな感じです。
ここの実装方法は使っているフレームワークやライブラリによって大きく変わってくると思います。逆にいうとフレームワークやライブラリのめんどくさい部分をここに集中させれることもあります。
また、ユーザーからの入力をアプリケーション層に伝えます。
リポジトリ層~データの管理~
リポジトリ層では発生したデータの管理をします。データの置き場って感じです。ポイントはレポジトリ層は変数みたいに使えることです。なんの言語でも構いませんが、リスト型の変数を思い浮かべてみてください。そのリストにはどこからでも、データを追加したり、削除したりすることができます。また、そのリストは勝手にデータベースを操作してくれて、永続化しないといけないものは自動でやってくれます。この魔法の変数みたいなのがリポジトリです。
アプリケーション層~実際にアプリを動かす~
ここまでドメイン層、プレゼンテーション層、リポジトリ層とでてきましたが、まだそこにいるだけで何もしません。彼らをうまく利用し、実際にアプリを動かしていくのがアプリケーション層になります。指示役です。
逆にいえば、この指示役は業務の知識や表示の方法、データ管理の仕方を気にすることなく、何がしたいかだけ言えばいいという状態になります。
実際アプリの価値はこの指示役がどんなことをするかで決まってくるので、無駄なことを気にせず価値の創造に専念することができます。
STEP2:実装
ドメイン層、プレゼンテーション層、リポジトリ層、アプリケーション層の実装方法を紹介します
ドメイン層
ドメイン層では「エンティティ」、「バリューオブジェクト」、「ドメインサービス」というものを実装していき、知識を詰め込んでいきます。
エンティティ(Entity)
エンティティでは独自の識別子(id)を持ち、変更されることがあるクラスです。現実世界において、同じ状態だとしても見分けることが必要なモノのことたちです。
具体例としては「ユーザー」などがあります。「ユーザー」はたとえ同じ年齢や名前を属性として持っていたとしても、かならず「ユーザー」どうしは見分ける必要があります。
TypeScriptで実装してみます
class User{
id : number;
name : string;
age : number;
constructor(id : number, name: string, age : number){
this.id = id,
this.name = name,
this.age = age
}
}
ここに必要な知識や計算を追加していきます。
例えばユーザーが18歳以上なら成人という知識(ルール)を追加します
class User{
id : number;
name : string;
age : number;
constructor(id : number, name: string, age : number){
this.id = id,
this.name = name,
this.age = age
}
//追加
isAdult() : boolean {
if(this.age >= 18){
return true;
}else{
return false;
}
}
}
※18という数字は定数にして扱ったほうが好ましいですが、ここでは面倒くさいので、そのままにしてます。
これで、どこかで20歳以上を成人と間違えて判断することはなくなります。また成人年齢が変わったとしても、ここだけ修正すればよくなります。
またエンティティがエンティティを所有することもあります。できるだけ現実と近いように表現するのが目標です。
バリューオブジェクト(ValueObject)
バリューオブジェクトとはエンティティとは逆で識別子(id)がない、名前のとおり「値」を表すクラスです。日付、金額、住所などがバリューオブジェクトの例です。idがないので状態が同じものであれば同一のものと認識します。1000円札だったらどの1000円札も同じ価値を持ち、同一の1000円札として扱うのと同じです。
ここではユーザーネームをValueObjectにして実装してみます。
class UserName{
firstName : string;
familyName : string;
constructor(firstName : string, familyName : string){
this.firstName = firstName;
this.familyName = familyName;
}
equal(userName : UseName) : boolean{
if(this.firstName == userName.firstName && this.familyName == userName.familyName){
return true;
}else{
return false;
}
}
}
「値」なので比較できるようequal()メソッドを実装しています。もし数値系のバリューオブジェクトなら足し算や引き算メソッドを実装していてもいいと思います。
ここにも必要なら知識を追加します。
class UserName{
~~省略~~
//追加
getFullName() : string{
return this.firstName + " " + this.familyName;
}
}
バリューオブジェクトの何が嬉しいかというと、コードの可読性が向上することと、バグの防止になることです。
全部numberやstringで宣言していくよりも、それぞれの型にちゃんと名前があったほうが読みやすくなります。また、コンストラクタに制約をつけておくことで不正な値(年齢が-3歳など)にならないことを保証できます。そのためバリューオブジェクトは一度生成したら、その後変更できないようにし、値を変更するときは新たに生成するのが望ましいです。
こんな感じです。
let userName = new UserName("taro","tanaka");
userName.firstName = "hide" //NG
userName = new UserName("hide", "tanaka"); //OK
ただValue Objectは実装の手間にたいする費用対効果が薄いと感じていて、特別こんな型があったいいなと思ったり、ややこしいものに限って作ったりしています。
どこまで真面目に実装するかは好みだと思います。
ドメインサービス(Domain Service)
エンティティとバリューオブジェクトを作っていき、それでも書ききれなかった知識はドメインサービスとして実装します。関数として実装していきます。
例えば口座から口座にお金を移す処理などがこれに該当するかと思います。
あるユーザーのポイントを他のユーザーに移動するのを例に実装してみます。
function movePoint(fromUser : User, toUser : User, amount : number){
fromUser.point -= amount;
toUser.point += amount;
};
※細かい部分は省略
知識やルールを管理する方法としてドメインサービスは最後の手段というイメージで、できるだけエンティティやバリューオブジェクトを利用したほうが、可読性的にも、バグの防止にも望ましいと思われます。
リポジトリ層
リポジトリ層ではリスト型の変数みたいなインスタンスを作っていきます。つまり、クラスをつくり、それを実体化して提供するシステムまで作ります。
なんのリストかというと、先ほど作ったドメイン層のエンティティのインスタンスを収納するリストです。
まずリポジトリのインターフェイスを作成します。
インターフェイスを作っておくことによって、リポジトリの入れ替えが簡単になります。特にリポジトリはDBやAPIしたりと環境依存する要素が多いので、テスト用のリポジトリに簡単に入れ替えれるようにすると快適にデバックできます。
interface IUserRepository{
add(user : User) : void;
remove(user : User) : void;
getUserById(id : number) : User;
}
次にインターフェイスを実装したもの作っていきます
class UserRepository implements IUserRepository{
add(user: User): void {
~~省略~~
//DBに保存する必要がある場合その処理もここに
}
remove(user: User): void {
~~省略~~
}
getUserById(id: number): User {
~~省略~~
//DBから取り出したり、キャッシュを利用したり
}
DB保存の処理などもこのリポジトリ内で実装してしまいます。
こうすることで、外からリポジトリを使う側はまるでリストを扱っているかのようにして、データ永続化までできちゃいます。
リポジトリはみんなで同じインスタンスを共有したいので、シングルトンにします。(参考↓)
class UserRepository implements IUserRepository{
private static _instance: UserRepository;
private constructor(){}
public static get instance(): UserRepository {
if(!this._instance) {
this._instance = new UserRepository();
}
return this._instance;
}
add(user: User): void {
throw new Error("Method not implemented.");
}
remove(user: User): void {
throw new Error("Method not implemented.");
}
getUserById(id: number): User {
throw new Error("Method not implemented.");
}
}
これで
UserRepository.instance;
で同じインスタンスを手に入れることができます。
クラスだけあっても仕方ないのでリポジトリのインスタンスを提供する関数も作ります。
function useUserRepository() : IUserRepository{
return UserRepository.instance
}
なぜこんな関数を作るかというと、レポジトリのインスタンスを取得する処理をここに共通化させることで、テストのときなどに簡単に中身の入れ替えが容易になるからです。
例)
class FakeUserRepository implements IUserRepository{
//DBなどを利用しないでそれっぽいものを実装
~~省略~~
}
function useUserRepository() : IUserRepository{
if(mode=="test"){
//テストのモードの時は、DBなどを利用しない簡易的なリポジトリに入れ替える。
return FakeUserRepository.instance
}else{
return UserRepository.instance
}
}
使う側はこんな感じです
const userRepository = useUserRepository();
userRepository.add(user);
アプリケーション層
アプリケーション層では各層を組み合わせ、実際のアプリの機能となるものをつくっていきます。基本的には機能ごとに関数を実装していけば十分ですが、クラスをつくりその中の静的関数として実装してておくと、一時的に変数を保存したかったときに便利だったり、関数を分野ごとに整理できたりするのでおすすめです。また静的にしておくことで、クラスをインスタンス化する必要がないので、扱いやすくなります。
新しくユーザーを作成する機能を具体例に実装してみます。この機能はUserHandleUseCaseで括られているとします。
class UserHandleUseCase{
static createUser(name : string, age : number){
const user = new User(name, age);
useUserRepository().add(user);
usePresenter().showUser(user); //プレゼンタ―については後述
}
}
これらの関数はプレゼンテーション層、つまり、ページのクリックイベントなどから発火されます。
アプリケーション層はプレゼンテーション層と連携する必要があります。プレゼンテーション層はフレームワークやライブラリの影響を強く受ける部分なので、それに応じてアプリケーション層も柔軟に形を変えて実装していくのがいいかと思います。
プレゼンテーション層
プレゼンテーション層ではアプリの見た目の部分と、それをアプリケーション層と連携するものを実装します。
プレゼンテーション層はフレームワークやライブラリによってどういう実装になるか大きく変わっていく部分なので、具体的な実装例は省略します。
プレゼンテーション層として実装すべきポイントは2つで、まずアプリケーション層から画面の変更を行えるようになっていることと、プレゼンテーション層からアプリケーション層の機能を発火できるようになっていることです。
この部分は変に自分で全部で実装しようとせず、フレームワークやライブラリに頼って実装していくのがシンプルでいいと感じます。
最後に
ここではドメイン層、プレゼンテーション層、リポジトリ層、アプリケーション層で考えてきましたが、必要に応じて新しく層を作ったり、減らしたりする必要があると思います。(認証層とか、セキュリティ層とか)
とにかく、書きやすい、読みやすい、テストしやすいを第一に実装していくのがいいかなと思います。