ArchitectureをNode.js+Typescriptで実装してみるEngineeringAuthornishioこんにちは。バックエンドエンジニアの西尾です。スペースマーケットではAPIサイドは主にRailsを利用していますが、最近は一部サービスでNode.js+Typescriptを使い始めました。個人的にはまだ触れていなかった領域であったのでお勉強がてら簡単なコードを書いてみたいと思っていました。達人に学ぶソフトウェアの構造と設計を読んでいます。Architectureという設計思想について語られています。この最初はいいけど、後から辛いを防ぐというのはどんどん大きくなりつつあるスペースマーケットでも同じことが言えるのではないかという部分に共感を覚え学び始めています。読んでいるとなんとなく原則や思想については分かった気がするが実際にどう実装するのかという部分が見えにくいものがありました。Typescriptを用いて実際のコードに落とし込んでみようと思います。Architectureは関心事の分離をするという目的を達成するための1つの手法として提唱されています。Architectureでは、4層の円が描かれており、各円はソフトウェアの領域を表しています。重要なルールとして、依存性は内側だけに向かっていなければならないとしています。達人に学ぶソフトウェアの構造と設計)を参照ください。今回はこの思想を元に実際のコードに落とし込んでみます。Architectureを適用しようと思います。するという4つの機能となります。フロントは作らず、サーバーサイドの実装のみ行います。使用技術は主にTypeScriptNode.jsExpressMySQLの4点になります。ディレクトリ構成ディレクトリ構造は上記のようにしてみました。次から各層ごとの実装について説明します。Rulesを格納する場所としました。今回はその配下modelsにTaskのモデルを配置します。今回は複雑なロジックもなく単純なCRUDの実装のみなのでTask.tsについてはデータ構造とアクセサメソッドだけになります。Rulesを格納する場所とし、配下にusecasesとrepositoriesの2ディレクトリとしました。DeleteTask.ts上記5ファイルを格納しています。repositories配下には円の境界を越えるために具象クラスではなく抽象クラスを格納しusecasesから使用するようにしています。円の境界を超える部分について少し説明すると、ユースケースのCreateTask.tsでやることはtitleとdescriptionを受け取って、それを永続化するということをやりたいです。これを単純に書くと次のような形になるかと思います。}}しかし、この書き方では永続化するためにDBのpoolを受け取っているため、DBに依存していることがわかります。Architectureのルール違反となってしまいます。やってこの境界を越えるのかというと依存関係逆転の原則(DIP)を使って、具体的にはインターフェースや継承を使って解消します。上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。Promise<Task>}ITaskRepositoryについては、どこにファイルを配置するか悩みましたが、今回はapplication配下にrepositoriesというフォルダを切り格納しています。ITaskRepositoryではインターフェースのみを定義し、実際に保存するという具体的な処理は記載していません。CreateTask.tsはconstructorでITaskRepositoryを受け取り、ITaskRepositoryという抽象クラスに依存している状態になります。taskRepositoryのインスタンス化は円の外側のレイヤーで行うことになります。Redisに保存するためのRedisTaskRepositoryであろうが、CreateTaskクラス内では詳細を気にしなくてよくなり(データを永続化するという抽象的なことには依存しているけど、DBにデータを永続化するという具体的なことには依存していない)、関心事が分離されている状態になります。このようなDIPを用いて円の境界を超えることができました。レイヤーinterfacesディレクトリ配下は、controllersとdatabase,及び出力向けにjson形式にするserializersの3つとしました。円の中のGatewaysにあたるdatabaseは次のような実装となりました。<他はここでは省略>}usecase部分で登場したITaskRepositoryの具象クラスをここで定義しています。やっていることはmodelデータを受け取り、実際にDBに保存するということをしています。DB接続についてはここでは具体的には定義せず、usecaseの時と同様にDIPを用い、1つ外側のレイヤーに具体的な処理を書き、ここではIDBConnectionという抽象クラスに依存するようにしました。packageであろうが、その他独自のpackageであろうがここでは意識しなくて良いことになります。<他はここでは省略>}Controllerではrequestを解析、CreateTaskのuseCaseを実行しDBにデータを永続化、その後出力向けにデータを変換(serializers)して返しています。先ほどユースケースで登場したtaskRepositoryのインスタンス化もここで行なっています。次にserializersについては円で言うとpresentersにあたり、次のようなコードを実装しました。最後に円の一番外側の部分について実装していきます。DB接続部分と実行部分です。mysqlを使っています。}}続いてルーディング部分です。ここでExpressフレームワークが登場します。router最後にserver.tsを記載します。appこのようにフレームワークやドライバーの部分は円の一番外側に記載しました。最終的なツリー構造は下記のようになりました。Architectureに従っていくと関心事が分離されるのでソフトウェアが疎結合になるというのを少し実感することができました。他の方も言われているように、クラスが多くなったり、コードの記述量が増えることから、初期にとりあえず機能を提供することを優先するといった時には向かないということも実感できました。初期のフェーズは終わり、1つ次のステージに向かうためには初速の開発速度を犠牲にしてもこのようなアーキテクチャーを導入し、システムを疎結合にしておくと後々開発速度が鈍足になることを防げるのではないかと思いました。今回は非常に簡単なコードでテストとかも実装していないので、後々楽になると言う部分をなんとなくの概念でしか体感することはできなかったので、もう少し複雑にした時に見えてくる世界もいつか体感してみたいと思います。クリーンアーキテクチャーについて理解できていない部分も多いので(右下の図が何を示しているのかなど)引き続き調べていきたいとも思いました。今回はTypescriptで初めて実装してみたのですが、個人的な感想としては、やってみるとめちゃめちゃ型定義しろしろ言われ、普段型なし言語を使っているためか結構うっとおしかったです。固い感じのシステムを作る際にはやはり型は定義した方が良いのかなとも思いました。(あまりにうっとおしいので今回は一部anyにしちゃったところがありますが、、、)今回のサンプルコードはこちらにあげています。作る上で参考にした文献についても下記に記載します。Serverを構築してみる最後にスペースマーケットでは、現在以下の募集を行っております。(https://www.wantedly.com/projects/222428)データ分析エンジニア(https://www.wantedly.com/projects/211321)フロントエンドエンジニア(https://www.wantedly.com/projects/226855)AuthornishioスペースマーケットのWebエンジニアです。エディタはバックエンドVim、フロントVSCodeを使います。