投稿日:
2019年4月10日
最終更新日:
【マイクロサービス】流行りのSpring Boot 2 + Gradle + Java8でマルチプロジェクトな個人開発をしていこう レイヤー構造化編【DDD(ドメイン駆動設計)】
YouTubeも見てね♪
Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
レッドブル エナジードリンク 250ml×24本
翼を授けよう!
モンスターエナジー 355ml×24本 [エナジードリンク]
脳を活性化させるにはこれ!
ドラゴンクエスト メタリックモンスターズギャラリー メタルキング
みんな大好き経験値の塊をデスクに常備しておこう!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
サンディスク microSD 128GB
スマホからSwitchまで使える大容量MicroSDカード!
目次
土台は整ったのでAPIモジュールとして動かそう
前回の記事では、APIとして最低限動くところまでご紹介しました。
今回はレイヤー構造を整えていこうと思います。
手順
プロジェクトの構成
まずは各プロジェクトの構成を整理していきましょう。
今回は以下のような構造にしてみようと思います。
プロジェクト | 説明 |
---|---|
apiプロジェクト | RESTfulなユーザーインターフェースを管理 ※ service やmodel はAPIやWEB、BATCH単位で異なるのでこの中にmodel/service パッケージを切っていきます |
datasourceプロジェクト | APIアクセスやDBアクセスなどを管理 |
entityプロジェクト | DBテーブルなどのEntityを管理 |
valueプロジェクト | 項目単位のValueオブジェクトを管理 |
果たしてこれが正解なのかは分かりませんが、一旦はこちらで進めてみようと思います。
考え方によっては、ServiceとModelも別プロジェクトにして参照する場合もありますが、API用のモデルやサービスをWEBやBatchで使うことはなく、必要のない依存の結果無駄に容量が大きくなってしまうので、上記のようなプロジェクト構成にしています。
ただ、今回はレイヤー毎にプロジェクトを分けないだけで設計的にはMVC構造を考慮して行く想定でいます!
valueプロジェクト
まずは一番コアとなるvalueプロジェクトです。
このプロジェクトでは名前やメールアドレス、住所などの一項目レベルを管理していきます。
モデル毎にフィールドを用意するとバリデーションや記述がばらけてしまうため、一元管理がしにくくなってしまいます。
そこで、valueプロジェクトで管理することでそのサービスに登場する項目やバリデーションにブレがなくなるので、バグや仕様のねじれなども抑えることが出来るかな?と言う考えです。
entityプロジェクト
次はentityプロジェクトです。
こちらはデータ構造に対するentityモデルを管理するプロジェクトです。
例えば、DBのテーブルモデル等はAPIやWEB、Batchから見て必ず同一である必要があるため、このentityプロジェクトで管理して行く想定です。
datasourceプロジェクト
次はdatasourceプロジェクトです。
こちらは外部APIやDatabaseアクセスなどを行うクラスを管理するプロジェクトです。
こちらも同じく、外部APIやデータベースは、APIやWeb、バッチで共通なのでプロジェクトとして切り出しています。
apiプロジェクト
最後にapiプロジェクトです。
こちらはこれまでの手順で作ってありますが、この中にサブパッケージとして、controller/model/service
パッケージを構築して行く想定です。
modelパッケージ
entityモデルと違い、I/Oモデルは必ずしもentityと一致するわけではないので、そのクッションをこのmodelで吸収して、共通のEntityモデルへと変換する想定です。
serviceパッケージ
こちらはAPI単位の業務処理を管理して行きます。
同じ会員登録でも、APIからの処理とバッチからの処理で業務内容が異なる可能性があるので、serviceも別プロジェクトにせずにAPIプロジェクトの一部として行きます。
controllerパッケージ
こちらは言わずもがなですが、RESTfulなAPIを構築するために必要不可欠なパッケージです。
infrastructureパッケージ
最後にinfrastructureパッケージです。
こちらはAPIモジュールに関する基盤周りの設定や共通定数等を管理していきます。
各プロジェクトの作成
では、実際に各プロジェクトを追加していきましょう。
ディレクトリ構成
ディレクトリ構成は以下のような形になりました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
.
├── README.md
├── api
│ ├── build.gradle
│ └── src
├── datasource
│ ├── build.gradle
│ └── src
├── devtools
│ ├── api
│ ├── build.gradle
│ ├── gradle
│ ├── gradlew
│ ├── gradlew.bat
│ ├── property.gradle
│ └── settings.gradle
├── entity
│ ├── build.gradle
│ └── src
└── value
├── build.gradle
└── src
|
各プロジェクトのビルド関連ファイル
次に、各プロジェクトのビルド関連ファイルの設定を見ていきましょう。
今回はdevtools
プロジェクトのbuild.grdle
は特に変更ないので割愛します
devtoolsプロジェクト
1
2
3
4
5
6
7
8
9
10
11
|
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'devtools'
includeFlat 'value'
includeFlat 'entity'
includeFlat 'datasource'
includeFlat 'api'
|
1
2
3
4
5
6
7
8
9
|
ext {
accountService = [:]
accountService.springBootVersion = '2.1.3.RELEASE'
accountService.sourceCompatibility = 1.8
accountService.group = 'tech.blogenist.service'
accountService.version = '0.0.1-SNAPSHOT'
accountService.lombokVersion = '1.18.6'
}
|
apiプロジェクト
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
springBoot {
mainClassName = 'tech.blogenist.service.account.AccountApplication'
}
repositories {
}
dependencies {
implementation project(":value")
implementation project(":entity")
implementation project(":datasource")
implementation "org.springframework.boot:spring-boot-starter-web:${accountService.springBootVersion}"
implementation "org.springframework.boot:spring-boot-devtools:${accountService.springBootVersion}"
compileOnly "org.projectlombok:lombok:${accountService.lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${accountService.lombokVersion}"
}
|
datasourceプロジェクト
1
2
3
4
5
6
7
|
repositories {
}
dependencies {
implementation project(":value")
implementation project(":entity")
}
|
entityプロジェクト
1
2
3
4
5
6
7
8
|
repositories {
}
dependencies {
implementation project(":value")
compileOnly "org.projectlombok:lombok:${accountService.lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${accountService.lombokVersion}"
}
|
valueプロジェクト
1
2
3
4
5
6
7
|
repositories {
}
dependencies {
compileOnly "org.projectlombok:lombok:${accountService.lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${accountService.lombokVersion}"
}
|
ポイント
ポイントは依存関係の優先度を考えて、dependencies
句にてプロジェクトを参照する必要があります。
また、今回はlombok
も使いたいので事前に依存関係に追加しています。
APIをそれぞれのレイヤーのクラスを使うように修正
では、プロジェクト構成が整ったところで前回作成したAPIを改修していきましょう。
今回の修正で以下のようになります。
valueプロジェクト
AccountIdValueクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package tech.blogenist.service.account.value.account;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor( staticName = "of" )
@NoArgsConstructor( staticName = "empty" )
@Data
public class AccountIdValue
{
private Integer value;
}
|
AccountNameValueクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package tech.blogenist.service.account.value.account;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor( staticName = "of" )
@NoArgsConstructor( staticName = "empty" )
@Data
public class AccountNameValue
{
private String value;
}
|
AgeValueクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package tech.blogenist.service.account.value.account;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor( staticName = "of" )
@NoArgsConstructor( staticName = "empty" )
@Data
public class AgeValue
{
private Integer value;
}
|
SexValueクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package tech.blogenist.service.account.value.account;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor(staticName = "of" )
@NoArgsConstructor(staticName = "empty" )
@Data
public class SexValue
{
private Type value;
@AllArgsConstructor
public enum Type
{
NONE( "未設定" ),
MALE( "男性" ),
FEMALE( "女性" );
@Getter
private String lavel;
}
}
|
entityプロジェクト
AccountTableEntityクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package tech.blogenist.service.account.entity.db.account;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.blogenist.service.account.value.account.AccountIdValue;
import tech.blogenist.service.account.value.account.AccountNameValue;
import tech.blogenist.service.account.value.account.AgeValue;
import tech.blogenist.service.account.value.account.SexValue;
@Builder
@AllArgsConstructor( staticName = "of" )
@NoArgsConstructor( staticName = "empty" )
@Data
public class AccountTableEntity
{
private AccountIdValue id;
private AccountNameValue name;
private AgeValue age;
private SexValue sex;
}
|
datasourceプロジェクト
AccountDataSourceクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package tech.blogenist.service.account.datasource.db.account;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Repository;
import tech.blogenist.service.account.entity.db.account.AccountTableEntity;
import tech.blogenist.service.account.value.account.AccountIdValue;
import tech.blogenist.service.account.value.account.AccountNameValue;
import tech.blogenist.service.account.value.account.AgeValue;
import tech.blogenist.service.account.value.account.SexValue;
@Repository
public class AccountDataSource
{
public List< AccountTableEntity > findBy()
{
// 一旦Stub処理
return Arrays.asList(
AccountTableEntity.builder()
.id( AccountIdValue.of( 10001 ) )
.name( AccountNameValue.of( "山田 太郎" ) )
.age( AgeValue.of( 20 ) )
.sex( SexValue.of( SexValue.Type.MALE ) )
.build(),
AccountTableEntity.builder()
.id( AccountIdValue.of( 10002 ) )
.name( AccountNameValue.of( "山田 花子" ) )
.age( AgeValue.of( 18 ) )
.sex( SexValue.of( SexValue.Type.FEMALE ) )
.build() );
}
}
|
apiプロジェクト
AccountListingElementModelクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package tech.blogenist.service.account.api.model.account.listing.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.blogenist.service.account.value.account.AccountIdValue;
import tech.blogenist.service.account.value.account.AccountNameValue;
import tech.blogenist.service.account.value.account.AgeValue;
import tech.blogenist.service.account.value.account.SexValue;
@Builder
@AllArgsConstructor( staticName = "of" )
@NoArgsConstructor( staticName = "empty" )
@Data
public class AccountListingElementModel
{
private AccountIdValue id;
private AccountNameValue name;
private AgeValue age;
private SexValue sex;
}
|
AccountListingResponseModelクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package tech.blogenist.service.account.api.model.account.listing.response;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@AllArgsConstructor( staticName = "of" )
@NoArgsConstructor( staticName = "empty" )
@Data
public class AccountListingResponseModel
{
private List< AccountListingElementModel > accounts;
}
|
AccountListingServiceクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package tech.blogenist.service.account.apiservice.account.listing;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.blogenist.service.account.api.model.account.listing.response.AccountListingElementModel;
import tech.blogenist.service.account.api.model.account.listing.response.AccountListingResponseModel;
import tech.blogenist.service.account.datasource.db.account.AccountDataSource;
import tech.blogenist.service.account.entity.db.account.AccountTableEntity;
@Service
public class AccountListingService
{
@Autowired
private AccountDataSource accountDataSource;
public AccountListingResponseModel findBy()
{
List< AccountTableEntity > entities = accountDataSource.findBy();
List< AccountListingElementModel > elements = entities.stream()
.map( entity ->
{
AccountListingElementModelelement = AccountListingElementModel.builder()
.id( entity.getId() )
.name( entity.getName() )
.age( entity.getAge() )
.sex( entity.getSex() )
.build();
return element;
} )
.collect( Collectors.toList() );
return AccountListingResponseModel.of( elements );
}
}
|
RestPathConstクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package tech.blogenist.service.account.api.infrastructure.rest.path;
public class RestPathConst
{
public class VersionPath
{
public static final String V1 = "v1/";
}
public class CategoryPath
{
public class AccountPath
{
public static final String BASE_PATH = "accounts/";
}
}
}
|
AccountListingControllerクラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package tech.blogenist.service.account.api.controller.account.listing;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.blogenist.service.account.api.infrastructure.rest.path.RestPathConst;
import tech.blogenist.service.account.api.model.account.listing.response.AccountListingResponseModel;
import tech.blogenist.service.account.apiservice.account.listing.AccountListingService;
@RestController
@RequestMapping( RestPathConst.VersionPath.V1 + RestPathConst.CategoryPath.AccountPath.BASE_PATH )
public class AccountListingController
{
@Autowired
private AccountListingService service;
@GetMapping
public AccountListingResponseModel listing()
{
AccountListingResponseModel response = service.findBy();
return response;
}
}
|
動作確認
では、この状態でサーバーを起動して動作確認をしてみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
{
"accounts": [
{
"id": {
"value": 10001
},
"name": {
"value": "山田 太郎"
},
"age": {
"value": 20
},
"sex": {
"value": "MALE"
}
},
{
"id": {
"value": 10002
},
"name": {
"value": "山田 花子"
},
"age": {
"value": 18
},
"sex": {
"value": "FEMALE"
}
}
]
}
|
正しく各レイヤーを通じてレスポンスが返ってくるようになりましたね♪
終わりに
これで各レイヤーを通るベースとなる処理が整いました。
次回は実際にDBへアクセスする部分の準備としてFlywayの導入をしていこうと思います。