しゃべぶろ

気になった技術の備忘録を残します。

【ドメイン駆動設計】サンプル(iddd_agilepm)を読解く その2

概要

前回はパッケージ構成について触れたので、
今回はアプリケーションサービスについて触れてみる。
hghyk023.hatenablog.jp

アプリケーションサービス

IDDD本の中で以下のように定義されている。

アプリケーションサービスは薄く保ち、モデル上でのタスクの調整にだけ使うようにすること。

アプリケーションサービスは、ドメインモデルやドメインサービスのクライアントとなり、
両者で表現すべきではない、アプリケーション要件を実現する時に使うべきである。
例えば、認証機能やメール送信機能などはアプリケーションサービスの責務となる。

iddd_agilepmでのアプリケーションサービスの使い方を覗いてみる。

applicationパッケージ

applicationパッケージ配下はこんな感じ。

application
├── ApplicationServiceLifeCycle.java
├── notification
│   └── NotificationApplicationService.java
├── process
│   └── ProcessApplicationService.java
├── product
│   ├── InitiateDiscussionCommand.java
│   ├── NewProductCommand.java
│   ├── ProductApplicationService.java
│   ├── RequestProductDiscussionCommand.java
│   ├── RetryProductDiscussionRequestCommand.java
│   ├── StartDiscussionInitiationCommand.java
│   ├── TimeOutProductDiscussionRequestCommand.java
│   └── backlogitem
│       └── BacklogItemApplicationService.java
├── sprint
│   ├── CommitBacklogItemToSprintCommand.java
│   └── SprintApplicationService.java
└── team
    ├── ChangeTeamMemberEmailAddressCommand.java
    ├── ChangeTeamMemberNameCommand.java
    ├── DisableMemberCommand.java
    ├── DisableProductOwnerCommand.java
    ├── DisableTeamMemberCommand.java
    ├── EnableMemberCommand.java
    ├── EnableProductOwnerCommand.java
    ├── EnableTeamMemberCommand.java
    └── TeamApplicationService.java
  • ApplicationServiceLifeCycleというクラスが存在する。
  • 1 つのServiceに対して複数のCommandクラスが関連している。

ApplicationServiceLifeCycle

  • 各サービスの処理開始/終了時に呼び出されている。
  • LevelDBとSlothMQを使ってイベントストアにドメインイベントを登録している。
  • 確かにこれはapplicationパッケージ配下に置くものだね。

ServiceとCommandクラス

例として、ProductApplicationServiceを取り上げる。
以下はinitiateDiscussionメソッドを抜粋

public void initiateDiscussion(InitiateDiscussionCommand aCommand) {
    ApplicationServiceLifeCycle.begin();

    try {
        Product product =
                this.productRepository()
                    .productOfId(
                            new TenantId(aCommand.getTenantId()),
                            new ProductId(aCommand.getProductId()));

        if (product == null) {
            throw new IllegalStateException(
                    "Unknown product of tenant id: "
                    + aCommand.getTenantId()
                    + " and product id: "
                    + aCommand.getProductId());
        }

        product.initiateDiscussion(new DiscussionDescriptor(aCommand.getDiscussionId()));

        this.productRepository().save(product);

        ProcessId processId = ProcessId.existingProcessId(product.discussionInitiationId());

        TimeConstrainedProcessTracker tracker =
                this.processTrackerRepository()
                    .trackerOfProcessId(aCommand.getTenantId(), processId);

        tracker.completed();

        this.processTrackerRepository().save(tracker);

        ApplicationServiceLifeCycle.success();

    } catch (RuntimeException e) {
        ApplicationServiceLifeCycle.fail(e);
    }
}
  • まず、リポジトリを使って集約ルートのProductオブジェクト取得する。
    • リポジトリの引数としてVOを渡している、newして渡しているのが結構斬新(自分だけかも)
    • VOは使い捨てることが出来るので、こういう使い方が出来るんだろうね。
    • Productが無かったら、Exceptionをthrowする。
  • initiateDiscussionメソッドで自身の状態を変化させ、リポジトリを使って永続化する。
  • ProcessId、TimeConstrainedProcessTrackerなんだこれ...w
    • ProcessIdは別プロジェクトのiddd_commonでドメインモデルとして定義されている。
    • DBにプロセスID単位で書き込んで、処理の状態を確認出来るようにしているのかな??


上記メソッド以外も見てみる。

public String newProduct(NewProductCommand aCommand) {...}
public String newProductWithDiscussion(NewProductCommand aCommand) {...}
public void requestProductDiscussion(RequestProductDiscussionCommand aCommand) {...}
private void sendEmailForTimedOutProcess(Product aProduct) {...}
private void requestProductDiscussionFor(Product aProduct) {...}
  • 恐らくユースケース毎にメソッドが作成されており、ユビキタス言語を反映したメソッド名になっている。
  • publicメソッドは引数にCommandクラス、privateメソッドがCommadクラスを使用していない。


あとCommandクラスも1個取り上げてみる。

public class NewProductCommand {

    private String tenantId;
    private String productOwnerId;
    private String name;
    private String description;

    public NewProductCommand(String tenantId, String productOwnerId, String name, String description) {
        super();

        this.tenantId = tenantId;
        this.productOwnerId = productOwnerId;
        this.name = name;
        this.description = description;
    }
// getter/setter省略
}
  • CommandクラスはPOJOであり、getter/setterしかない。
    • フィールドは標準クラスで構成されており、VOは使用されていない。
    • サービスのクラアイントにドメインの情報を漏らさないようにしている??

まとめ

今回の例だと、アプリケーションサービスはドメインモデルの永続化やイベントストアへの登録を担当しており、ドメインロジックは記述されていなかった。
次はドメインモデルを読解いて、アプリケーションサービスとの違いを理解したい。

【ドメイン駆動設計】サンプル(iddd_agilepm)を読解く その1

概要

IDDD本のサンプルソースがあるのでそれを読み解く。
結構な量があるので、iddd_agilepmから読解いて見ようと思う。
今回は、パッケージ構成を見てみる。

iddd_agilepm

まず、IDDD本の中で登場する架空の企業であるSaaSOvation社は、
アジャイルプロジェクト管理ツールを開発しており、
この「iddd_agilepm」はアジャイルプロジェクト管理コンテキストとなる。
アジャイルプロジェクト管理ツールとは既存製品で言うJIRAやPivotal Trackerみたいなものであり、
「プロダクト」や「バックログアイテム」などの概念が存在する。

パッケージ構成

全体構成はこんな感じ。
大きくわけると「application」「domain/model」「port/adapter」の3つに分けられる。

agilepm
├── application
│   ├── notification
│   ├── process
│   ├── product
│   │   └── backlogitem
│   ├── sprint
│   └── team
├── domain
│   └── model
│       ├── discussion
│       ├── product
│       │   ├── backlogitem
│       │   ├── release
│       │   └── sprint
│       ├── team
│       └── tenant
└── port
    └── adapter
        ├── messaging
        │   ├── rabbitmq
        │   └── sloth
        └── persistence;

applicationパッケージ

  • アプリケーションサービスが存在する。
  • アプリケーションサービスに渡す、Commandクラスが存在する。
  • サブパッケージはサービス単位で切られている。
    • domain/modelパッケージとの関連性が見れる。
    • sprintパッケージは、product配下じゃなくていいのかな??

domain/modelパッケージ

  • エンティティと値オブジェクト、ドメインイベントが存在している。
  • リポジトリインターフェイスが存在する。
    • 実装はport/adapterパッケージ配下に存在する。
    • 依存関係逆転の法則ってやつ?

port/adapterパッケージ

  • ヘキサゴナルアーキテクチャを採用している。
    • よくあるのはinfrastructureパッケージ
  • RabbitMQによるイベント処理と、LevelDBによる永続化処理が実装されている。
    • presistence配下に、leveldbというサブパッケージを切ってもいいと思う。

【ドメイン駆動設計】EntitiyとVO(Value Object)の違い

概要

現在ドメイン駆動設計を勉強しており、EntitiyとVO(Value Object)の違いがよく分からないので自分なりに纏めてみる。

ドメイン駆動

業務での関心事(ドメイン)を中心にアプローチしていく設計手法。
ドメインに注視して設計していくため、設計を進める事で要件を深堀出来たり、
将来的な仕様追加に柔軟に対応出来るシステムを作る事が出来る。
ドメイン駆動設計をする際は、OOA(オブジェクト思考アプローチ)を使って進めて必要がある。
今までDOA(データ中心アプローチ)によるシステム設計を経験してきた自分からすると結構戸惑う。

OOA(オブジェクト思考アプローチ)

自分のOOAのイメージはこんな感じ、「データ + 振る舞い」
各オブジェクトは自身にデータを保持しており、
またそれを利用者にどう見せるか、どうデータを変化させるかという振る舞いを持っている。
こうする事でデータモデルが変わったとしても、利用者側には影響を出さずに要件を満たす事が出来る。

Entity

話を戻してEntityについて記載する。
ドメイン駆動におけるEntityとはある関心事の中で一意に表現すべき概念のこと。
また、Entityは自身の状態が変化するという特性を持っている。
要件の中で状態が変更する概念はEntityとして管理すべきである。

VO(Value Object)

一方VOは状態は持っておらず、その時点で決まる固定値を格納するために利用する。
また、Entityを補足説明する物であるのでEntityとセットで使用する(...と思う)。
VOを利用するメリットとしては、固定値を加工して返却したり、複数のEntityで概念を共有する事が出来る。

IDDD本によるとVOは以下のように定義されている。

  • そのドメイン内の何かを計測したり定量化したり、あるいは説明したりする。
  • 状態を不変に保つことができる。
  • 関連する属性を不可欠な単位として組み合わせることで、概念的な統一体を形成する。
  • 計測値や説明が変わったときには、全体を完全に置き換えられる。
  • 値が等しいかどうかを、他と比較できる。
  • 協力関係にあるその他の概念に、副作用のない振る舞いを提供する。

個人的にはこの定義が好きである(?)
例えばVOでも値を変更したりすることは普通にあると思う。
その場合は、値を変更するのではなくオブジェクト全体を書き換えることを推奨している。

  • 計測値や説明が変わったときには、全体を完全に置き換えられる。

【Spring Boot】@AutoConfigureを理解する。

概要

Spring Bootには、Bean定義を自動で行ってくれるAutoConfigureという仕組みがある。
今まで何となく使っていたので、ここで理解を深めたい。
こちらの記事が非常に良く纏まっているので、参考にさせて頂く。*1

AutoConfigure

16. Auto-configuration
Spring Boot auto-configuration attempts to automatically configure your Spring application based on the jar dependencies that you have added.

  • 追加したjarの依存関係に基づいて、Springアプリケーションを自動で設定してくれる。 *2
  • Spring FrameworkではxmlでBeanの定義をしていたが、Spring Bootでは不要でる。

自動設定をするには、対象クラスに@SpringBootApplicationか@EnableAutoConfigurationを付与する必要がある。

サンプル

@SpringBootApplication
public class SampleApplication {
	/* 以下省略 */
}
  • 自動設定を有効にするために@SpringBootApplicationを付与している。

@SpringBootApplicationを付与するだけで、DIコンテナに様々なBeanを登録してくれる。
では、Beanはどのように登録されているのだろうか?

@EnableAutoConfiguration

上述で述べた@EnableAutoConfigurationから紐解いてみる。
ちなみに、@SpringBootApplicationは内部で@EnableAutoConfigurationを使用している。
他にもアノテーションを利用しているが今回は省略する。

@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
	/* 以下省略 */
}
  • @Importで他のコンフィギュレーションクラスをインポートしている。
  • クラスを直接指定せずに、EnableAutoConfigurationImportSelectorを指定している。
  • EnableAutoConfigurationImportSelectorはspring-boot-autoconfigure.jar/META-INF/spring.factoriesに列挙されているコンフィギュレーションクラスを取得する。

spring.factoriesはこんな感じ。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
/* 以下省略 */

これを見ると末尾にAutoConfigurationとつけるようになっている。自作ライブラリ用のコンフィギュレーションクラスを作成する際は、この命名規則に準ずるのが良さそう。

CassandraAutoConfigurationを覗いてみる。

/* 一部抜粋 */
@Configuration // (1)
@ConditionalOnClass({ Cluster.class }) // (2)
@EnableConfigurationProperties(CassandraProperties.class) // (3)
public class CassandraAutoConfiguration {

	private final CassandraProperties properties;

	private final List<ClusterBuilderCustomizer> builderCustomizers;

	public CassandraAutoConfiguration(CassandraProperties properties,
			ObjectProvider<List<ClusterBuilderCustomizer>> builderCustomizers) {
		this.properties = properties;
		this.builderCustomizers = builderCustomizers.getIfAvailable();
	}

	@Bean
	@ConditionalOnMissingBean // (4)
	public Cluster cluster() {
		CassandraProperties properties = this.properties;
		Cluster.Builder builder = Cluster.builder()
				.withClusterName(properties.getClusterName())
				.withPort(properties.getPort());
		if (properties.getUsername() != null) {
			builder.withCredentials(properties.getUsername(), properties.getPassword());
		}
		/* 省略 */
		customize(builder);
		return builder.build();
	}
}
項番 説明
(1) コンフィギュレーションクラスであることを定義している。Beanの登録をする事が可能となる。
(2) 指定したクラスがクラスパス上に存在した場合に適用される。
(3) 利用するプロパティクラスを定義している。そしてコンストラクタインジェクションでDIされている??ここは理解不足。
(4) BeanがDIコンテナに登録されていない場合に適用される。
  • Clusterクラスはcassandra-driver-coreライブラリに含まれている。なので、デフォルトでは含まれていない。エラーにならないのは謎である。
  • Beanを登録するコンフィギュレーションクラスと設定値を保持するプロパティクラスは切分けされている。
  • 自分は今まで同じクラス内に書いていたので、このように分けた方が良さそうだ。

このコンフィギュレーションクラスが正常に動作すれば、ClusterクラスのBeanがDIコンテナに登録されるはずである。今回はそこまで追わないが、今後もう少し調べてみたい。

まとめ

AutoConfigureの仕組みを理解する事が出来た。
この仕組みを使う事で、Spring上でOSSを使いやすくしている。
また、自作ライブラリにも取入れて簡単に扱いやすくしてみたい。

【Gradle】Spring Initializrが作成するbuild.gradle

概要

Gradleとはビルドツールのこと。
ライブラリを管理して、クラスパスに追加したりする事が出来る。
ビルドツールを使わないと、javaコマンドを叩いてクラスパスを追加しないといけない。
同様なプロダクトとして、AntやMavenがある。
大きな違いは、AntとMavenが設定ファイルをxmlで定義するのに対して、
Gradleでは、groovyでスクリプトファイルとして定義する。

groovy

JVM上で動作するスクリプト言語Javaで書かれたライブラリを呼出す事が出来る。
Rubyの影響を強く受けており、RailsならぬGrailsというフレームワークも存在する。
また、Grailsは内部でSpring Frameworkを呼んでいる。

サンプル

Spring Initializrで作成されたbuild.graldeを用いて勉強する。

buildscript {
	ext {
		springBootVersion = '1.5.13.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	mavenCentral()
}

dependencies {
	compile('org.springframework.boot:spring-boot-starter')
	testCompile('org.springframework.boot:spring-boot-starter-test')
}

buildscriptブロック

ビルドスクリプトが依存している情報をここで定義する。
例えば、Spring Bootを実行するためのタスク「bootRun」は、
spring-boot-gradle-pluginライブラリが提供しており、
このライブラリを読込んでいないと実行する事ができない。
ちなみ、buildscriptはメソッドであり、引数にクロシージャを渡している。

void buildscript(Closure configureClosure)
Configures the build script classpath for this project.

The given closure is executed against this project's ScriptHandler. The ScriptHandler is passed to the closure as the closure's delegate.

Parameters:
configureClosure - the closure to use to configure the build script classpath.

Gradle API 4.7

extブロックは、変数定義に使用する。
repositoriesブロックは、ライブラリのリポジトリを指定する。
dependenciesブロックは、クラスパスに追加するライブラリを指定する。

apply plugin

プラグインを有効にする記述。

apply plugin: 'java' // javaをbuildするために必要。
apply plugin: 'eclipse' // なくても良い?
apply plugin: 'org.springframework.boot' // bootRunタスクを追加してくれる。

groupとversion

プロジェクトの設定を記述する。

sourceCompatibility

プロジェクトが使用するJavaのverを指定する。

compileとtestCompile

プロジェクトの依存関係を指定する。
他にも指定方法があるので纏めてみる。

指定方法 説明
compile プロジェクトのプロダクトコードをコンパイルするのに必要な依存関係
runtime プロダクトのクラスを実行するときに必要になる依存関係
testCompile プロジェクトのテストコードをコンパイルするのに必要な依存関係
testRuntime テストを実行するのに必要な依存関係

runtimeはプロジェクトを実行する時に使用する依存関係を指定する。

【Spring Framework】ApplicationEventについて

概要

Springでイベント通知をしたい場合は、ApplicationEventを発行してあげれば良い。
発行するにはApplicationEventPublisherそして、ApplicationListenerを使ってイベントを受信する。
これらを使えば、コンポーネント間の独立性を高める事が出来る。

ApplicationEvent

ApplicationEventは抽象クラスとなっていので、利用用途毎にサブラクスを作成する必要がある。

public class SampleEvent extends ApplicationEvent{

    private final String msg;

    public SampleEvent(Object source, String msg) {
        super(source);
        this.msg = msg;
    }

    public String getMsg () {
        return msg;
    }
}

ApplicationEvent自体は大して重要ではない。
重要なのは、ApplicationEventPublisherApplicationListener

ApplicationEventPublisher

イベントを発行する事が出来るインターフェイスである。

@Component
public class SamplePublisher {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void sendMessage(String msg) {
        applicationEventPublisher.publishEvent(new SampleEvent(this, msg));
    }

}

publishEvent()はAbstractApplicationContext内で実装されている。
実装を見ると発行されたEventはAbstractApplicationContext内に保持または、
ApplicationEventMulticasterによって、リスナーにイベントを発行している。
この違いはなんだろう...?

spring-framework/AbstractApplicationContext.java at master · spring-projects/spring-framework · GitHub

ApplicationListener

イベントを受信できるインターフェイスである。
Spring4.2以降は、このインターフェイスは使用せずに@Eventlistenerを付与するだけで良い。

@Component
public class SampleListener {

    @Autowired
    private SamplePublisher samplePublisher;

    @EventListener
    public void onMyEvent (SampleEvent event) {
        System.out.println("Thread : " + Thread.currentThread().getName() + " event received: " + event.getMsg());
    }

}

ユースケース

データを読込、加工して、DBに登録するアプリケーションを例に考えて見る。

ApplicationEventを使用しない場合

読込処理 -> 加工処理 -> 登録処理

なんの変哲も無い、良くあるプログラムである。

ApplicationEventを使用した場合

読込処理 -> 加工処理 -> ApplicationEvent -> 登録処理

加工処理と登録処理の間にApplicationEventを利用した。
こうする事で、両処理はお互いの処理内容まで気にする必要が無くなる。
加工処理は定義されたEventを発行すれば良いし、登録処理は受信したイベントを元に処理を行えば良くなる。また、テストも書きやすくなると思う。