業務を通じてレイヤー化アーキテクチャを学んだ話
どうもこんにちは。
気づけば年末の振り返りから1ヶ月経ってしまいました...もっとブログを書く習慣を付けたいです。
さて、今回のテーマはレイヤー化アーキテクチャについてのお話です。
前々スプリントで実装した案件が、非常に学びが多いものだったのでブログに載せることにしました。
扱った案件をそのまま書く訳にはいかないので、今回は架空のサービスを考えます。
それは、恋活マッチングサービスです。
仮に、えんむすびという名前を付けましょう。
えんむすびでは、会員は自分のプロフィールを書くことができ、異性の会員はそれを閲覧することができます。しかしながら、このような恋愛に関するサービスではイカガワシイ人物の影が潜むものです。
怪しげな外部サイトへの誘導、イカガワシイ誘い文句などなど...健全なプラットフォームを脅かす会員を根こそぎブロックするべく、会員のプロフィールは審査員がチェックしています。
そんな中、審査員の方から次のような依頼を受けました。
審査員「会員様のプロフィールを審査するときに、禁止ワードをハイライトさせる機能をつけて欲しいです。それから、禁止ワードの追加・削除が気軽にできるようになりませんかね...」
私「なるほど。確かに今の仕様だと、禁止ワードの追加や削除は直接エンジニアがSQLを叩くしかないですもんね。ハイライト機能のついでに、追加や削除を審査員の方ができるようにしましょう!」
審査員の方からの要望を元に、上司や先輩からアドバイスを頂きながら次のような要件定義をしました。
1)禁止ワードの登録・削除・検索ができる専用の画面を作成する
2)禁止ワードの登録・削除・検索は、RDSを参照する
3)会員のプロフィールを審査で表示する際、禁止ワードが存在していた場合はそのワードをハイライトさせる
4)ハイライト時の禁止ワードは、RDSではなくRedisを参照する
5)Redisの禁止ワードは寿命24時間のキャッシュとし、期限切れの際はRDSのデータを新たにセットする
早速何も考えずに実装してみました。
それがこちらです。今見ると目を覆いたくなる酷い実装です...。
RDSとRedisに関する処理をLogicというクラスにひとまとめにしているのが酷いです。
さらに、要件5)を満たすための業務ロジックの中で、RDSとRedisのORマッパーが一緒に呼び出されているという悪夢のようなメソッドを生み出していました。
Redisの世界にRDSへの依存ができてしまっていたのです。
そこでLogicをRDS用とRedis用に分けた修正版がこちらになります。
要件5)の部分は、赤色のActionで実装しました。
これで、少なくともRDSの世界とRedisの世界は分けられたのですが...(実際に実装したのはここまでです)
レイヤー化アーキテクチャにするとしたらどう書けるんだろう?
という興味が沸々と湧いてきたので、 DDDやクリーンアーキテクチャの勉強も兼ねて、レイヤー化を考えてみることにしました。
色々な記事を読んで、レイヤー化にも様々な手法があることを学んだのですが、Spring MVCアーキテクチャが一番今回の案件に適していると感じました。実際に書いてみたのがこちらです。
主にこの2つの記事が参考になりました。
これらを読んでいて、今回の案件はLogicをRepository(ドメイン層)とRepositoryImpl(インフラストラクチャ層)の2つに分けたほうがいいことに気がつきました。
というのも、使用するDBサーバーやORマッパーを柔軟に変えたいと思った時に、あまり業務ロジックと関わって欲しくないからです。
「インターフェイスというのは規格という意味で、その実態がどのように実現されているかどうかは、ドメイン層は知らなくていい」
ということを以前教わったのですが、ようやくその意味を深く理解できるようになりました。「DBサーバーとこんなやりとりをします」という決まり事だけRepositoryに書いて、ORマッパーを使った実装はRepositoryImplに書けば良いのです。
そうすれば、「今度はMySQLじゃなくて、PostgreSQLにしよう」となったときも、DBサーバーとのやりとりは書かれているので、安心してRepositoryImplだけを変更することができます。これは、ドメイン層にとっても優しいし、エンジニアの同僚にとっても優しい仕様だと感じました。
せっかくなのでクラス図の練習をしたいと思い、PlantUMLを使ってレイヤー化した際のクラス図を作ってみました。
ControllerとRepositoryImplの中身は省略しました。
ついでにコードも載せますね。
@startuml skinparam class { BackgroundColor #FFFBF2 ArrowColor #1253A4 BorderColor #FFB600 } interface NgWordRepository { NgWord createNgWord(NgWord ngWord); void deleteNgWord(Long NgWordId); List<NgWord> selectNgWords(NgWordType type); boolean existsNgWords(NgWord ngWord); } interface NgWordCacheRepository { List<NgWord> selectNgWords(NgWordType type); void setNgWords(List<NgWord>); boolean existsNgWords(NgWord ngWord); } class NgWordRepositoryImpl class NgWordCacheRepositoryImpl class NgWordService { NgWord createNgWord(NgWord ngWord); void deleteNgWord(Long NgWordId); List<NgWord> fetchNgWordCache(NgWordType type); } class NgWordController { } NgWordRepository <|..NgWordRepositoryImpl NgWordCacheRepository <|..NgWordCacheRepositoryImpl NgWordService ..> NgWordRepository NgWordService ..> NgWordCacheRepository NgWordController ..> NgWordService @enduml
PlantUMLの使い方はこちら3つのサイトが参考になりました。
Open-source tool that uses simple textual descriptions to draw UML diagrams.
PlantUML を便利に使うための3つの Tips - Qiita
今回業務で2つのDBサーバーを使う案件を実装したことで、レイヤー化アーキテクチャの利点を理解することができました。
百聞は一見に如かずならぬ、百見は一実装に如かずだなと。
どんなに本や記事を見ても、自分で実装するまでその大切さはわかないことを学びました。
もちろん、知っていなければわかる以前の問題なので、本や記事でのキャッチアップは重要です。これからもっともっと読まなきゃです。
でも、同じくらいコードを書かなきゃだめだなとも思いました。
読み書きに励んでいきます。