やっつけ改修になりがちな社内システムで,ドメイン駆動設計(DDD)を実践してみたい。
コピペしまくりの結果、コード量が100万行を超えたモノリスでくたびれた自社システムがある(本システムを介しての年商は100億円を超えたくらい)。
この数か月、このシステムのIoT対応に携わっている。予算との関係でその一部を内製することになった。関連するシステムはこれから1~2年でガラッと作りかえられていく予定。そうした変化を織り込むためにも、これを機会に内製部分をドメイン駆動設計していきたい。他の業務の合間に設計と実装を進めていくので、本シリーズは随時更新という形で。
内製するシステムの設計は次回以降に行うとして、今回は、私が採用するドメイン駆動設計
シリーズを通じての参考文献
- Robert C.Martin, "Clean Architecture 達人に学ぶソフトウェアの構造と設計", 2018
- Vaughn Vernon, "Reactive Messaging Patterns with the Actor Model: Applications and Integration in Scala and Akka", 2015
- 『実装クリーンアーキテクチャ』(クリーンアーキテクチャの先達によるプロダクト採用事例)
『The Clean Architecture』
はじめに、『実装クリーンアーキテクチャ』さんが引用している、クリーンアーキテクチャの概念図を私も引用させていただく。
私なりの解釈でざっと説明しておく。エンタープライズシステムのコード実装は、クリーンアーキテクチャに基づくと以下のように解釈できる。
- エンティティ層の実装は、「企業の稼ぐ力の原動力としてのビジネスルール、最重要のビジネスデータ」を扱う。いわゆるコアビジネスと密に関わり、高度に抽象化されている。
- ユースケース層の実装は、「コアビジネスを時流に合わせ伸ばしていくために必要となるビジネスルール」を扱う。一例としては、物販系のシステムでは、webでの販促キャンペーンに関する実装。
- アダプター(interface adapter)層の実装は、「ビジネスルールとUI、UX、DBのつなぎ込み」を扱う。この層の実装には、正しい意味でのMVCが含まれる。ビジネスルールを入れてはならない。
- 最外層(frameworks&drivers)の実装は、時々で採用するフレームワーク(例,Spring Boot,PlayFramework)、データベース、OSコードの呼び出し、他システムとの連携などを扱う。究極の詳細(具象)である《main》もここに含まれる。
エンティティ層の実装は、ほとんどが、企業の機密情報となる。企業の稼ぐ力の秘伝のタレのようなもの。ということで、エンティティ層については、本記事ではこれ以降言及しない。正しく設計されたシステムであれば、企業経営が順調な限り、エンティティ層のコードはほとんど変化しない(はずである)。
実装経験豊富な方ほど、他方の最外層に、データベースと並んでフレームワークが置かれていることに違和感や不安感を覚えるかもしれない。あるいは、フレームワークと独立してビジネスルールを記述することを、理想論として理解はできても、本当に限られた納期で実装できるのだろうかといった思い。こうした違和感や思いは《幻想》にすぎないのかを、クリーンアーキテクチャに則った設計と実装を通じて検証してみようというのが、本シリーズ。
注.クリーンアーキテクチャに従った実装は、モノリスかマイクロサービスかを問わない。
本日の記述は、あたかもモノリスなシステムを茶化しているように読めてしまうかもしれないのではじめに書いておく。クリーンアーキテクチャに従った設計では、アーキテクチャは、モノリスを用いるかマイクロサービスを用いるかといったことがらは、実装の詳細として、時々の必要に応じ決めていく事柄である。例えば、ビジネスモデルを実現するためにweb系のシステムが必要となった際、モノリシックに実装を進めていった方がよいと判断した場合は、PlayFramework等のWAFを活用して実装していく。複数のマイクロサービスとした方が良いと判断した場合は、akka等のreactive frameworkを用いたリアクティブ・アーキテクチャとして実装を進めていく。
すでに各所のて述べられているように、正しく設計されたモノリシックなシステムは、大規模化に従いマイクロサービスへと移行することも比較的容易である。
今回、私は、既存のモノリシックなシステムを改修するコード量を最小にすることが,保守性の観点から望ましいと判断したため、グルーシステムとしてマイクロサービスを用いるアプローチを取ることとした。
クリーンアーキテクチャの《矢印》の意味
同じく、『実装クリーンアーキテクチャ』さんにならい、
上図のの《矢印》、すなわち、クリーンアーキテクチャにおける依存の方向性をコードで軽く確認しておこう。
ここでは、scalaを用いて確認していく。C# の方が馴染みのある方は、こちらも合わせて参考のこと。
import scala.util.Try
case class User(name:String)
class UserRepository{
private var users = List[User]()
def add( username:String) = Try {
// ここにデータの永続化方法を実装していく予定
val newuser=User(username)
users = users :+ newuser
}
def getAll = users
//その他、updateなどの実装を加えていく予定
}
abstract class CreateUser{
def addToRepository(name:String):Try[Unit]
}
class SystemUser extends CreateUser{
private val myUserRepository = new UserRepository
//UserRepositoryを持つ(has:依存)
def addToRepository(name:String) ={
myUserRepository.add(name)
}
def showAll = {
val users = myUserRepository.getAll
users.foreach {u => println(u)}
}
}
※ これは《矢印》の意味を確認するためだけの書捨てコード。
抽象クラスCreateUserが、保持(コンポジション)している UserRepository は具象クラス。
この場合、CreateUserは、UserRepositoryに依存している。逆に、UserRepositoryは、CreateUserのことは知らない(依存していない)。このように一方向的な依存関係でシステムを記述していく、ということが、クリーンアーキテクチャに基づく設計・実装のキモとなる。
- has-a-関係についての参考 学習メモ:コンポジションと集約
- scalaのTryが気になった人はこちら
さて、scalaの場合、REPLを用いて、上のコードを簡単に試すことができる。
> scala
Welcome to Scala 2.12.7 (Java HotSpot(TM) 64-Bit Server VM, Java 11.0.1).
Type in expressions for evaluation. Or try :help.
scala> :paste
// Entering paste mode (ctrl-D to finish)
import scala.util.Try
case class User(name:String)
class UserRepository{
private var users = List[User]()
def add( username:String) = Try {
// ここにデータの永続化方法を実装していく予定
val newuser=User(username)
users = users :+ newuser
}
def getAll = users
//その他、updateなどの実装を加えていく予定
}
abstract class CreateUser{
def addToRepository(name:String):Try[Unit]
}
class SystemUser extends CreateUser{
private val myUserRepository = new UserRepository
//UserRepositoryを持つ(has:依存)
def addToRepository(name:String) ={
myUserRepository.add(name)
}
def showAll = {
val users = myUserRepository.getAll
users.foreach {u => print(u)}
}
}
// Exiting paste mode, now interpreting.
import scala.util.Try
defined class User
defined class UserRepository
defined class CreateUser
defined class SystemUser
では、SystemUserを使ってみよう。
scala> val sysUsers = new SystemUser
sysUsers: SystemUser = SystemUser@737ff5c4
scala> val users=Array(
| "モノリシックシステムユーザー1",
| "分散システムユーザー1",
| "モノリシックシステムユーザー2",
| "分散システムユーザー2",
| "モノリシックシステムユーザー3",
| )
users: Array[String] = Array(モノリシックシステムユーザー1, 分散システムユーザー1, モノリシックシステムユーザー2, 分散システムユーザー2, モノリシックシステムユーザー3)
scala> for (name <- users) sysUsers.addToRepository(name)
scala>
scala> sysUsers.showAll
User(モノリシックシステムユーザー1)User(分散システムユーザー1)User(モノリシックシステムユーザー2)User(分散システムユーザー2)User(モノリシックシステムユーザー3)
「あっ、showAllメソッドで改行するの忘れた!」と気づいた時には、当然、UserRepository等のクラスのことは意識せずに、SystemUserクラスを改修できる。
scala> :paste
// Entering paste mode (ctrl-D to finish)
class SystemUser extends CreateUser{
private val myUserRepository = new UserRepository
//UserRepositoryを持つ(has:依存)
def addToRepository(name:String) ={
myUserRepository.add(name)
}
def showAll = {
val users = myUserRepository.getAll
users.foreach {u => println(u)}
}
}
// Exiting paste mode, now interpreting.
defined class SystemUser
scala> sysUsers.showAll
User(モノリシックシステムユーザー1)User(分散システムユーザー1)User(モノリシックシステムユーザー2)User(分散システムユーザー2)User(モノリシックシステムユーザー3)
scala> val sysUsers = new SystemUser
sysUsers: SystemUser = SystemUser@6e68ccc9
scala> val users=Array(
| "モノリシックシステムユーザー1",
| "分散システムユーザー1",
| "モノリシックシステムユーザー2",
| "分散システムユーザー2",
| "モノリシックシステムユーザー3",
| )
users: Array[String] = Array(モノリシックシステムユーザー1, 分散システムユーザー1, モノリシックシステムユーザー2, 分散システムユーザー2, モノリシックシステムユーザー3)
scala> for (name <- users) sysUsers.addToRepository(name)
scala>
scala> sysUsers.showAll
User(モノリシックシステムユーザー1)
User(分散システムユーザー1)
User(モノリシックシステムユーザー2)
User(分散システムユーザー2)
User(モノリシックシステムユーザー3)
このあとも、より良い振る舞いとすべく、好きなようにSystemUserクラスに手を加えていける。
クリーンアーキテクチャの《矢印》に従うことの大事さ
ということで、クリーンアーキテクチャにおける《矢印》は、多少でもコードを書いた経験がある人であればなんということもないあたりまえの依存関係であることが分かった。だがしかし、クリーンアーキテクチャの《矢印》に従い、システムを設計・実装・改修し続けていくことは、必ずしも容易ではない。
急ぎの改修を重ねたシステムなどでは、こうした依存関係がおかしなこと(例、矢印が双方向になったり循環参照したり)になって、くたびれていくことがありがち。
もひとつ大事なhumble objectパターン
クリーンアーキテクチャ本は、《矢印》の他にもひとつ単純だけれど大切なhumble objectパターンについて言及してくれている。
個人的に風邪ひいてる中徹夜になってしまったので、どこまで書けるかだけれど、DDD入門者としてhumble objectパターンに触れておきたい。ネタとしては、humble objectに従っていないアンチパターンなシステムを3つがっつり見てきた(見ている)ところかな。