システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

JSFのメモ

リファレンス

https://javaserverfaces.github.io/docs/2.3/

https://javaserverfaces.github.io/docs/2.3/vdldocs/facelets/index.html

8.9 HTML5-Friendly Markup - Java Platform, Enterprise Edition: The Java EE Tutorial (Release 7)

ViewScoped

【JSF2】ViewExpiredExceptionが多発した原因 - タイガー!タイガー!じれったいぞー!(SE編)

【JSF2.2】セッションごとに保持できるView画面件数が25画面までに制限されている - タイガー!タイガー!じれったいぞー!(SE編)

参考リンク

GitHub - IBM/japan-technology: IBM Related Japanese technical documents - Code Patterns, Learning Path, Tutorials, etc.

h:messageの使い方 - urotasabisの日記

ふつうのHTMLを書こう! イマドキのJSFの書き方 - タグ編 - Qiita

JSFを拡張子htmlで動くようにする - じゃばらの手記

JSFでエラーのある項目の背景色を変える - じゃばらの手記

JSFでエラー項目の背景色を変える - 中年プログラマーの息抜き

【Eclipse×TomcatでJavaEE開発】JSF・CDI・JPAを使えるようにするまでの設定 | ITのおもちゃ箱

JSFを使うなら読んでおきたいリンクまとめ - Instructor's memo

JSFのFaceletsでxhtmlに直接アクセスさせない方法 - DENの思うこと

JSFファイル(xhtml)をWEB-INF配下に隠す - 気になったことをいろいろやってみる

JSF 2.2ではf:eventのpreRenderViewではなくf:viewActionを使う? - DENの思うこと

【もうハマらない!】JSFをきちんと理解しよう。 - Qiita

JSFで生成されるidあれこれ - Challenge Engineer Life !

[JSF] ブラウザキャッシュを無効にする | KATSUMI KOKUZAWA'S BLOG

Convert the Alerts component of 'Bootstrap' to a component of JSF 2.0 | KATSUMI KOKUZAWA'S BLOG

「FilterよりJSFのmanaged-beanのプロパティにアクセスしたい」(1) Java Solution − @IT

違い - jsf ui repeat id - 解決方法

属性 - jsf:value - 解決方法

2005-05-23

[JSF 2.3] Websocket を試す(その2) | KATSUMI KOKUZAWA'S BLOG

FormとValueObjectの関係を整理する

vermeer.hatenablog.jp

で書けなかったモヤモヤしたところを、自分なりにBeanValidationを中心として考えていく中で整理できつつあるので、具体的な実装に入る前に、まず まとめてみたいと思います。

それは、画面用オブジェクト(以降、Form)とドメインオブジェクトの関係についてです。

具体的には

P210
ドメインオブジェクトをそのまま使うことを重視すべきです

のところです。

ビジネスロジック部分については、そのまま使えることを重視するのは賛成です。

ただ、Formは本質的には「文字列」です。

なぜ「そのまま使うこと」と言えるのだろうと、思ったのですが実装サンプルとなりうるコードを読んでみて分かりました。

isolating-the-domain

SpringMVCだとフレームワークでイイ感じにFormとドメインオブジェクトをマッピングしてくれるんですね。

でも、ここで自分が何にモヤモヤしているのか、ということも具体的に分かりました*1

それは、FormへマッピングをするためにドメインオブジェクトのtoStringメソッドに「Formのための実装」が強制されているということです。

ドメインオブジェクトは「すべての層から独立したものとしたい」と考えている以上、toStringであったとしても そこに具体的なアプリケーション仕様に直結する作法は含みたくありません。

このことについては、APIデザインの極意 Java/NetBeansアーキテクト探究ノート にも書かれています。

もし含むとしたら、それはFormクラスとして実装すべきだと思います。

では、具体的にコードとしてどうすれば良いでしょうか?

以下に私の考えるイメージを示します。

コードによる表現

画面全体となるFormクラス

画面全体またはリクエスト単位の集合です。

ここでは、必須入力を処理するAnnotationを付与しています。

@FormNotBlankは「画面上で必須な項目」を意図したものです。

加えてgroups = FormValidation.classにより検証優先度を指定して、@FormNotBlankでエラーがあったら以降の検証処理をしないようにしています。

あくまで「操作における必須情報」であって、オブジェクト生成におけるNotNullとは別物です。

@Data
public class Form {

    @Valid
    @FormNotBlank(groups = FormValidation.class)
    private FormItem item;

}

Formの部品

集合の部品となるForm
その主たる関心事であるドメイン(ValueObject)を包含します。

インスタンス化およびgetterの際の型は基本的にStringです。

これはWebであるための物理制約です。

ValueObjectとStringの変換だけでなく、ドメインに直結しない「表現」に関する物理的な実装を このクラスが担います(論理的な情報はValueObjectに実装します)。

ここではValueObjectのプリミティブな情報をValidation用に返却するメソッドを追加しています。

public class FormItem implements FormObject<String> {

    @Valid
    private final ValueObject value;

    public FormItem(String value) {
        this.value = ValueObject.of(value);
    }

    public String value() {
        return this.value.getValue();
    }

    @Override
    public String getValidatationValue() {
        return this.value();
    }

}

ValueObject

関心事の中心となるドメインの実装です。

(桁チェックしかしていないので、これを「関心事の中心」というのも変ですが)

@Value(staticConstructor = "of")
public class ValueObject {

    @Min(value = 100)
    private final String value;
}

さいごに

デメリットは実装するクラスが増えてしまうことですが、Domain層とPresentation層の役割を物理的に分けることで、それぞれの関心事による実装汚染を防ぐことができると私は考えます。

もちろん、SpringMVCやRailsのように簡易化するための(Easyにするための)仕組みも生産性のことを考えると有効だと思います。

とはいえ、Easyのための仕組みと、設計としてありたい形は別物だと思います。

まずは「設計としてありたい形」と「実際に実現可能か」というところを きちんと整理した上で、それを「Easyにする仕組み」をさらに準備する という流れで色々と考えていきたいと思っています。

具体的な全体像は別記事でBeanValidationとあわせて まとめる予定です。

*1:コードで理解するのは大事だと再認識

BeanValidationのメッセージを遅延変換させる

はじめに

BeanValidationのメッセージを遅延変換する、ということの意図を簡単に説明したいと思います。

BeanValidationは検証(validate)した際にメッセージ変換も一緒に実行してくれます。

基本的に、これはありがたい機能なのですが、Domainで発行したメッセージをそのままPresentationで扱うのは適さない場合があります。

例えばクライアントロケールに応じたメッセージ出力をしたい場合です。

ロケールを考慮したメッセージ出力の制御については、こちらの記事が有効です。

masatoshitada.hatenadiary.jp

この方法は有効なのですが、少し条件があって 検証とメッセージ変換が同時に行っても良い場合、というところがあります。

これだとPresentation層で検証して、その結果をクライアントに返却する分には十分なのですが、Domain層やInfrastructure層で検証した場合、少々困ったことになります。

どういうことかというとDomain層でvalidateを行った場合、ロケールをどうにかして渡してあげる必要があります。ただ、これをしてしまうとPresentation層の情報をDomain層で参照しなくてはならずレイヤー違反を起こしてしまいます。

やりたいことは、Domain層のvalidation結果を、Presentation層で変換してクライアントに返却する、です。

ということで、検証とメッセージ変換を分けて実行する(遅延変換する)、という要求が生まれ、それに対応するための仕組みを考えてみよう、と思いました。

ざっくり

大げさなことをする必要はありません。

検証結果オブジェクトに保有している情報を元にメッセージを変換する、だけです。

検証時にもメッセージ変換をしているので冗長的な処理ではありますが、目的は上述の通りなので これは割り切ります。

ついでに参照するPropertyファイルもValidationMessagesとは異なるものを指定することにします

実装

変換するクラス

/**
 * BeanValidationの検証結果メッセージを変換するクラス.
 *
 * @author Yamashita,Takahiro
 */
public class BeanValidationMessageInterpolator {

    private final Locale locale;

    private BeanValidationMessageInterpolator(Locale locale) {
        this.locale = locale == null ? Locale.getDefault() : locale;
    }

    public static BeanValidationMessageInterpolator create() {
        return new BeanValidationMessageInterpolator(null);
    }

    public static BeanValidationMessageInterpolator of(Locale locale) {
        return new BeanValidationMessageInterpolator(locale);
    }

    public String toMessage(ConstraintViolation<?> constraintViolation) {

        ResourceBundleLocator resourceBundleLocator = (Locale _locale) -> {
            Control control = Control.getNoFallbackControl(Control.FORMAT_DEFAULT);
            return ResourceBundle.getBundle("Messages", this.locale, control);
        };

        ResourceBundleMessageInterpolator interpolator = new ResourceBundleMessageInterpolator(resourceBundleLocator);

        MessageInterpolatorContext context = new MessageInterpolatorContext(
                constraintViolation.getConstraintDescriptor(),
                constraintViolation.getInvalidValue(),
                constraintViolation.getRootBeanClass(),
                Collections.emptyMap(),
                Collections.emptyMap()
        );
        String message = interpolator.interpolate(constraintViolation.getMessageTemplate(), context);
        return message;
    }

}

ロケールの指定が出来るようにしていますが、今回は使用していません。

使用方法は見ての通りということで割愛します。

ResourceBundleMessageInterpolatorがメッセージを変換するクラスです。

これに必要なパラメータを渡していきます。

ResourceBundleLocatorが参照するResourceBundleを管理するクラスです。

今回は手抜きをしてラムダで実装しましたが、本来は継承して別クラスにした方が良いと思います。

MessageInterpolatorContextには検証結果の情報を渡します。

第4、第5パラメータは今回使用しないので、空Mapを渡しています*1

最後にResourceBundleMessageInterpolator#interpolateで変換したい値のプロパティまたは、値そのものを指定して変換します。

検証対象のクラス

@Value(staticConstructor = "of")
public class ValidationTarget {

    @Size(min = 11)
    private final String noMessage;

    @CustomType
    private final String customTypeValue;

    @AssertTrue(message = "{custom.assert}")
    protected boolean isValid() {
        return false;
    }

    @Size(min = 100, message = "{min}以上の文字を指定してください。")
    private final String withMessage;

}

Lombok@Valueの説明は割愛します。

標準APIアノテーション

@Size(min = 11)

  • 検証時に変換されたメッセージ
    ValidationMessagesの標準のメッセージが使用されます。

  • 遅延変換したメッセージ
    MessagesのPropertyキーの一致するメッセージが使用されます。

独自アノテーション

独自の検証Validator@CustomTypeを作成。

ValidationMessagesに追記した {CustomValidator.message}に指定したメッセージが使用されます。

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {CustomValidator.class})
public @interface CustomType {

    String message() default "{CustomValidator.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {

        CustomType[] value();
    }
}
public class CustomValidator implements ConstraintValidator<CustomType, String> {

    @Override
    public void initialize(CustomType constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null;
    }
}

メッセージを取得するのPropertyキーはCustomValidator.messageです。

  • 検証時に変換されたメッセージ
    ValidationMessagesに追記したメッセージが使用されます。

  • 遅延変換したメッセージ
    MessagesのPropertyキーの一致するメッセージが使用されます。

アクセッサメソッドの戻り値検証

''@AssertTrue(message = "{custom.assert}")''

アクセッサメソッド*2の戻り値を検証します。

メッセージはPropertyキーを指定しています。

  • 検証時に変換されたメッセージ
    ValidationMessagesに追記したメッセージが使用されます。

  • 遅延変換したメッセージ
    MessagesのPropertyキーの一致するメッセージが使用されます。

直接アノテーションで指定

@Size(min = 100, message = "{min}以上の文字を指定してください。")

表示するメッセージを直接アノテーションで指定しています。

  • 検証時に変換されたメッセージ
    指定したメッセージがそのまま使用されます。

  • 遅延変換したメッセージ
    指定したメッセージがそのまま使用されます。

テストクラスと実行結果

public class ValidationTest {

    @Test
    public void lazyMessageConvert() {

        ValidationTarget target = ValidationTarget.of("noMessage", "customTypeValue", "withMessage");
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

        Set<ConstraintViolation<ValidationTarget>> results = validator.validate(target);
        for (ConstraintViolation<ValidationTarget> result : results) {
            BeanValidationMessageInterpolator interpolator = BeanValidationMessageInterpolator.create();
            String convertedMessage = interpolator.toMessage(result);

            System.out.println(">>>>>>>>>");
            System.out.println("設定した値                               = " + result.getInvalidValue());
            System.out.println("検証時に変換されたメッセージ = " + result.getMessage());
            System.out.println("遅延変換したメッセージ           = " + convertedMessage);
        }

    }

}
>>>>>>>>>
設定した値                   = false
検証時に変換されたメッセージ = direct set message template  with annotation
遅延変換したメッセージ       = アノテーションに直接設定したメッセージテンプレート
>>>>>>>>>
設定した値                   = withMessage
検証時に変換されたメッセージ = 100以上の文字を指定してください。
遅延変換したメッセージ       = 100以上の文字を指定してください。
>>>>>>>>>
設定した値                   = noMessage
検証時に変換されたメッセージ = size must be between 11 and 2147483647
遅延変換したメッセージ       = 文字数は 11 から 2147483647 の間にしてください。
>>>>>>>>>
設定した値                   = customTypeValue
検証時に変換されたメッセージ = custom validatior message(default template message)
遅延変換したメッセージ       = 独自検証のメッセージ(validatorクラスで指定したデフォルトメッセージ)

Code

Bitbucket

さいごに

ResourceBundleの扱い方や参照するPropertyファイルについて、あれこれしたいところは残っていますが、とりあえず変換するところまで。

あと、この記事だけでは何が嬉しいのか分かりにくいと思います。

もう少し実際の利用ケースに応じた実装を肉付けしていけば遅延変換の嬉しさが伝わるかと思いますが、それはまた後日ということで。

*1:これは今後 私がやりたいことで活用する予定なので、その時に説明します

*2:getXXとかisXX

パッケージ構成の考察

最新の考察

vermeer.hatenablog.jp


レイヤーで論理的な役割を整理したので、次はパッケージです。

パッケージ概要

フォルダ構成例

  boundedcontext
    ├─application
    │  └─service
    │      └─hoge
    │  
    ├─domain
    │  ├─model
    │  │  └─hoge
    │  │    hogeFactory
    │  │    hogeRepository
    │  ├─query
    │  └─rule
    ├─infrastructure
    │  └─datasource
    │      └─hoge
    │        entity
    │        repositoryImpl
    └─presentation
        └─hogeregister

Presentation

  • 画面(や コンポーネント)、スコープ(ConversationやFlow)の単位。
  • RestURIの単位。
  • 実装技術単位ではなく、インタフェース単位でフォルダを作成。
    (特定のフォルダ名ではないので、上図でも背景を白にしています)
  • Controller(Action)とForm(表現の実体)は基本的に同一フォルダにまとめる。
    ただし、複数コンポーネントで整理したい場合は、サブフォルダで分割。
    同様に同一スコープの単位でフォルダを作成しても良い。
    (登録→確認→完了を「XX登録」を親フォルダとした構成イメージ)
  • 命名:画面単位(Aggregate)はHogePage、部品はFugaForm

Application

Service

  • ServiceおよびDomainにより振る舞いを表現する
  • トランザクション境界
  • コンテキストや主たる関心事でフォルダ分割して見通しを良くしても良い。
  • 登録系は動詞単位、参照系はEntityまたはDTO単位
  • Repositoryを使える唯一の層*1

Infrastructure

ドメインモデルを永続化する具体的な実装。 DataSource、Messagingだけということではなく、よくある永続化の参考例。 他にFileSystemなど、永続化対象に応じてフォルダを作成する。 一般的な永続化であるDatasourceによる整理を行う。

DataSource

  • DomainのRepositoryの実装

Domain

Model

  • ValueObjectとEntityによる関心事の実装
  • AggregateRootModel単位でフォルダ作成
  • Repositoryを使ってはいけない

Rule

  • Modelで表現するのに適切でないルールの実装
  • DDDのDomainServiceに類似
  • コンテキストや関心事でフォルダ分割して見通しを良くしても良い
  • Repositoryを使って良い
    Repository(DAO)よりもビジネスロジックを表現したい場合に使う
    *2
  • 複数のEntityを扱った判断/編集を行う際に使用する
  • 無くて済むなら作らない方が良い

Repository

  • パッケージはModelと同じにして集約ルートとの関連を明確にする
  • 集約ルートを経由したデータ操作を行う(DBとは限らない)
  • インターフェースによるInfrastructure層との依存解決
  • 入出力の関心事の操作の主体*3

Query

  • DTOを戻り値とする(Entity(集約)クラスを戻り値としない)Repository
  • Modelではないため、別途フォルダーを設けてDTO単位で実装する
  • 参照のみ
  • インターフェースによるInfrastructure層との依存解決

Factory*4

  • UtilなどをInjectしてEntityを生成
  • 複雑なDomainやEntityの生成

考察

登録系のServiceは動詞

調べた限りでは「名詞(Entity名)Service」にしているケースが多いですが、登録系については「動詞」の方が良いように思います。
事前条件、事後条件の検証を 同一サービス内で実装することで 関心事の集約を図ろうとした場合、動詞の単位で実装した方が可読性も良いと思います。

DataSourceのentityフォルダ配下はフラット

2019/1/25 訂正
分類不要という観点で整理すると言うのはNG。

DBのテーブル自体がフラットなので、当該フォルダ配下も分類もしなくても良いかな、と思っています。
テーブルの構造についてはER図(設計)および、RepositoryにおけるDomain x Entity の変換において構造化されるので*5、フォルダによる構造化は必須としなくても良いだろうという判断です。
また、NetBeansによる自動生成したJPAのEntity資産を必要以上に編集しないだけ、というのが本音もあります。

DataSourceのEntityはRepositoryImplと同じフォルダ

2019/1/25 追記

@Entityはpackage privateにして、RepositoryImplと同じパッケージに配置します。
こうすることで、@Entityへの操作を限定できます。
JPA@Entityは、mutableなJavaBeanなので気をつけて扱わないと想定していないデータ更新を引き起こす可能性もあり、コード上でも取り扱えるスコープを狭くしておいた方が良いという考えからです。

RepositoryはDAOではない

集約のルートととしたDAO(CRUD)的なところが基本になるけど、DAOではありません。
DAOは、あくまでORMが機能として提供するものであり、 Repositoryは あくまで使用者です。
Domain層ではinterfaceによる宣言しかできないので 戻り値(もしくは実行時例外)のEntity(もしくはDomainObject)によって表現するに留めます。
取得ロジックにドメインの関心事がある場合は、Ruleとして独立させて、Infrastructure層に ロジックを分散させないようにします。 *6
取得目的に関心事がある場合はメソッド名でそれを表明して入出力の窓口とすることで入出力を集約します。*7

Domain層に配置する意義にもつながるように思います。

Repositoryへのロジックの実装

基本的にRepositoryはDAOとの橋渡しが役割になりますが、ドメインロジックとまでは言えないけれど、いちいちルールとしてクラスを作るほどのことのないものもあります。

実際に、私がやってみて思った事例として

データ検索をしたけれど 対象データが無かった場合に実行時例外をスローしたい、というような要件があった場合、Serviceに判定を設けて振り分けてしまえば それでおしまいなのですが Serviceには判定を実装したくない(気が付けばロジックの置き場所になるトリガーになりかねないから)、Infrastructure層のRepositoryImplで実行時例外をスローさせても良いけれど 厳密には こういう判断もロジックなので 分散はさせたくない、でも このくらいのことのためにRuleクラスを作成するのも なんだか煩わしい、というケースがありました。

紆余曲折を経て*8、結論としては
interfaceのdefault実装を使ってRepositoryにロジックを実装することにしました。
(詳細は、後日 ブログにまとめる予定)

Repositoryインターフェースが「データを取得する行為に直結したロジック」の置き場所としての役割をもつことで、個人的には Serviceもすっきりし、Infrastructureにロジックも漏れ出ず 割と良い整理が出来たように思います。

DomainServiceじゃなくてRuleと表現する理由

Serviceという表現は紛らわしいと思いました。
それにDomainServiceの表現を調べると、ほとんど「ドメインオブジェクトの責務ではない、複数のドメインを扱うビジネスルール」に近しい表現がされており、それであれば「ルール」の方が紛らわしくない、という考えに至りました。

パッケージで境界付けされたコンテキストを表現

パッケージは名前衝突をさけるためにプロダクトのURLを逆にする、くらいしか考えていなかったのですが、パッケージは境界付けされたコンテキストにするという発想をDDD関連の記事で読んで、随分と思考がスッキリしたので、それを採用しました。

組織のドメイン.コンテキスト.レイヤー.詳細

モジュールの管理単位はコンテキストで集約。
共通部品とかインフラ層の抽象化したものとかは、フレームワークみたいな形で、別のコンテキストとしておくことで、自然と資産の分類がイイ感じになるのでは?と考えています。

なお、以前は

組織のドメイン.レイヤー.コンテキスト.詳細

という構成でした。
この構成だと、コンテキストがレイヤーの下であるため、発想が自然とモノリシックになってしまっていました。
ちょっとした違いかもしれませんが、実装によって発想が歪みにくくなる効果を期待しています。

FactoryでInjectって?(2018/11/8 追記)

Domainの実装はPOJOで実現できること、もしくはAnnotationを使った宣言による制御を通じて実現する(もしくは出来うる範囲に留めて 知識の表現を優先する)ことを想定していますが、共通Utilなどの便利な部品群を使いたい場合も 起こり得ると思います。
ザックリと 3つあると考えていて、CollectionsやArraysのように汎用的なライブラリに相当するもの、プロダクトとして共通的なDomain・機能としておきたいもの、汎用・共通な機能だけれど実装を置き換え可能なものにしておきたいもの、です。

1つ目は、横断的なものとして どのレイヤーからでも使用するUtilとして公開して、それを使います。何も考えずに実装するとcommonパッケージが出来て、Utilが集まって、、となるパターンですが java.* に相当するものは分離不可能なので皆無を前提とするのは難しいと思います。ただし 十分に吟味はしたいところです。

2つ目は、commonとは似て非なると思っているのですが、あくまでプロダクトにおける共通Domainです。例えば、myproduct.core とか myproduct.base とか そういう分類です。ニュアンスになってしまっていますが、他プロダクトで使うのではなく、同一プロダクトでの使用だが 複数パッケージで使いたいもの です。

3つ目は、実装を置き換えられるようにするということでDIを検討することになります。個々のDomainでDIして、、という やり方が思い付きますが、そうすると対象DomainがDI管理下になるようにしないといけません。ですが Domainにおける実装は基本的にDIを前提としていません。となると、どこかでUtilのインスタンスを渡してあげる必要があります。 Entityで、、とも考えられますが、Entityは Domainの集約であって生成が主たる仕事では無いと考えます。ということでFactoryでEntityを生成する際に、DIしておいたUtilのインスタンスを、そのUtilを必要とするDomainに提供するというやり方にしよう、と考えました。
そうすると、DIP*9によって実現するためInterfaceが必要になります。で、そのInterfaceですが、上述の1つ目に相当する場合は、どのプロダクトでも適用する仕様をまとめたパッケージで管理します(つまり commonに相当するレベルとして扱う)。2つ目に相当する場合は myproduct.core とか myproduct.baseというパッケージで管理して 当該プロジェクトだけで参照するものとして管理します。

ただし、Factoryを使うことも含めて、基本的に そうならないような設計になることが望ましいと考えています。
安易に「これは2つのパッケージで扱うから共通にしよう」ではなく、
例えば、「3つ目のパッケージとして新たに切り出してServiceを経由して実装すべき」かもしれません。
例えば、宣言的な実装をすべき対象かもしれません。

StreamAPIやlombokのような仕組みを自作して、それを横断的に使うというケースであれば、上述の1つ目に相当すると思いますし、その場合は そのライブラリと一蓮托生レベルの蜜結合なシステムだと思います。覚悟のある判断なので、それを止めることはできません。
逆に、Javaの基本構文の範囲と Annotationによる補助で実現出来うる実装にDomainの実装は留めて 外部ライブラリによる影響を極力低くしておきたい、という発想でシステムを構築しようとした場合は、面倒でも ここで提示した Factoryに相当するものを経由して結合度を下げることになるのかな?と考えています。

結局 言いたいのは、Utilは気軽に作るんじゃなくて オレオレFWとして一蓮托生となるもの以外は 慎重に作ろう、ということかもしれません。

RuleでRepositoryは使わない

2021/2/14 追記
キッカケは以下(および一連のスレッド)

「かとじゅんさんが言ったから」というよりも「あっ、そっかアプリケーションサービスでやれば良いんだ」と腹落ちしたから。
結果、DI(Scoped)からも解放されました。

アプリケーションサービスからアプリケーションサービスを呼べば良いだけなのに「トランザクション境界」という認識が強すぎたように思います。
これは例えば「Repositoryをトランザクション境界にする」と類似の整理誤りな気がしなくもないです。
個人的にはRepositoryから結果を取得して編集をするアプリケーションサービスを作ることは避けたいので、RepositoryにCRUDを超えたメソッドを作ろうと思います。「RepositoryはListのように使う」という考えもあるように思いますが、上述の通り「Repositoryは入出力を関心事とする主体」という整理にも合致しますし 元々の「Repositryへのロジックの実装」とも整合性はとれるように思います。

Scopeについて

Presentation

vermeer.hatenablog.jp

Service

Scopeは RequestScoped

本来は状態を保持することがないのでApplicationScopedでも良さそうなところです。
ServiceクラスはApplicationScopedにしている事例が多いため悩みました。
ですが、Infrastructure層のクラスのScopeを限定しない(後述)という整理をした場合、RequestScopedにしておかないとDIできません。
(ServiceがApplicationsScopedでDIしたいクラスがRequestScopedだとDIできない)

ということで 調整役という目的を達成するために、RequestScopedとしました。

Domain

そもそもDomain層のクラス自身が主体的にScopeを持つことは無いと考えています。
もし、そのような設計となっている場合、DomainクラスそのものにScopeをつけるのではない方式で実装できないか再考した方が良いと思います。
(例えばDIではなく、Scopeを持つFormクラスにDomainを内包するような)。

例外は Rule。
Repositoryを使う場合、DIをしないといけないため Scopeが必要です。
Ruleは Serviceから使用するので RequestScopedで良いと思いますが、Dependantでも良いかもしれません。
*10

Infrastructure

Infrastructure層のクラスは基本的にDIを通じて 各層に実装を提供することになるので全てのクラスにScopeの指定をすることが前提になります。
ただし、使用するライブラリの特性など色々な制約下に晒される層であるため 特定のScopeに限定することは出来ません。
例えば、DataStoreは 状態を持たない一例として、ApplicationScopedにする、としても 全てのクラスがApplicationScopedになるとは限らないということです。

参考

実践DDD本 第7章「ドメインサービス」~複数の物を扱うビジネスルール~ (1/4):CodeZine(コードジン)

混乱しがちなサービスという概念について - かとじゅんの技術日誌

履歴

追加考察

パッケージ構成の考察(2) - システム開発で思うところ

このあたりの発想も参考になりそう

Atomic Designをやめてディレクトリ構造を見直した話|食べログ フロントエンドエンジニアブログ|note


*1:2021/2/14 追記

*2:2021/2/14 削除

*3:2021/2/14 追記

*4:2018/11/8 追記

*5:少なくとも集約ルートを通じた操作において

*6:2021/2/14 削除

*7:2021/2/14 追記

*8:実装してみては眺めて、戻しては眺めて、考えてみては直してみて眺めてのループ

*9:依存性逆転の原則

*10:2021/2/14 削除

レイヤーの考察

オレオレDDDを一から整理し直し。

まずは、レイヤーから。

レイヤー構成

Presentation

  • 外部インターフェース(外部向け定義および操作)に関する機能を実装する層
  • 「外部に対する表現」が主たる役割

Application

  • ドメインを用いて機能を実現する層
  • 判断/加工に関するロジックは実装しない
  • 他システムとの連携(腐敗防止)でもある

Infrastructure

Domain

  • 機能仕様を実装する層
  • ドメイン(関心事)の実装」が主たる役割

考察

関心事に着目できる実装を目指す

ApplicationとDomainの実装を読めば、主な関心事を読み取れることが目標であり、そのためのレイヤー分割です。

表現と実装

「表現」は、画面やJsonなどの外部向けに提供する定義や機能、業務フロー(詳細手順ではない)のようなプロセス(サービス)の組み合わせ、という意味です。

「実装」はコードによって機能を実現することを意味しています。

例えば「表現」は、ドキュメントから自動生成した資産で事足りるようなものであり、「実装」はコードにより柔軟に対応(もしくはコードにより複雑さに対応)するもの、というイメージです*2

MVCとの違い

レイヤーは論理的な整理という理解です。

今回の整理とMVCは何が違うの?と思われるところがあるかもしれません。*3

MVCと今回の整理を比較すると、PresentationはVC、ApplicationはM(もしくはCを含む)、Infrastructureは あえていえばM*4、DomainはM というのが私の理解です。

Infrastructureをレイヤーとして明確に分け、各層とはインターフェースを介して疎結合を実現する、というところがDDDの実装面で学んだの私なりのポイントです。そして、ここがMVCと混ぜて考えてしまい混乱してしまったところでもあります。*5

クライアントサイドは全てPresentationでも無い

SPA*6は、すべてPresentaionか?というと、それは違います。クライアント側はクライアント側でレイヤー分割をした実装をすべきです。

基本的な整理は同じで、Infrastructure層ではJsonやWebSocketによりサーバーからデータを取得したり、サーバーに更新指示をする、というイメージです。

テスト

各層を疎結合にするレイヤー構造であれば、プロジェクト(パッケージ)毎にリポジトリを設けることも可能です。

1つのプロジェクト(パッケージ)だと時間のかかるテストでも、分散して構成管理することで並列でテストを行うことも出来ます。

最終的に結合したテストは必要ですが、それ以前のビルドやテストを並列に実施できるメリットは大きいと考えます。*7

この辺りは、具体的な実装をしていく中で自分なりに分割・分散をやってみたTIPSを積み上げられればと思っています。

参考資料

www.slideshare.net

一旦まとめた後に見つけた資料。
Go言語だけれど 参考になるところが多そうなのでメモ


DDD超入門(後編) - Domain-Driven Designの適用Step - エンタープライズギークス (Enterprise Geeks)


*1:OSSなどの外部ライブラリも含む

*2:もちろん自動生成で複雑なことに対応もできると思いますが、あくまでイメージとして

*3:少なくとも私自身がそうでした。

*4:個人的にはMVCでは表現されていないと思っています

*5:ちなみに、VがPresentation、CがApplication、というように単純な置き換えレベルで考えていました

*6:Single Page Application

*7:私のシステムのような小粒なものだと効果はあまりありませんが(笑)

【雑記】ささやかかもしれないですが

昨日(3月7日)、ブログのトータルアクセスが1万件を超えていました。

書評が異常な底上げになっているので、他人のふんどし部分が大きいと思いますが それでも嬉しいものですね。

ブログ作成当初、JSFによるSelectItemsをAjaxで操作することについて、実際に自分が調べたけど まとまったものが無かったということもあって書いた

vermeer.hatenablog.jp

が当初から今に至るまで 常に上位にいるようなので*1、自分と同じようなことを調べていた方へのささやかかもしれませんが手助けになっていればと思います。

代わりに自分としてはコードも含めて時間のかかったAnnotationProcessor周りのことについては全くアクセスは無いという(笑)。

まぁ、これについては自分のライブラリのための まとめなので汎用性のないものであり仕方ないかなぁとも思います。

Visitorパターンを使っていない*2ところもあって、あんまり参考にならないところもあるでしょうね。

ブログを1年ちょっと書いていて、一番助けられているのは自分自身なのは間違いないところなので、これからもマイペースに整理していこうと思う次第です。

*1:アクセス数は少ないですよ

*2:正しくは私が使えなかった