何
- CleanArchitectureを読んで、一部のシナリオをSpring Bootで実装するとどうなるか試したくなった。
地図
- (以下、単純化したおおまかな地図であり、書籍ではそんなことは言っていない/言いきっていないことがあります)
- コンポーネントとは、関連する機能をまとめたものである
- コンポーネントには、詳細から本質までのグレデーションがある
- 詳細は代替可能なもの。データをどこに保存するか(DB、ファイル、クラウドストレージ)や、どう表示するか(WEB、コンソール、PDF、etc..)は詳細に当たる。
- 本質は、エンティティやエンティティを利用するユースケースとなる。
- エンティティは最重要なビジネスルールやデータを持つ。
- ユースケースはアプリケーション固有のビジネスルールを持つ。(詳細を問わない範囲で、エンティティの使い方を定義する)
- 入出力からの距離が遠いほど、レベルが上がる(=本質度が上がる。エンティティはレベルが高くなる)
- 上位レベルのコンポーネントは下位レベルのコンポーネントを意識するべきではない。(エンティティはユースケースに依存しない。ユースケースは詳細に依存しない)
- 上位レベルが下位レベルを意識してしまう場合、中間にインタフェースを用いることにより、抽象への依存に置き換えられる。(依存性逆転の法則)
- 例)ユースケースの中で、「エンティティを保存する」場合に、それが「MySQLに」であることがユースケースの実装に現れてはいけない。インタフェースを挟むことで、「どこかしかるべき場所に保存する」とする
- 依存性逆転の法則自体は、コンポーネントの切り離し方については言及しない。切り離し方には3種類存在する。(ソースレベル、デプロイレベル、サービスレベル)
- ソースレベルは単一プロジェクトの中のクラス構成。デプロイレベルではjarが分かれる。サービスレベルでは隔離レベルが上がり、ネットワーク通信しないとやり取りできない。
- このうち、デプロイレベルでの切り離しをSpring Bootの複数のプロジェクトによって試してみて、pom.xmlの依存関係がどうなるかを確認する。
- 切り離しのレベルが上がるほど、開発や管理のコストはあがる。(例えばjarを分けた時に、動作可能なバージョンをどのように管理・周知するか?)モノシリックな構成が絶対悪なわけではない。(ただし、境界を意識し続けないと、必要が生まれた時にもはや分離できない。)
- 分離するメリットの一例は、プラグインや詳細の切り替えをコンパイルなしでできること。
作成するプロジェクト
artifact-id | 名前空間 | 内容物 |
---|---|---|
my-business | com.example.demo.business | エンティティ、ユースケース、レポジトリインタフェース |
my-repository1 | com.example.demo.repository | レポジトリの実体を持つ(例:mysql向け) |
my-repository2 | com.example.demo.repository | レポジトリの実体を持つ(例:ファイル向け) |
my-runner | com.example.demo.runner | バッチ処理起動メイン |
- 今回のサンプルでは、my-repository1と2の名前空間を同じにした
pom.xmlの依存関係
- my-runnerがmy-repository1を使用している。my-repository2は未使用状態。
ソースサンプル(my-business)
SomeEntity.java
package com.example.demo.business;
public class SomeEntity {
public int id;
public String name;
}
SomeUseCase.java
package com.example.demo.business;
@Component
public class SomeUseCase {
@Autowired
private MyRepositoryInterface repo;
public void doSomething(String name) {
SomeEntity entity = new SomeEntity();
entity.name = name;
repo.save(entity);
}
}
MyRepositoryInterface.java
package com.example.demo.business;
public interface MyRepositoryInterface {
public void save(SomeEntity someEntity);
}
ソースサンプル(my-repository1)
MyRepositoryImpl1.java
package com.example.demo.repository;
import org.springframework.stereotype.Component;
import com.example.demo.business.MyRepositoryInterface;
import com.example.demo.business.SomeEntity;
@Component
public class MyRepositoryImpl1 implements MyRepositoryInterface {
public void save(SomeEntity someEntity) {
System.out.println("save to mysql." + someEntity.name);
}
}
ソースサンプル(my-repository2)
MyRepositoryImpl2.java
package com.example.demo.repository;
import org.springframework.stereotype.Component;
import com.example.demo.business.MyRepositoryInterface;
import com.example.demo.business.SomeEntity;
@Component
public class MyRepositoryImpl2 implements MyRepositoryInterface {
public void save(SomeEntity someEntity) {
System.out.println("save to file." + someEntity.name);
}
}
ソースサンプル(my-runner)
MyRunnerApplication.java
package com.example.demo.runner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages={"com.example.demo.runner, com.example.demo.business, com.example.demo.repository"})
public class MyRunnerApplication {
public static void main(String[] args) {
SpringApplication.run(MyRunnerApplication.class, args);
}
}
MyRunner.java
package com.example.demo.runner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import com.example.demo.business.SomeUseCase;
@Component
public class MyRunner implements CommandLineRunner{
@Autowired
private SomeUseCase someUseCase;
@Override
public void run(String... args) throws Exception {
System.out.println("running runner...");
someUseCase.doSomething(args[0]);
}
}
Spring Bootでこれらを動かすための工夫など
- scanBasePackagesには、全ての名前空間を列挙した。
- 起動ポイントとなるmy-runner以外では、pom.xmlから以下を消した。これがあると
mvn install
の際にプロジェクトの中にmainがないぞと怒られた
pom.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
起動方法
- my-runner以外の各プロジェクトで、jarを作る。
mvn install -DskipTests=true
- my-runnerの起動。
init.sh
$ mvn spring-boot:run -Dspring-boot.run.arguments="hello"
...
running runner...
save to mysql.hello
作ってみての感想。メリット編
- 本質(my-business)から、その他のプロジェクト(my-runner,my-repository1)の利用ができないことにより、「本質が詳細に依存してはいけない」ルールをコンパイルによって強制できる。
- レポジトリの切り替えが、my-runnnerのpom.xmlの書き換えだけで、コンパイルなしでできる
作ってみての感想。デメリット編
- 開発効率が落ちる。うまく動かない時に、さまざまな可能性があるため、切り分けが大変。jarのバージョン管理をしだすとさらに大変になる。
よくわからなかった点
- scanBasePackagesについて、将来的に切り替える可能性のある詳細モジュールが複数ある場合、コンパイルなしでの切り替えを目指す場合は列挙しておかないといけないのかな?
- 開発中のソース修正のたびに
mvn install
をしなくても、IDEが面倒を見てくれるはず、と期待したがうまくいかなかった。どこかに設定が必要な模様。
今後の発展
- ユースケースとエンティティを別のjarに分けるとしたらどうすればよいか?
- ユースケースについている、Springのアノテーションを取り除くためにはどうすればよいか?
- 本の第22章にでてくるユースケース出力ポート(I)、ユースケース入力ポート(I)、ユースケースインタラクターを登場させたらどうなるか?