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

Javaで主にシステム開発をしながら思うところをツラツラを綴る

Application層の検証結果の出力順序を制御する

vermeer.hatenablog.jp

上記ではDomainやFormでの検証結果については、Pageクラスで指定した順序でメッセージ出力する事が出来ました。

ですが、Application層以降の検証不正については、レイヤーを跨った関連付けをする仕組みを持たないと その順序性を管理する事ができません。ということで色々と考察をしたのが以下です。

vermeer.hatenablog.jp

そして今回は考察を踏まえて実装した機能について説明します。

実装例であれば「こういう実装をして実行したら、結果として こんな感じになります」ということになると思いますが、今回は 仕組みを中心に説明をしたいと思います。
つまりApplication層から、どういうやってメッセージの順序を編集しているのか という感じで 遡るイメージです。

Code(関連するもの)

以下の説明でポイントになるコード。 (全コードは最後にリンクしています)

Service

package exsample.jsf.application.service;

import spec.interfaces.application.commnand.Command;
import spec.validation.PostConditionValidationGroups.PostCondition;
import spec.validation.PreConditionValidationGroups.PreCondition;
import spec.validation.Validator;
import spec.annotation.application.Service;
import exsample.jsf.domain.model.user.User;
import exsample.jsf.domain.model.user.UserRepository;
import javax.inject.Inject;
import javax.validation.constraints.AssertTrue;

@Service
public class RegisterUser implements Command<User> {

    private User user;

    private UserRepository userRepository;
    private Validator validator;

    public static class Error {

        public static final String SAME_EMAIL_USER_ALREADY_EXIST = "{same.email.user.already.exist}";
        public static final String USER_CANNOT_REGISTER = "{user.cannot.register}";
    }

    public RegisterUser() {
    }

    @Inject
    public RegisterUser(UserRepository userRepository, Validator validator) {
        this.userRepository = userRepository;
        this.validator = validator;
    }

    @AssertTrue(message = Error.SAME_EMAIL_USER_ALREADY_EXIST, groups = PreCondition.class)
    private boolean isNotExistSameEmail() {
        return userRepository.isNotExistSameEmail(user);
    }

    @Override
    public void validatePreCondition(User entity) {
        this.user = entity;
        validator.validatePreCondition(this);
    }

    public void with(User user) {
        validatePreCondition(user);
        userRepository.register(user);
        validatePostCondition(userRepository.persistedUser(user));
    }

    @AssertTrue(message = Error.USER_CANNOT_REGISTER, groups = PostCondition.class)
    private boolean isExistEntity() {
        return userRepository.isExistEntity(user);
    }

    @AssertTrue(message = Error.SAME_EMAIL_USER_ALREADY_EXIST, groups = PostCondition.class)
    private boolean isNotExistSameEmailAtOtherEntity() {
        return userRepository.isNotExistSameEmailAtOtherEntity(user);
    }

    @Override
    public void validatePostCondition(User entity) {
        this.user = entity;
        validator.validatePostCondition(this);
    }

}

ポイント

    public static class Error {

        public static final String SAME_EMAIL_USER_ALREADY_EXIST = "{same.email.user.already.exist}";
        public static final String USER_CANNOT_REGISTER = "{user.cannot.register}";
    }

ServiceとPageで使用するメッセージ情報を関連付けるために 定数のインナークラスを作成しました。

Controller

import spec.annotation.presentation.controller.Controller;
import spec.annotation.presentation.controller.EndConversation;
import spec.annotation.presentation.controller.ViewContext;
import exsample.jsf.application.service.RegisterUser;
import exsample.jsf.application.service.UserService;
import exsample.jsf.domain.model.user.User;
import javax.inject.Inject;

@Controller
public class UserRegistrationAction {

    @ViewContext
    private UserRegistrationPage registrationPage;

    private UserService userService;

    private RegisterUser registerUser;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, RegisterUser registerUser) {
        this.registrationPage = registrationForm;
        this.userService = userService;
        this.registerUser = registerUser;
    }

    public String fwPersist() {
        this.registrationPage.init();
        return "persist-edit.xhtml";
    }

    public String confirm() {
        User requestUser = this.registrationPage.toUser();
        registerUser.validatePreCondition(requestUser);
        return "persist-confirm.xhtml";
    }

    public String modify() {
        return "persist-edit.xhtml";
    }

    public String register() {
        User requestUser = this.registrationPage.toUser();
        registerUser.with(requestUser);
        User responseUser = userService.persistedUser(requestUser);
        this.registrationPage.update(responseUser);
        return "persist-complete.xhtml";
    }

    @EndConversation
    public String fwTop() {
        return "index.xhtml";
    }

}

ポイント

Serviceから発行された検証不正とPageのFieldを関連付ける対象は @ViewContextで宣言したものを対象とします。

Page(Form)

import spec.annotation.FieldOrder;
import spec.validation.Validator;
import spec.annotation.presentation.view.InvalidMessageMapping;
import spec.annotation.presentation.view.View;
import exsample.jsf.application.service.RegisterUser;
import exsample.jsf.domain.model.user.GenderType;
import exsample.jsf.domain.model.user.User;
import exsample.jsf.domain.model.user.UserId;
import exsample.jsf.presentation.userregistration.form.DateOfBirthForm;
import exsample.jsf.presentation.userregistration.form.EmailForm;
import exsample.jsf.presentation.userregistration.form.GenderForm;
import exsample.jsf.presentation.userregistration.form.NameForm;
import exsample.jsf.presentation.userregistration.form.PhoneNumberForm;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.validation.Valid;

@View
public class UserRegistrationPage implements Serializable {

    private static final long serialVersionUID = 1L;

    private Validator validator;

    private UserId userId;

    @Valid
    @FieldOrder(1)
    @InvalidMessageMapping(RegisterUser.Error.SAME_EMAIL_USER_ALREADY_EXIST)
    private EmailForm userEmail;

    @Valid
    @FieldOrder(2)
    private NameForm name;

    @Valid
    @FieldOrder(3)
    private DateOfBirthForm dateOfBirth;

    @Valid
    @FieldOrder(4)
    private PhoneNumberForm phoneNumber;

    @Valid
    @FieldOrder(5)
    private GenderForm gender;

(メソッドは省略)

}

ポイント

    @FieldOrder(1)
    @InvalidMessageMapping(RegisterUser.Error.SAME_EMAIL_USER_ALREADY_EXIST)
    private EmailForm userEmail;

Serviceで発行した検証不正のメッセージと、Pageのフィールドを関連付け。

Interseptor

import ee.jsf.messages.JsfMessageConverter;
import ee.validation.ConstraintViolationsHandler;
import ee.validation.ViewContextScanner;
import java.util.List;
import javax.annotation.Priority;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import spec.annotation.presentation.controller.Action;
import spec.interfaces.infrastructure.CurrentViewContext;
import spec.interfaces.infrastructure.MessageConverter;
import spec.interfaces.infrastructure.MessageHandler;
import spec.validation.BeanValidationException;

@Action
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@Dependent
public class BeanValidationExceptionInterceptor {

    private final CurrentViewContext context;

    private final MessageConverter messageConverter;

    private final MessageHandler messageHandler;

    @Inject
    public BeanValidationExceptionInterceptor(CurrentViewContext context, JsfMessageConverter messageConverter, MessageHandler messageHandler) {
        this.context = context;
        this.messageConverter = messageConverter;
        this.messageHandler = messageHandler;
    }

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {

        String currentViewId = context.currentViewId();

        try {
            return ic.proceed();
        } catch (BeanValidationException ex) {
            ConstraintViolationsHandler handler = new ConstraintViolationsHandler.Builder()
                    .messageSortkeyMap(ViewContextScanner.of(ic.getTarget().getClass().getSuperclass()).scan())
                    .constraintViolationSet(ex.getValidatedResults())
                    .build();
            List<String> messages = messageConverter.toMessages(handler.sortedConstraintViolations());
            messageHandler.appendMessages(messages);
            return currentViewId;
        }

    }
}

と、そのInterceptorで使っている主たる機能

public class ConstraintViolationsHandler {

    private final List<SortKeyConstraintViolation> sortKeyConstraintViolations;

    private ConstraintViolationsHandler(List<SortKeyConstraintViolation> sortKeyConstraintViolations) {
        this.sortKeyConstraintViolations = sortKeyConstraintViolations;
    }

    public List<ConstraintViolation<?>> sortedConstraintViolations() {
        return sortKeyConstraintViolations.stream()
                .sorted(comparing(SortKeyConstraintViolation::getSortkey)
                        .thenComparing(s -> s.getConstraintViolation().getMessageTemplate()))
                .map(SortKeyConstraintViolation::getConstraintViolation)
                .collect(Collectors.toList());
    }

    public static class Builder {

        private final MessageTmplateSortKeyMap messageTmplateSortKeyMap;
        private Set<ConstraintViolation<?>> constraintViolationSet;

        public Builder() {
            messageTmplateSortKeyMap = new MessageTmplateSortKeyMap();
            constraintViolationSet = new HashSet<>();
        }

        public Builder messageSortkeyMap(MessageTmplateSortKeyMap messageTmplateSortKeyMap) {
            this.messageTmplateSortKeyMap.putAll(messageTmplateSortKeyMap);
            return this;
        }

        public Builder constraintViolationSet(Set<ConstraintViolation<?>> constraintViolationSet) {
            this.constraintViolationSet = constraintViolationSet;
            return this;
        }

        public ConstraintViolationsHandler build() {
            return new ConstraintViolationsHandler(
                    messageTmplateSortKeyMap.replaceSortKey(
                            SortkeyConstraintViolationConverter.of(constraintViolationSet).toList()
                    ));
        }

    }
}

ポイント

前提として@Controller@Stereotype@Actionを組み込んでいます。

BeanValidationExceptionInterceptor

コードに説明用コメントを追記しました。

try {
    return ic.proceed();
} catch (BeanValidationException ex) {
    ConstraintViolationsHandler handler = new ConstraintViolationsHandler.Builder()
    
            // ViewContextScanner で @ViewContext で指定したクラスから 
            // @InvalidMessageMapping から Serviceのメッセージとフィールドを関連付けて
            // @FiledOrderの順序を取得して ソートに必要な情報を編集
            .messageSortkeyMap(ViewContextScanner.of(ic.getTarget().getClass().getSuperclass()).scan())
            .constraintViolationSet(ex.getValidatedResults())
            .build();
            
    // 検証不正をソート順で並べてメッセージ出力する
    List<String> messages = messageConverter.toMessages(handler.sortedConstraintViolations());
    messageHandler.appendMessages(messages);
    return currentViewId;
}

ざっくり

上述のポイントを何となく見てもらえれば雰囲気は伝わるかと思いますが、ざっくりと流れを説明すると

  • Serviceから検証不正を発行
  • Presentation層のInterceptorで捕捉
  • Interceptorでは Controllerで使用するPageクラスを@ViewContextで特定
  • @ViewContextで特定したPageクラスの@InvalidMessageMappingとServiceのメッセージで検証不正の結果を関連付け
  • 関連付けしたフィールドの@FieldOrderで順序を取得
  • 「メッセージと順序」をペアを作成
  • 検証不正情報から「メッセージと検証不正結果クラス」のペアを作成
  • 「メッセージと順序」と「メッセージと検証不正結果クラス」で「順序と検証不正結果クラス」を作成
  • 「順序と検証不正結果クラス」をソートして、検証不正結果クラスの情報から メッセージを取得して出力

という感じです。

最後の方の「順序と検証不正結果クラス」で まとめておいて、最終的にメッセージを出力するだけなところに冗長さを感じるかもしれませんが、この辺りは Domainの検証不正の仕組みとあわせて処理するようにしているための施策です。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

今回は順序情報について取得・編集をしました。
この方法で、フィールド情報との関連付けができそうなので 色々と応用が出来る気がします。

パッケージ構成の考察(2)

vermeer.hatenablog.jp

で ベースを考えて 以下の参考プロジェクトを コツコツと肉付けしています。

vermeer.hatenablog.jp

新しい機能を実装する中で段々と どのパッケージに どのクラスを配置させるのが良いのか混とんとしてきました。
このまま なんとなく続けるのではなく、一旦 手を止めてパッケージの構成を整理することにしました。

概要

以前の考察のメインは青色のところで、今回の考察のメインはオレンジのところ。

実際のパッケージ構成

+---ee
|   +---interceptor
|   |   +---controller
|   |   +---scope
|   |   |   \---conversation
|   |   \---validation
|   +---jsf
|   |   +---context
|   |   +---exceptionhandler
|   |   \---messages
|   \---validation
|       \---priority
|
+---exsample
|   \---jsf
|       +---application
|       |   \---service
|       +---domain
|       |   +---model
|       |   |   \---user
|       |   \---validation
|       +---infrastructure
|       |   \---datasource
|       |       \---user
|       \---presentation
|           \---userregistration
|               \---form
|
+---spec
    +---annotation
    |   +---application
    |   \---presentation
    |       +---controller
    |       \---view
    +---exception
    +---interfaces
    |   +---application
    |   |   \---commnand
    |   \---infrastructure
    +---scope
    |   \---conversation
    \---validation

specパッケージ

spec、つまり仕様を表現するパッケージです。

プロダクトにおける統一的なインターフェースであったり、宣言的実装のためのアノテーションだったりを まとめたものです。

オレオレFWにおける枠組みであり、 特定の技術に極力依存しないようにして、純粋な仕様として可搬性を担保することを意識しています。

例えば依存するパッケージは java.*またはjavax.*に限定するといった感じです。

eeパッケージ

実装のためのパッケージです。

各種ライブラリはプロダクトの infrastructureで ライブラリを直接使用しても良いのですが、毎回つくるのは 面倒です。
specのアノテーションを マーカーとして各種機能を提供したりします。

オレオレFWの実装にあたります。

考察

specにおけるパッケージの命名ルールですが、用途と レイヤーのどちらを先にするのか迷いました。

今回は 用途を先にしてレイヤーを後にしました。
理由はvalidationなどのレイヤーを跨った仕様を扱う際、レイヤーを上位にすると どうもしっくりしなかったからです。

spec.interfacesは、厳密には spec.forproduct.interfacesとかにした方が良いかもしれませんが必要以上に長くなるのも面倒だな、と思って 短くしました。ただ、他のパッケージが増えて来たりする中でコンテキストとして分類しておくべきだと思ったら、spec.forproduct.interfaces または spec.product.interfaces というように サブパッケージを設けるかもしれません。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

今後もサンプルプロジェクトとして肉付けを続けていきますが、最終的には specパッケージを仕様の集約として、eeパッケージを そのサンプル実装として まとめ上げたいと思っています。

パッケージ構成について、迷いが無いかと言うと、、あります。
ありますが、何を見直すべきか まだ分かりません。

焦らずコツコツ。

Application層の検証結果をPageに関連付ける方法を考える

試行錯誤の 垂れ流しです。

やりたいこと

  • BeanValidationの結果や実行時例外を Pageクラスの特定のフィールドに関連付ける
  • 関連付ける対象フィールドは複数のケースもある

フィールドに直接関連付けたいのはメッセージ順序の情報を保持している Pageクラスのフィールドに付与した Annotation(@FieldOrder)から情報を取得したいからです。

なお、BeanValidationの結果は 特定のフィールドに関連付けて明示するべき要件になると思いますが、検証不正以外の実行時例外において 特定のフィールドに関連付ける必然は無いようにも思います。ということで、実行時例外とフィールドとの関連付けは後回し、もしくはしなくても良いかもしれないとして一旦は 考慮から外しておきます。

必要な要素

BeanValidationのメッセージキー(message template)と Pageクラスのフィールドのペアとなる情報。

例えば、Factoryクラスで表現するとしたら

ErrorMappingField.of("message.key",Hoge.class.fieldA)

複数だったら

ErrorMappingField.of("message.key",{Hoge.class.fieldA,Hoge.class.fieldB})

みたいな感じ。
なお、例としてFactoryで表現はしたけど、Factoryにするという訳ではありません。

関連付け処理は、BeanValidationをクライアント用のメッセージ変換をするのと同様に ControllerのAction用のInterceptorで行います。 要素としては、マッピング処理用のInterceptorを準備するということになります。

レイヤー

処理はInterceptorで行うとして、定義は どのレイヤーに配置するのが良いでしょうか?

発生源から遡る形で考察します。

Application

Application層の検証不正は、主にServiceから発行されるので、Serviceをベースに考えます。 ここで出来ることは、BeanValidationで検証不正理由となる情報を message で表現することだけです。

次に考えるとしたら、message に指定する情報を String(文字列)とするのか、EnumクラスのValueを使用するようにするか、です。

@AssertTrueのメッセージを見れば、要件を把握することは出来ますし、幸い Annotationで情報を付与しているので 実行時であれ ツールであれ Serviceに どのような message が指定されているのか抽出することも可能です。
その上で Enumクラスでわざわざクラス実装までする必要があるか?というところです。

ここで、検証不正を発行する側ではなく、発行された検証不正情報を扱う 利用側の立場で考えてみます。 変換するためには、元の検証不正の情報(message)を漏れなく、誤記なく把握したいです。
となると、

  • ビルド時にAnnotationから情報を取得してチェックする
  • Enumで定数型クラスとして定義する

のどちらかのやり方が想定されます。
前者は そのために仕組みを構築しないといけないし、回りくどさのあるアプローチだと思います。 (ほぼ必然的ですが)Enumクラスを作って Serviceにおける message の設定でも、その value を使用するというのが良さそうです。

Enumクラスにするとして、それは どのレイヤーで管理するのが良いでしょう?

Serviceクラスから発行されるものだから、Serviceクラスで定義して、可視性をpublicにしておいて 利用側でも参照できるようにするというアプローチが1つ。
利用側がPresentation層のクラスであるということは明確で、Interceptorで変換する際にも利用するので 全レイヤーから横断的に使用するということで Domainレイヤーで扱うというのが、もう1つ。

直感的には、Serviceクラス内で定義しておくのが良さそうに思います(つまり1つ目のアプローチ)。
その上で全レイヤーから扱えるような設計をカバーしたいところです。
全レイヤーから操作をするために、Enumクラスに message と 関連するターゲット を取得するインターフェースを設けて、それを経由して操作するようにすれば 良いかな。

もう1つのDomainレイヤーで扱うというのも悪くは無いと思っていて、雑に言うと message.propertiesで検証不正の情報をメッセージに変換するのだから、逆に それを使うだけという考え方。
message.properties の情報から Enumクラスを同期を取って作成して それを使えば 検証不正の結果出力において 表示が出来なかったということにはならないので 表示安全と言えます。 欠点は、Serviceが発行する検証不正の種類がServiceクラス内で定義している場合と違って、実際のServiceの実装を見ないと分からないというところ。
つまり連携対象とすべき情報が漏れてしまう可能性があるということです。 ただ、情報が欠落するというのではなく、あくまで並び順が制御できないだけなので致命的な障害という訳でも無いという考え方もあります。

なお、補足的な話としてmessage.properties の情報から Enumクラスを同期を取って作成については、以前作成したaptライブラリを使用することを想定しています。*1

vermeer.hatenablog.jp

ただ、今考えている構想だと Keyの扱いへの考慮が足りないですし 、BeanValidation用の messageTemplate として使う場合、{ } で囲っておかないといけないなど 多少手直しというか機能追加は必要です。

ちなみに、上述だと message.properties としていますが、このライブラリは Service単位(もしくはユースケース単位)で プロパティファイルを作成しておいて、Enumクラスに変換することも可能なので 2つの要件を満たすことも出来るのではないかな?と思っています。

なんとなく、方向性は見えてきた気がします。

  • Serviecの検証不正のメッセージはEnumクラスで実装
  • システム全体 もしくは サービス単位で Enumクラスを作成
  • 当該Serviceが どのような検証不正を発行するか、については 出来そうだったら追加考察するけど、出来なかったとしても致命的な障害を引き起こすわけではないとして割り切ることも視野に入れる

一旦このくらいで次のことを考えます。

Presentation

Serviceからの検証不正をマッピングする先が、Pageクラスのフィールドなので 連携マッピングは Presentation層のパッケージで扱います。

とりあえずマッピングにおけるKey情報は、Application側で考えた Enum値だとして、ここで考えるのは Valueとなる画面側です。

なお、フィールドの背景色を変える場合は画面IDとなる文字列を 何かしらの形で関連付ける必要がありますが、とりあえず今回は メッセージ表示順なので Pageクラスのフィールドと関連付けさえできれば 一旦良しとします。

では、それを実装する場所は Controllerか Pageか?

Controller

以前、検証不正のあったフィールドの背景色を変更する時にやったのは、検証前に 実行時例外で保持させているメッセージ と 画面フィールドのIDをマッピングしておいてから検証およびServiceの実行をするという やり方をしていました。
ということで、Controllerから考えてみたいと思います。

Controllerで「BeanValidationのメッセージキー(message template)と Pageクラスのフィールドのペアとなる情報」と類似なものを 設定しておいて Serviceを実行する感じになると思います。
以前は エラー用のScopeを持ったBeanを作っておいて、DIをして実現しました。Controllerのメソッド内で設定をしていましたが、今 思えば Annotationで宣言しておいて Interceptorで処理するときに 情報を取得して 裏側で処理しても良かったかもしれません。
ということで、もし 今回 Controllerでマッピングするなら、Anntaionでクラス全体、またはメソッド単位に Annotationで宣言的に実装することにするとして、
これでも良いかな、という印象はあるのですが 検証不正とPageのフィールドとのマッピングをしようとすると、Pageクラスのフィールドの可視性を privateよりも広く公開しないといけません。そうしないと Controllerから指定できないからです。

その上で気になるというか、どうしたものか、というところ

  • 多量のマッピングがあった時には ControllerがAnnotationまみれになってしまう
  • ネストや繰り返しになっている場合の指定方法は どうする?

正直、ノーアイディアなので、以前の検証不正の背景色変更の時に どうやって実装的にやったのかな?と思って確認してみると。。
メッセージ と 画面フィールドのIDをマッピングした情報を Scope管理下のBeanに保持しておいて、Faceletsの 色を意味するclass値を メソッド経由でDIしたBeanから取得するということをしていました。*2

単一フィールドは

class="#{errCssForm.find().cd}"

テーブルのフィールドは

 class="#{errCssForm.find(row.id).familyName}"

こんな感じです。 JSF(Facelets)だと、参照する Map型のインスタンスのキー値の指定ができるというだけです。
errCssForm.find()の戻り値として、画面IDとclass値のMapが返却されます。そのMapにキー値cdがあった場合は、そのvalueとしてエラーを表現するclass値(背景色)が返却されるという仕掛けです。 .find以降のフィールド風の値は 型安全なものではありませんし入力補完もありません*3。あくまで キー値(String)というだけです。

もしこれを Classファイルのフィールドにやろうとしたら、、んー、やっぱり いいアイディアが浮かばない。。

ただ、ここまで考えたり思い出したりする中で分かったことはマッピングする情報はPageクラスのフィールドだから、Controllerで指定するのではなくて Pageクラス側で宣言的に定義しておいた方が良いんじゃないか?ということです。

ということで、Controllerで考えることは ここまでとします。

Page

宣言的に指定するとした場合、イメージとしては

@InvalidMessageMapping(value=RegisterUser.Error.EXISTS_USER)
@FieldOrder(1)
private UserName uesName;

みたいな感じ。
特定のフィールドに関連付けるものだけにしか付与しないとはいえ、1つのフィールドに大量の検証不正を関連付けると Annotationまみれになってしまうのは避けようがないかもしれません。

@InvalidMessageMapping(value=RegisterUser.Error.EXISTS_USER)
@InvalidMessageMapping(value=RegisterUser.Error.ALREADY_CHANGE)
@FieldOrder(1)
private UserName uesName;

引数を配列にしたら、どうでしょう?

@InvalidMessageMapping(value={RegisterUser.Error.EXISTS_USER,RegisterUser.Error.ALREADY_CHANGE})
@FieldOrder(1)
private UserName uesName;

これは これでアリな気もしますが、手間と可読性自体は あまり変わりが無いように思います。 Annotationに第二引数を設けてグループ化する必要があれば、配列にする意味もありますが、そうでないから そこまででも無いかなぁという感じです。

次に、Pageクラスがネストした場合はどうしましょう?

全てのフィールドの配下全てのクラスに当該Annotationが存在しないか走査するという やり方もあると思いますが、さすがに性能面でやりたくありません。
とするとBeanValidationの@Validでマーカーするのと同じような仕組みで親クラス側で指定すれば良いかなと考えています。
例えば @MessageMappingが付いていれば、そのクラスのフィールドでも @InvalidMessageMapping があれば 連携情報として使用する感じです。

ところで、このようなネスト構造は 今後も色々とありそうです。

例えば

vermeer.hatenablog.jp

で扱った@GroupLabelも似たような発想のものです。 もっと広く見ると、@FieldOrderが付与されているフィールドも、同じかもしれません。
細かくAnnotationを設けておくことで、細かい制御ができるメリットはありますが、細かすぎるとAnnotationまみれになってしまいます。
多少、冗長であることは覚悟の上で、マーカーとなるAnnotationについては、もっとザックリと 一纏めにした方が良いような気もします。
たとえば、Pageクラスに対してはスコープを管理するAnnotationとして@Stereotypeで集約した@Viewというものを作成しました。それと同じようにフィールド用に@ViewItemというものを作成しても良いかもしれません。
イメージとしては、基本仕様は@FieldOrderと同じで、任意項目はパラメータで指定する感じ。 悩ましいのは、Annotationまみれから、パラメータ過多のAnnotaionになっただけ、となりそうなところ。
逆の発想としては@ViewItemを作るよりも、配下の情報を参照するマーカーとして@FieldOrder@GroupLabelの いずれかがあった場合は対象とするという やり方の方が良いかもしれません。 これなら、任意のAnnotationを後から追加できる仕組みだから柔軟かもしれません。

ネスト構造については、このくらいかな?

さて、ネストに似ているけど 異なるテーブル構造(繰り返し)は どうでしょう?

これまでの整理では、行情報よりも列情報の指定が優先されることになることになりそうです。
対処案としては Annotation、@Valid が付与できるコレクション、Annotationという順番で 順序制御をすれば、目的は満たせそうです。
@Valid が付与できるコレクションとは、具体的には「配列、 Iterable を実装したコレクション、 Map」を考えています。
発想の元がBeanValidationだから、まぁこれで良いかな?というくらいの雑な整理です。 ただ、現状準備している実験場は、テーブル形式の入力サンプルは無いので、とりあえず保留。

段々と 整理出来つつあるように思いますがServiceで検証不正が発生した結果をマッピングするPageクラスと そもそも どうやって関連付けしましょう?

Actionで指定

Actionのクラスまたはメソッドに、レスポンスに使用するコンテキストとして指定するイメージです。

@ResponseContext(HogePage.class)
public String hoge(){
    return "index.xhtml"
}

とか

@ResponseContext(HogePage.class)
public class UserRegistrationAction {

    public String hoge(){
        return "index.xhtml"
    }

}

クラスに指定したら、配下のAction全てに適用するイメージです。
この場合の発想としては、ServiceのCallerであるControllerのAction単位で、そのレスポンスとなるPageクラスを指定するという考え方です。
PageクラスはAggregate Rootとして扱っていますが、例えば 複数のコンポーネントで構築されているという実装をした場合なら、パラメータに複数のクラス指定をするイメージです。

フィールドに指定

PageクラスのFieldにAnnotationを付与して宣言的に表現する。

public class HogeAction {

    @ViewContext
    private FugaPage;

    public String hoge(){
        return "index.xhtml"
    }

}

もし、複数のコンテキストで実装されている場合は、それぞれのFieldにAnnotationを付与します。

で、どっち?

コードを見つつ思った印象としては、後者の方が良い気がします。

理由は、Annotationのパラメータとしてクラスを指定するやり方だと、実際にActionで使用しているPageクラスであるかどうか保証がない気がするからです。単純なタイプミスも含めて 実際に使用しているFieldの型と目視なりコンパイラなりで確認をしないといけません。
また基本的にServiceから発行された検証不正のマッピング用途が現状の想定ですが、それ以外の「Service - Controller - Form」を関連付けるマーカーという意味で考えても 意図が伝わりやすい方式のように思います。

参考情報

JavaEE使い方メモ(Bean Validation) - Qiita

さいごに

考え始めた時の構想とは随分と異なる結論になった気がしますが、なんとなく自分としては 腑に落ちる整理が出来た気がします。
あとは実際に実装してみて、出来る出来ない 実際に出来たコードを見て考え直す、というプロセスに入ろうと思います。


追記

2018/10/26

Annotationのパラメータとしてインターフェースを指定できませんでした。
Enumは指定できますが、そうするとEnum毎にAnnotationを準備しないといけません。

Service毎にEnumを作るのではなく、システム全体で1つのEnumを作って 全てのServiceで参照をするというやり方ならできるとは思います。
出来れば Service単位で管理したいのだけれど最悪 そうするしかないのかなぁ。。
でも、そうすると@InvalidMessageMappingが構造(仕様)を現すAnnotionではなくで具体的な実装と密になってしまうのでNGです。

となると、Stringで指定する方法で考えるしかないかな? 考えられる やり方としては Serviceクラスに 昔懐かしい 定数クラス方式。
少なくとも 構造的な記述を静的に実装はできます。
型安全な実装指針ではなく、実装作法を示す必要があるので、出来れば 基本型を使うような方式は取りたくなかったけど 仕方ないかなぁ。。

あっ、また あのライブラリが日の目を見るタイミングを逸してしまったのでは・・・。

なお、配列指定については、@InvalidMessageMapping側で 配列指定しておけば、上述の両方の要件は満たせるようです。


*1:やっと日の目を見るか!?

*2:文章だと訳が分からないところですが、とりあえず雑に書いておきます

*3:型が無いMapに格納される値なので 入力補完のやりようがない

Page/Formのvalidateを見直す

vermeer.hatenablog.jp

vermeer.hatenablog.jp

で考えたことに対して、実際の実装を経て 妥協(?)した

vermeer.hatenablog.jp

を踏まえて、改めて整理をした方が良さそうに思い 考えてみることしました。

はじめに

考え直してみようと思ったことを呟いたもの。

振り返り

  • Pageは、Formの集約ルート
  • Formは、DomainObjectをPresentation用に包含したクラス
  • ScopeはPage単位で制御
  • Pageは、Entityへの変換をする
  • FormとDomainObjectにValidationを定義してControllerで検証
  • Pageは、Formを管理するPOJOとしたい

ざっくりいうと、Viewのデータ管理と変換をするPOJO ということを役割にしようと思いました。 ですが「FormにValidationを定義してControllerで検証」が 想定通りには出来ませんでした。

CDIインスタンスから値を取得するためには、アクセッサ(getter)を使わないといけない」というところが理由です。

対処案としては

  • Pageクラスにvalidateメソッドを準備して それを使う
  • 検証用のアクセッサ(getter)を準備して@Validを設定する
  • 検証用に新たにインスタンスを生成する

いずれかで、できそうでした。

「Pageクラスにvalidateメソッドを準備して それを使う」は そもそもvalidateはControllerで行いたいということで採用しませんでした。
理由はValidatorをPageクラスにDIしないといけないから、私の想定しているPOJOの範疇を超えている気がしたというのも理由です。
「検証用のアクセッサ(getter)を準備して@Validを設定する」を採用しなかったのは、 検証用のアクセッサというのが、どうもしっくりこないというか アクセッサが多すぎるというか、それだったら「検証用に新たにインスタンスを生成する」で 役割を明確にしたインスタンスを返却するということで まとめた方が可読性が良いかな?ということで一旦 整理をしました。

考察

「検証用に新たにインスタンスを生成する」で、一旦 良し としたのですが、フィールドの並び順の指定など、Viewのデータ管理を Pageクラスで 実装していくことで「Pageクラスではなく 検証用のインスタンスに 関心事が集約されている」という状態になってしまっていると感じるようになりました。

となると、有力な候補となる実装は validate(this) を使った やり方です。

あとは、JSFらしく作って、インスタンス生成メソッドを準備するやり方でしょうか。
発想としては検証用としてインスタンス生成をするのであれば、いっそのことプロパティをStringで管理してしまおう、と。
どうせ割り切るのであれば、これくらい割り切ってしまえ という発想です。

再考察

validate(this) をPageクラスに含める(ValiatorをPageクラスへDI)ことになるわけですが、元々は Pageクラスは 限りなくPOJOにしたい、validateはControllerで行いたいと考えていました。
こじつけであれ、方式を変える際には 自分なりの論拠を整理しておきたいところです。

さて、そもそも 私の考えていたPOJOは 当初案の時点でも達成出来うる方式だったでしょうか?
改めて疑いの目でPageクラスの実装を見て、今更ながらに気が付いた事がありました。
JSFの実装に依存する記載があります。
これは、最終的には infrastructure層とDIで解決するつもりで 先送りにするつもりだった実装です。
そうなんです、DIしないようにしたい、という発想での設計は、そもそも実現できていなかったのです。。

他にも、設計として 中途半端というか 統一されていないと感じるところがありました。
Controllerで、Validateをするためだけに、Pageクラスの内部情報をわざわざ公開して、しかもやることは validateに渡すだけという DTOなんだか ViewのEntity的役割のものなんだか、どこを目指しているのか まとまりのない実装になっていることに気が付きました。 これは、動詞を現すクラスで 検証はした方が良い、という私の思い込みも原因の1つです。
Entityを生成(Page/Formから変換)をするのであれば、不変条件は満たしていることを保証するべきでしょう。
例えば、SpringBootだったらリクエストパラメータでValidateをするように。

つまり、PageクラスにEntity(DomainObject)への変換をする役割を与えるのであれば、その検証論理だけでなく、検証実行も 防御的にすれば良かったのです。。

結論としては、こじつけでもなんでもなく、Pageクラスにて Entity(DomainObject)への変換をするのであれば 不変条件検証も もれなく実施すべきであるので validateはPageクラスでする、というだけだったということです。

今 思えば、なんで考えが行き届かなかったのだろう?ということのような気もしますが、実際のところ Serviceクラスの実装において事前条件・事後条件の検証実行ロジックの置き場所を考えたから、今 改めて考えてみて気が付いたという気もしています。

実装

対応前

Page

@View
public class UserRegistrationPage implements Serializable {

    private static final long serialVersionUID = 1L;

    private UserId userId;

    private EmailForm userEmail;

    private NameForm name;

    private DateOfBirthForm dateOfBirth;

    private PhoneNumberForm phoneNumber;

    private GenderForm gender;

    public UserRegistrationPage() {
    }

    public UserRegistrationPage(UserRegistrationPage me) {
        this.userId = me.userId;
        this.userEmail = me.userEmail;
        this.name = me.name;
        this.dateOfBirth = me.dateOfBirth;
        this.phoneNumber = me.phoneNumber;
        this.gender = me.gender;
    }

    public void init() {
        this.userId = new UserId();
        this.userEmail = new EmailForm();
        this.name = new NameForm();
        this.dateOfBirth = new DateOfBirthForm();
        this.phoneNumber = new PhoneNumberForm();
        this.gender = new GenderForm();
    }

    public void update(User user) {
        this.userId = user.getUserId();
        this.userEmail = new EmailForm(user.getUserEmail().getValue());
        this.name = new NameForm(user.getName().getValue());
        this.dateOfBirth = new DateOfBirthForm(user.getDateOfBirth().getValue());
        this.phoneNumber = new PhoneNumberForm(user.getPhoneNumber().getValue());
        this.gender = new GenderForm(user.getGender().getValue());
    }

    public Map<String, String> checked(Integer index) {
        GenderType genderType = GenderType.find(index);
        Map<String, String> map = new HashMap<>();
        if (this.gender.isSameType(genderType)) {
            map.put("checked", "checked");
        }
        return map;
    }

    public String targetFor(UIComponent component, String targetName) {
        return component.getClientId() + "-" + targetName;
    }

    public void setGender(Integer index) {
        this.gender = new GenderForm(GenderType.find(index));
    }

    public User toUser() {
        return new User(this.userId, userEmail.getValue(), name.getValue(), dateOfBirth.getValue(), phoneNumber.getValue(), gender.getValue());
    }

   //検証用のインスタンスを生成するメソッド
    public Object getValidationForm() {
        ValidationForm obj = new ValidationForm();
        obj.userEmail = userEmail;
        obj.name = name;
        obj.dateOfBirth = dateOfBirth;
        obj.phoneNumber = phoneNumber;
        return obj;
    }

   //検証用のクラス定義。順序やValidを指定している。
    @SuppressFBWarnings("URF_UNREAD_FIELD")
    private static class ValidationForm {

        @Valid
        @FieldOrder(1)
        private EmailForm userEmail;

        @Valid
        @FieldOrder(2)
        private NameForm name;

        @Valid
        @FieldOrder(3)
        private DateOfBirthForm dateOfBirth;

        @Valid
        @FieldOrder(4)
        private PhoneNumberForm phoneNumber;

    }

//setter getter省略

Controller

@Controller
public class UserRegistrationAction {

    private UserRegistrationPage registrationPage;

    private UserService userService;

    private RegisterUser registerUser;

    private Validator validator;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, RegisterUser registerUser, Validator validator) {
        this.registrationPage = registrationForm;
        this.userService = userService;
        this.registerUser = registerUser;
        this.validator = validator;
    }

    public String fwPersist() {
        this.registrationPage.init();
        return "persistedit.xhtml";
    }

    public String confirm() {
       // 検証用インスタンスを生成・取得して、検証を実施。
        validator.validate(registrationPage.getValidationForm());
       // 検証不正が無いということでEntity(DomainObject)を生成
        User requestUser = this.registrationPage.toUser();
        registerUser.validatePreCondition(requestUser);
        return "persistconfirm.xhtml";
    }

    public String modify() {
        return "persistedit.xhtml";
    }

    public String register() {
        User requestUser = this.registrationPage.toUser();
        registerUser.with(requestUser);
        User responseUser = userService.persistedUser(requestUser);
        this.registrationPage.update(responseUser);
        return "persistcomplete.xhtml";
    }

    @EndConversation
    public String fwTop() {
        return "index.xhtml";
    }

}

対応後

Page

@View
public class UserRegistrationPage implements Serializable {

    private static final long serialVersionUID = 1L;

    private Validator validator;

    private UserId userId;

    @Valid
    @FieldOrder(1)
    private EmailForm userEmail;

    @Valid
    @FieldOrder(2)
    private NameForm name;

    @Valid
    @FieldOrder(3)
    private DateOfBirthForm dateOfBirth;

    @Valid
    @FieldOrder(4)
    private PhoneNumberForm phoneNumber;

    @Valid
    @FieldOrder(5)
    private GenderForm gender;

    public UserRegistrationPage() {
    }

    @Inject
    public UserRegistrationPage(Validator validator) {
        this.validator = validator;
    }

    public void init() {
        this.userId = new UserId();
        this.userEmail = new EmailForm();
        this.name = new NameForm();
        this.dateOfBirth = new DateOfBirthForm();
        this.phoneNumber = new PhoneNumberForm();
        this.gender = new GenderForm();
    }

    public void update(User user) {
        this.userId = user.getUserId();
        this.userEmail = new EmailForm(user.getUserEmail().getValue());
        this.name = new NameForm(user.getName().getValue());
        this.dateOfBirth = new DateOfBirthForm(user.getDateOfBirth().getValue());
        this.phoneNumber = new PhoneNumberForm(user.getPhoneNumber().getValue());
        this.gender = new GenderForm(user.getGender().getValue());
    }

    public User toUser() {
        this.validator.validate(this);
        return new User(this.userId, userEmail.getValue(), name.getValue(), dateOfBirth.getValue(), phoneNumber.getValue(), gender.getValue());
    }

    public Map<String, String> checked(Integer index) {
        GenderType genderType = GenderType.find(index);
        Map<String, String> map = new HashMap<>();
        if (this.gender.isSameType(genderType)) {
            map.put("checked", "checked");
        }
        return map;
    }

    public String targetFor(UIComponent component, String targetName) {
        return component.getClientId() + "-" + targetName;
    }

//setter getter省略

Controller

@Controller
public class UserRegistrationAction {

    private UserRegistrationPage registrationPage;

    private UserService userService;

    private RegisterUser registerUser;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, RegisterUser registerUser) {
        this.registrationPage = registrationForm;
        this.userService = userService;
        this.registerUser = registerUser;
    }

    public String fwPersist() {
        this.registrationPage.init();
        return "persistedit.xhtml";
    }

    public String confirm() {
        User requestUser = this.registrationPage.toUser();
        registerUser.validatePreCondition(requestUser);
        return "persistconfirm.xhtml";
    }

    public String modify() {
        return "persistedit.xhtml";
    }

    public String register() {
        User requestUser = this.registrationPage.toUser();
        registerUser.with(requestUser);
        User responseUser = userService.persistedUser(requestUser);
        this.registrationPage.update(responseUser);
        return "persistcomplete.xhtml";
    }

    @EndConversation
    public String fwTop() {
        return "index.xhtml";
    }

}

所感

Controllerがスッキリしました。
Pageクラスの変換にて、不変条件が充足されている事を保証することで、Controller側は 安全な実装が達成できているように思われます。
なにより「検証用に新たにインスタンスを生成する」実装が無くなったことで、分かりやすいコードになったと思います。

メモ

DIするValidatorについて、1つだけ想定外というか、言われてみればそうなのかもしれないけど… というようなことがありました。
それは、Validatorの具象クラスである BeanValidator が そのままでは PageクラスにDI出来なかったことです。

Pageクラスが ConversationScopedであるため、DIする具象クラスは Serializableじゃないということで実行時例外が発生しました。

BeanValidator は プロパティを持たないクラスなので Serializableは不要だろうと思っていましたが、そういう訳では無かったようです。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

「じぶんのかんがえるさいきょうのふれーむわーく」の道のりは険しいですねぇ。
なにせ方式を考えている過去の自分が怪しいのですから。。


Serviceを実装

vermeer.hatenablog.jp

の流れで、Application層のServiceを実装することにしました。
また、Serviceの事前条件不正などをクライアントで どうやって出力するのか という検討の準備でもあります。

Serviceの基本的な あり方については 以下の記事で以前 整理したものをベースに実装しています。

vermeer.hatenablog.jp

加えて、先日の勉強会で得た知見を加味しつつ、自分なりに整理してみました。

vermeer.hatenablog.jp

なお、以下の記述において DDDは参考にしていますが、あくまで 私の考えるDDD風なシステム設計(および実装)です。
したがって、自分用に表現を変えているところがあります。
例えば

  • Serviceは、概ね ApplicationServiceに近しいもの
  • Ruleは、概ね BusinessServiceに近しいもの

という感じです。

そういう意味では、分かりにくい(紛らわしい)ところもあろうかと思います。

Service

機能の振る舞いを表現

「〇〇する」という表現に属するものを扱います。

近しいものとして 思いつくのは、ユースケース記述です。
ただし、ここでいう振る舞いは あくまで 機能としての振る舞いであって、アクターの振る舞いであるユースケースとは粒度が異なります。
ユースケースは、目的が達成され、かつ中断が行われないことを目安としているもので、ConversationScopedトークンなどで境界を設けるとか、もしくは目的を達成するまでの一連の業務フローというような システムによるデータ保持とは全く別に中断が行われていないこと、が境界になるという認識です。

一方、Serviceは 業務の関心事ではなく、トランザクション境界(システムによる一貫性の保証を行うための境界)を扱います。

とはいえ、振る舞いに関する要件を表現するにあたって、ユースケース記述の在り方は大変参考になります。 具体的には、登録系Serviceで後述します。

判断/加工はしない

判断/加工はDomainObjectでのみ行います。
if文や try catch など判断につながる表現も一切記述しません
(強い制約で どこまでできるか やってみたいという思いも含めて)

再利用を考えすぎない

意味のある振る舞いであること、メソッドの実装を読むことで フローを鳥瞰出来ること を目的として、共通化(再利用)を目的とせず
多少冗長であったとしても 実装を見たら仕様が分かるドキュメントのようにすることが大事だと思います。
むしろ、過度に再利用を考えてしまうと、実装が似ているから 共通化してみたら 逆に使いにくいとか抽象度が高すぎる表現になってしまって、結局 何をしているのか良く分からないということになりかねないのでは?という心配が(経験則的に)あります。

とはいえ、そのあたりの塩梅は結構 難しく 気が付くと サブサービスがたくさんできてしまっていたり、手続き的な発想で実装してしまって トランザクションスクリプトにもなりやすいです。
そうならないための施策が、Serviceでは 判断/加工 を実装しない とか DomainObjectの組み合わせで実装する という制約だと考えています。

パッケージ

Serviceは システム(境界づけられたコンテキスト)の中において分類せず フラットに配置することを考えています。

当初、Entity(Aggregate)やActor をパッケージの分割粒度にしては?と考えました。
実際 ほとんどのケースでEntityとServiceは1対1になると思います。
ですが Serviceは、特定のヒト・モノ・コトに分類するというよりも、システムが提供する 振る舞い(機能)と考えた方が、私としては シックリするところがあり、少なくとも Entity(Aggregate)やActorを分類基準にするのは良くなさそうだ と考えるに至りました。

とはいえ、規模が小さければフラットでも良いのですが、システムが提供する振る舞いが大量にあった場合、未分類だと探すのも大変です。

そこで考えうるところとしては…

サブとなるコンテキスト単位(もしくは何となく)

境界づけられたコンテキストが 非常に大きくて、何かしらサブコンテキストとして 分類しているのであれば、その単位で分割しても良いかもしれません。

もしくは、なんとなく 雰囲気として分類として まとめておいたら使いやすい という理由で分けても良いかもしれないとすら思っています。

何を言いたいかというと、Serviceの主語(主体)が、境界づけられたコンテキストという 大きな主語(主体)だと考えるからです。
あれこれ考えてもみたのですが、もっともらしい整理でパッケージ分割しようとして時間をかけるくらいなら「このServiceはペアで使うことが多いから、一緒のフォルダに入れておいた方が使いやすい」くらい 緩くても良いんじゃないかな?と考えるに至ったという感じです。

ユースケース(利用者の関心事となる振る舞い)単位

もうちょっと 論理的に積み上げられた分類をしておきたい、というのであれば ユースケース単位というのは どうでしょうか?

Serviceに対するイメージとして、私は ロバストネス分析におけるコントローラー(動詞)に近しいものと感じています。
特に「動詞と動詞をつなぐコントローラー」のApplication層側に相当するものが そういう印象です。*1

そうすると、Serviceをユースケースにおけるイベントフローと捉えることが出来るので、ユースケース単位でフォルダ分割をしても良いかな?と考えました。

まぁまぁ良さそうですが、残念ながら そうでもないのです。
Serviceは、あくまで機能である以上、複数のユースケースで使用されることが想定されるからです。
とはいえ、全くダメという分類でもないと思いますので、複数のユースケースで使用されるServiceについては、Serviceパッケージ配下に配置して 特定のユースケースに依存しないServiceであることを表現すれば良いのではないかな?と思っています。
または、いったん Serviceパッケージに フラットに配置しておいて、多くなってきたな と思ったら、特定のユースケースに限定的に使用されるServiceから usecaseパッケージ配下に ユースケース名のサブパッケージを設けて移動して整理をしていく、というやり方を考えています。

Repositoryにも ロジックをちょっと実装

Serviceが使用する Repositoryからの戻り値または実行時例外を どうにか捌く必要がありますがServiceには一切の判断につながる記述を設けないという 縛りを設けているため、Serviceに判断/編集を実装するわけにはいきません。

対処実装として Repository(interface) の defaultメソッドを使って ちょっと実装をしました。
Serviceに条件分岐を書かなくても良いようにもなるし、ちょっとした実装のためだけに わざわざ Ruleを作成する必要もありません。
RepositoryImpl(infrastructure層)に実装する方法もありますが、こちらには 極力 ロジカルな実装をしないようにしておきたい*2ので、今 考えているのは Repository の defaultメソッド方式です。

public interface UserRepository {

    public List<User> findAll();

    /**
     * 検索キーを元に最新のEntityを取得します.
     *
     * @param user 最新を取得するEntity
     * @return 取得した最新のEntity
     * @throws UnexpectedApplicationException 対象Entityが存在しない場合
     */
    public default User persistedUser(User user) {
        return this.findByEmail(user).orElseThrow(() -> new UnexpectedApplicationException("user.doesnot.exist.findbyEmail"));
    }

    /**
     * IDを元に最新のEntityを取得します.
     *
     * @param user 最新を取得するEntity
     * @return 取得した最新のEntity
     * @throws UnexpectedApplicationException 対象Entityが存在しない場合
     */
    public default User registeredUser(User user) {
        return this.findById(user).orElseThrow(() -> new UnexpectedApplicationException("user.doesnot.exist.findbyid"));
    }

    public default boolean isNotExistSameEmail(User user) {
        return this.findByEmail(user).isPresent() == false;
    }

    public default boolean isExistEntity(User user) {
        return this.findById(user).isPresent();
    }

    public default boolean isNotExistSameEmailAtOtherEntity(User user) {
        return this.findByEmail(user)
                .map(et -> et.getUserId().equals(user.getUserId()))
                .orElse(true);
    }

    public Optional<User> findById(User user);

    public Optional<User> findByEmail(User user);

    public void register(User user);

    public void remove(User user);

}

登録系Service

設計

登録系Serviceは振る舞い単位で作成します。
設計としてはユースケース記述の在り方を意識しています。

ユースケース記述 *3

項目 内容
ユースケース 〇〇する
目的 アクタまたはシステム・オーナーにとっての目的
事前条件 ユースケースを実行する前の状態
基本系列 アクターとシステムのやり取りで、典型的で正常な流れ(順序)
代替系列 アクターとシステムのやり取りで、例外的なステップ
事後条件 ユースケースが実行された後の状態
備考 ユースケールを理解するための情報、非機能要件などを書いても良い

登録系Serviceの設計への応用

項目 内容
Service名 〇〇する
目的(役割) 境界づけられたコンテキスト または ユースケースにおける 振る舞い もしくは 手続き
事前条件 Serviceを実行する前の状態
基本系列 振る舞いにおける典型的で正常な流れ(順序)
代替系列 振る舞いにおける例外的なステップ
事後条件 Serviceが実行された後の状態
備考 Serviceを理解するための情報、非機能要件などを書いても良い

という感じで、振る舞いに対して考慮したいことの観点としてユースケース記述は良さそうだと思って、自分なりに読み替えてみて それを1つの指針として 実装を考えてみることにしました。

実装と考察

具体的な実装を示しながら、説明を補足していきたいと思います。

実装

Controller

新規登録用のAction

@Controller
public class UserRegistrationAction {

    private UserRegistrationPage registrationPage;

    private UserService userService;

    private RegisterUser registerUser;

    private Validator validator;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, RegisterUser registerUser, Validator validator) {
        this.registrationPage = registrationForm;
        this.userService = userService;
        this.registerUser = registerUser;
        this.validator = validator;
    }

    public String fwPersist() {
        this.registrationPage.init();
        return "persistedit.xhtml";
    }

    public String confirm() {
        validator.validate(registrationPage.getValidationForm());
        User requestUser = this.registrationPage.toUser();
        registerUser.validatePreCondition(requestUser);
        return "persistconfirm.xhtml";
    }

    public String modify() {
        return "persistedit.xhtml";
    }

    public String register() {
        User requestUser = this.registrationPage.toUser();
        registerUser.with(requestUser);
        User responseUser = userService.persistedUser(requestUser);
        this.registrationPage.update(responseUser);
        return "persistcomplete.xhtml";
    }

    @EndConversation
    public String fwTop() {
        return "index.xhtml";
    }

}

Service

新規登録Service

@Service
public class RegisterUser implements Command<User> {

    private User user;

    private UserRepository userRepository;
    private Validator validator;

    public RegisterUser() {
    }

    @Inject
    public RegisterUser(UserRepository userRepository, Validator validator) {
        this.userRepository = userRepository;
        this.validator = validator;
    }

    @Override
    public void validatePreCondition(User entity) {
        this.user = entity;
        validator.validatePreCondition(this);
    }

    public void with(User user) {
        validatePreCondition(user);
        userRepository.register(user);
        validatePostCondition(userRepository.persistedUser(user));
    }

    @AssertTrue(message = "{same.email.user.already.exist}", groups = PreCondition.class)
    private boolean isNotExistSameEmail() {
        return userRepository.isNotExistSameEmail(user);
    }

    @AssertTrue(message = "{user.cannot.register}", groups = PostCondition.class)
    private boolean isExistEntity() {
        return userRepository.isExistEntity(user);
    }

    @AssertTrue(message = "{same.email.user.already.exist}", groups = PostCondition.class)
    private boolean isNotExistSameEmailAtOtherEntity() {
        return userRepository.isNotExistSameEmailAtOtherEntity(user);
    }

    @Override
    public void validatePostCondition(User entity) {
        this.user = entity;
        validator.validatePostCondition(this);
    }

}

Service名

一般的にはクラス名は名詞なので「目的語 + 動詞 + Command」で命名すれば良いと思います。

私は実験的な意味も含めて「動詞+目的語」で命名して実装してみました。

目的

機能における動詞ということで目的というよりも役割といった方が良いかもしれません。
ロバストネス図で、動詞から呼び出される動詞のイメージです。
実装としては、例示のように Controller(動詞)から呼び出すService(動詞)、もしくは Service(動詞)から呼び出す Service(動詞)という役割です。

事前条件

BeanValidationを使って宣言的な実装にしました。

groupsにより検証グループを指定して#validationPreCondition(user) で 対象となるグループのみを検証対象とします。

入力時のControllerでも 登録処理の事前条件だけ使用して検証しますし、 登録処理(#with )でも、防御的に検証します。

入力時は 使用する登録系Serviceの事前条件として契約的に実行しているので、登録時も同様に契約的に実行する ということも考えましたが 個人的に登録処理における事前条件検証は セキュリティのことも考えて防御的な実装にしました。

考察
事前条件が分かりやすい

登録するServiceに、必要となる事前条件を同じクラスに配置していることで 可読性が良いと思います。
BeanValidationを使って実現していますが、Controllerで「この登録処理を実行する前に 実行しておくべき検証」ということが明示出来ていれば BeanValidationではなくても良いと思います。 逆にBeanValidationの良さは、検証論理を1つ1つ個別に宣言的に配置できるというところです。
また メッセージ(検証不正の表現)を個別に出力したい場合や、優先度によってグルーピングをしたいときには BeanValidationのルールに則っているので それなりに分かりやすいと思います。

私の場合、Serviceの検証結果を、Controllerで統一的に捌くことを考えているので*4、BeanValidation を積極的に使っています。

事前条件が使いやすい

分かりやすい と ほぼ同じですが、ControllerでServiceに関する機能を実装する際に

  • 使用したいServiceの登録前に事前条件を使う
  • 登録する

という関連が同じServiceクラスで表現されているので、Serviceの内部を知らなくても良いので使いやすいです。
検証と登録を別クラスに分けると、「このServiceの実行前に、このService(もしくはDomainObject や Rule)を実行しておくこと」という 仕様を理解しておかないといけなくなります。
契約的な検証メソッドを使用する際、同一責務の範疇として 同じクラスに事前条件検証メソッドがあると使いやすい(分かりやすい)と思います。

パラメータValidationじゃないの?

実装経験はありませんので 参考にしているSpringBootプロジェクト*5のControllerの実装だとそうなっているということでの 追加考察というレベルですが、パラメータValidationは どうなんでしょうか?

結論としては、やるとしたら 条件付きになるだろう、という感じです。

まずパラメータで検証しているのは 事前条件というよりも不変条件だと思います。
入力対象のDomainObjectが型クラスとして成立している ということを保証しますが、当該メソッドの事前条件(満たされておくべき状態)を示すものではありません。
それでも、やりたい ということであれば、Serviceにおける検証は 事前検証のみとする という実装指針を設けて 不変条件・事後条件は Serviceクラスに宣言的に実装しない、という指針を立てれば とりあえず出来なくは無いと思います。

実際、当初の実装では 私は BeanValidationは不変条件で使うものということを意識していませんでした。
ただ、いざ「事前条件を表現するのであれば、事後条件も表現可能な設計指針じゃないとNGでは?」と考えた経緯があって、groups による指定を設けて分類をしよう と考えて 今回の実装に至っています。

Interfaceによる強制

振る舞いに関するクラスに対して、事前条件と事後条件の実装を統一的に強制する意図で Interfaceを設けました。

ただ、事前条件のパラメータが1つで本当に良いのか?というところに、まだ 十分に考察が出来ていない感覚があります。
一応、1つの振る舞いで扱うのは1つの集約と考えれば、まぁ大丈夫そうな気はしていますが Interfaceは強制力のある指針であるため見直す可能性があります。 *6

AOPは使わない

Interceptorで イイ感じに 事前条件・事後条件を挟み込んで実行したら 実装が楽になるのでは?と思いましたが止めました。

理由はAOPを機能要件には使用したくないという考えからです。
経験的には、良かれと思って機能要件にAOPを適用した結果、黒魔術感の強い実装になりすぎてしまい 方式を考えた直後は 良さそうに思っても、数か月後に挙動を確認した際「あれ?これ なんで動いているの?」ということがあったからです。
トランザクション境界や アクションの実行ログなどの横断的な非機能要件はAOPが有効だと思いますが、機能要件については 冗長であっても可視性を優先した方式を取った方が良いと思います。 ケースによっては 暫定的なパッチ的にAOPを使うことはあると思いますが、次リリース時には AOPではなく きちんとロジックとして組み込むべきでしょう。

基本系列(基本フロー・代替フロー)

振る舞いを実現するために、DomainObjectを組み合わせて表現します。
今回は、登録する(userRepository.register(user))だけです(すくなっ!)。
CRUDだと、これくらいになるので なかなかApplication層のうまみを感じませんね。
なので、使い捨て(?)なシステムだと、ControllerとServiceを統合したものでも十分かもしれません。

代替系列について、ちょっと気になったので、書籍*7で確認すると、代替系列に似た表現として 代替フローという分類があり 「代替フロー:基本フローより頻度が少ない正常な流れ」と説明されていました。
つまり、上述の整理では準正常も まとめて基本系列 としていると理解しました。*8
正常と準正常における 振る舞いの詳細を実装で表現しつつ、Serviceでは分岐(IF文)を実装しない方法としてはStrategyパターンで対応すれば良いかな?とザックリ思っています。 StrategyのFactoryとしてのRuleを設けるというのもあるかもしれません。

代替系列(例外フロー)

「振る舞いにおける例外的なステップ」とのことですが、書籍での 例外フロー「正常終了しない流れ」が こちらに相当すると考えます。 いずれにしても「正常に終了しない例外的な処理」ということで概ね良いでしょう。
実装としては、Domain不正を表現する独自の実行時例外を使えば良いかな、と思っています。

今回のケースだと、userRepository.register(user); において Insert時に キーの一意制約不正がDBにて発行される、というのが想定されます。
ですが、現状の実装ではDBはダミーのMapなので 発生することはありません。
この辺りは、JPAなどDataStoreの実装をする時に 具体的に詰めていきたいと思います。

事後条件

サンプル実装がダミーストレージ(というか ただのMAP)なので中途半端な考察です。

例えば、同一トランザクション中に(つまりCommitされていない)データ検証をする意味はあるのか? とか、そもそも Commit前なのに 検証が出来るのか? という疑問も残ります。 他にも、事後条件というのは そもそも どういう設計をするべきなのか?とか、事後検証不正があった場合のデータリカバリーへの連携を考慮した場合、どのような実装パターンをしておくべきなのか?とか 色々 課題はあると思っています。

そんな中で、これは面白いかもしれない、ということで扱ったものが 以下です。

ポイントは #getRegisteredUserです。

更新処理(UpdateUser#with)の事前条件は「データが存在する事」ではなくて「ユーザーが登録されている事」つまり 「RegisterUserの事後条件が充足していること」というのが 条件になると考えました。

登録処理のService(RegisterUser)と 更新処理のService(UpdateUser)が蜜結合になっていることが 良いのか?という悩ましさはあります。
でも、更新・削除の前提として新規登録されている事は密であるわけだから悪くはないだろうと思っています。

@Service
public class UpdateUser implements CommandPreCondition<User> {

    private User user;

    private UserRepository userRepository;
    private Validator validator;
    private RegisterUser registerUser;

    public UpdateUser() {
    }

    @Inject
    public UpdateUser(UserRepository userRepository, Validator validator, RegisterUser registerUser) {
        this.userRepository = userRepository;
        this.validator = validator;
        this.registerUser = registerUser;
    }

    @Override
    public void validatePreCondition(User user) {
        this.user = user;
        validator.validatePreCondition(this);
    }

    public void with(User user) {
        validatePreCondition(user);
        userRepository.register(user);
    }

    @ValidateCondition(groups = PreCondition.class)
    private ValidateCondition.Void getRegisteredUser() {
        return registerUser.invalidPostCondition(user);
    }
}

実装の補足をすると、戻り値のValidateCondition.Voidは、アノテーション@AssertTrueにして boolean にしても良かったんですが、registerUser.invalidPostCondition(user) の戻り値が、true か RuntimeException か という設計をせざるを得ず、それが 何となく気に入らなかったから 独自の型を準備しました。理想は、戻り値にvoidに出来れば良かったのですが、まぁ それはそれで有り得ないわけで。。*9

私は詳細のメッセージを出力したかったので、上述のような面倒なことを色々としましたが、新規登録の事後条件の結果をまとめて扱うのであれば、@AsserTrue にして messageを使えば良いです。

あと、実際にやってみて分かったのは、BeanValidationの検証時に実行時例外が発生したら、javax.validation.ValidationException に包含されてスローされるところです。
#getCause() で順番に遡っていけば 取得できましたが どうしたら良いのか分からず悩んでしまいました。

いずれにせよ、実際に実装してみて、文章書きながら整理していると 事後条件を考え足りていないというか、考えることに面白みがありそうな気がしてきた という感じです。
ですが、今のサンプル実装だとダミーDBでトランザクションもなく、JPAも使っておらずなので、実装で直面するべき課題をフォローできません。 現時点で 確認可能な範囲までで 以降の考察については、一旦 保留にしておきたいと思います。

目的/備考

JavaDocで表明したり、非機能要件はAnnotationで実現する、という感じです。
ここだと@Serviceが 目的の表明であり、非機能要件へのマーカーという感じです。

あとは、登録系Serviceであれば、事前条件・事後条件を示したInterfaceであったり、この2つを統合したCommandというInterfaceだったりで 構造の表明をしている、というところでしょうか。

参照系Service

登録系Serviceと違って、参照系Serviceは シンプルです。

戻り値の型単位(ファーストクラスコレクション含む)

EntityかDTOの単位(もしくはRepositoryと同じ粒度)でクラスを作れば良いかな と考えています。 取得する行為が主たる関心事ということで、登録系Serviceと同じように分割しても良いかもしれません。

正直、登録と違って 参照は 分割をする必然が 思いつきませんでした。
表面的には RepositoryとServiceとの違いがないところが 逆に気になるのですが無理に違いをつくるのも違うかな、と。
雑に言うとControllerから Repositoryを直接操作しないためのものだと考えれば 無理に違いを作る必然も無いかな、と。

メッセージ出力

今回は各種検証をBeanValidationで行う仕組みに集約して扱うようにしたので、メッセージ出力も それで扱うようになっています。

(ここから先は、自分のサンプル実装のためのメモです)

現時点で 出来ていないことは

機能要件における実行時例外を未処理

BeanValidationException(BeanValidationで発行する独自の実行時例外)以外の実行時例外があった場合、メッセージ出力する仕組みはありません。
むしろ、Beanvalidationよりも先に、汎用的な実行時例外をメッセージ出力する仕組みを先に作るべきだった気もしますが、まぁ 気になったところから順番にという感じでやっているので。。

今回の実装に関係するところでは「代替系列(例外フロー)」の実現のために 実行時例外を発行しても、その結果として クライアントに「〇〇してください」といういうようにメッセージで誘導をしたい場合の仕組みが 無い ということになります。
一応、CustomExceptionHandlerで、Interceptorで捌いていない実行時例外を処理してメッセージも出力していますが、ここでいう実行時例外は あくまで機能要件の範疇なので 本来は Interceptorで扱うべきです。

ついでにいうと、捕捉できなかった例外に備えたエラー画面への遷移も web.xmlに記述していません。

順序

Formとの関連付けの仕組みが無いので、検証不正のメッセージ出力の順序を制御する仕組みはありません。
今回は、結果的に1つしか出力されることは無いのですが 複数の結果を出力する際には順序性が担保できません。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

参考情報

www.slideshare.net

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

ユースケース記述(ゆーすけーすきじゅつ) - ITmedia エンタープライズ

さいごに

Application層の検証周りが なんとなくは出来た気がします。
事後条件については興味深い考察が少しできたような気がします。
ですが、まずはPresentation層を中心にコツコツやっていきたいので、一旦 保留として メッセージ周りに戻ろうかな、と思っています。

一人でやっていると、自分の中で蒸留できますが、逆に ループにも入るのが辛いところです。 そういう意味では、今回 非常に良いタイミングで 良い勉強会に参加できたのはラッキーでした。

あと、今回は 以前に書いた 「パッケージ構成の考察」を 何回も何回も見直ししました。
それなりには考えていたつもりですが、やっぱり実装をすることで 分かってくることが沢山あるなぁと改めて思った次第です。


*1:ユースケース駆動開発実践ガイドを、部分的に読んだだけなので 間違っているかもしれないです

*2:するのであれば Rule(Domain層)を作って、それを使うようにしたい

*3:ユースケース記述(ゆーすけーすきじゅつ) - ITmedia エンタープライズ より引用

*4:メッセージ出力に関連するところ

*5:https://github.com/system-sekkei/isolating-the-domain

*6:なお、複数パラメータになった時の対処については、今のところアイディアはありません

*7:楽天ブックス: UMLモデリング技能認定試験〈入門レベル(L1)〉問題集改訂版 - UML 2.0対応 - 竹政昭利 - 9784774132457 : 本

*8:この辺りの差異が 調べていくと ちょこちょこあって 困るけど、まぁ仕方ないかな とも思う

*9:そもそもEEのコンテナが立ち上がらなくなる

レガシーなコードにドメイン駆動設計で立ち向かった5年間の軌跡 に参加してきました

ddd-alliance.connpass.com

に参加してきました。
懇親会にも参加*1

さて、ブログに勉強会のことを書こうかな、と思ったのですが

私が、いい加減に まとめるよりも 遥かに良く かつ 素早いまとめがありましたので、そちらを紹介。

dev.classmethod.jp

私は、懇親会や その時に考えたことを ツラツラと綴ることにします。

勉強会

立ち上げ時の難しさと、継続するための難しさと、それぞれに施策を 現場の立場(つまり 知った顔のコンサルの営業トークではなく)で聞かせていただけたのは、本当に貴重な経験でした。

ごめんなさい、というところは「ドメイン駆動設計 失敗した事と成功した事」を聞いた後は、ちょうど 自分がアプリケーション層について 色々と考えているタイミングだったので、以降の話は 話半分しか入ってこなかったというところです。

感想と考察

ちょうど学びたかったことだった

アプリケーション層の実装について、ちょうど整理をしようとしているところで、個人的に色々と考えていたこともあって ドンピシャの内容でした。
ValueObjectやEntityの話は、DDD関連だと良く出てくるのですが、アプリケーション層については、ドメイン欠乏症にならないようにしましょう、という感じで その役割については「とにかく薄く」くらいしか無かったりするのですが、何を業務ロジックとするのか?ということについての考察を示していただけたのは 本当に有難かったです。

何をもって業務ロジックとするのか?

これは、なかなか難しいところ。
業務にとって意味あるものを 業務ロジックとする、というのはあるとしても、例えば「データが所得出来なかったとき」という表現で扱うロジックは その判定も含めて 業務ロジック? そうではない?
言ってしまえばケースバイケースだけれど、埋もれてしまった時に見つけるのが難しくなるくらいなら、変に「業務ロジックだけ」という分類をしないで「業務知識に相当するドメイン知識」「システムに近しいドメイン知識」として置き場所を決めておいた方が良いかもしれないと思うヒントになりました。

DomainServiceの役割

DomainServiceは、EntityやValueObjectでは表現できない業務ロジックを表現するもの、もしくは 複数のEntityを跨った業務ロジックに適用するもの、というのが私の認識だったのですが、BIGLOBEさんの 今の整理では DomainEventのFactroryとして使っているみたいでした。

確かに、そうすることで ApplicationServiceは スッキリとなっていました。

そこで気になったところと、それに対する自分の考察をツラツラと。

DomainService作りすぎてしまうのでは?

FatControllerからFatBusinessServiceになる、という話。
ApplicationServiceの進行役という役割は、どこまでシンプルにすべきなのか?という言い方でも良いかもしれないです。
ここについては、今回の話を踏まえつつ、自分なりに思うところもあり、考えるための良いヒントを頂けたので非常に良かったです。

DomainEvent とは?

私の理解では、ですが Repositoryで扱えるAggregateは基本的に1つ。
複数のAggregateにまたがった更新を行いたい場合に、
「このイベントでA(メイン)を更新したときに、一緒にB(サブ)も更新する」
という感じで「更新する」という行為そのものを DomainEventとしてオブジェクトにして Aの更新とあわせて Eventとして 通知しておいて、あとはシステム側で 例えば Observerだったり Interceptorなりが イイ感じに捌いて 結果整合性で なんとか満たす、という認識だったところ。

これは、既存のシステムがある場合、そうも言っていられないことが多くて「更新するためのオブジェクト」として 更新に必要な情報をかき集めて(これはEntityとは違う構造体)Repositoryに渡してあげることで、2つ以上のAggregate(もしくは、集約とかではなくて 単純にテーブルと言っても良い)を更新する という割り切りの産物なのかもしれない、と後から思いました。

あとは、メインであれサブであれ、結局のところ更新をするんだから分けずに全部 DomainEventに統一する、という考え方もあるかもしれません。

事前条件は どこで表現するのが良いのだろう?

ドメイン欠乏症の例で、ApplicationServiceに記述された前提条件について 確かに条件実装そのものを ベタで書くのは 止めるとして、DomainServiceの中に隠れてしまったら それはそれで ApplicationServiceにおける事前条件を確認するために DomainServiceを確認しないといけないとなるのは どうなのかな?と思いました。

Entity#verifyOrder は、OrderEvent を生成するための事前条件? それとも OrderApService#申込 の事前条件? 、同じように OrderDate#isInvalidも。

これは、私が ApplicationServiceって結局 どうあるのが良いのだろう?と、まさに悩んでいるところだったりします。

例えば、ApplicationServiceを使用する Controllerで、そのApplicationServiceが実行可能であるか 事前にチェックをしておこうと思った場合に、ApplicationService管理下の(publicではあるけれど)DomainServiceを Callするのが正しいの? という感じです。

私としては ApplicationService#validatePreCondition(entity) というものを ControllerでCallしたいというイメージです。

ざっくりいうと「このサービスの内部は知らないけど、依頼をしても良いのかい?」くらいしか Controllerとしては 意識しないという感じです。

もちろん、ApplicationServiceは 検証ロジックを呼び出す順番だけを管理して 実際の検証ロジック自体は 別クラス(DomainServiceや Domain)で実装します。

RepositoryImpl(Infrastructure層)にもロジックを実装する事もある

業務ロジックではない、条件分岐(?)は実装しているケースもあるとのこと。

この辺りは、私も悩ましいと思っているところで、例えば データ検索をして対象が無かった場合の戻り値への条件分岐を設けたいとして(データが無かったら実行時例外)、それは どうするのが良いの?という問題。
例えば、システム全体としてデータが無かった場合は、実行時例外をスローするので利用側で処理しましょう となり、かつ ApplicationServiceに条件分岐を書きたくないということだったら、あとはDomainServiceの出番になるわけですが、そうすると上述の DomainServiceまみれ、になってしまいます。

じゃあ、実行時例外はスローしない!としたところで、データの取得が出来なかったときに 例えばnullオブジェクトであれ、Optionalであれ返却するとしても、結局、条件分岐は必要です。

もちろん、条件分岐を設けることに意味のあるケースであれば DomainServiceで 実装するべきというのは 当たり前としても、常に必要というのとは訳が違います。

今、個人的に考えているのは、データ存在有無くらいで、業務ロジックとは言えないものであれば、Repository(interface)に defaultメソッドを設けて対応してしまっても良いのではないか?と思っています。
ちょっと試した感じでは、そこまで悪手でも無い気がしています。

ロジックを書くなら抽象クラスでしょう というところもあるのですが、抽象クラスは 拡張を目的としたものに使いたくて、DIの境界には使いたくないということと、インスタンス変数を保持できる仕組みだと 実装できる情報が増えすぎてしまうということで、まさに「ちょっとしたこと」しか実装できそうもない、interfaceのdefaultメソッドが ちょうど良いかな、と考えました。

Form

おっ、自分と同じ感じだ、ということで勝手に安心感を得てしまいました。
まぁ、それだけの話です(笑)

Converter

Enumによる変換クラスの実装は、学びが多かったです。
これ 一番初めに こうしよう と言った人、賢すぎ と思いました。

日本語を使っていることについても、対向システムの「表現」なので、変に自分たちの言葉に置き換え(英語化も含めて)たりせずに、そのまま使うというのは 納得感がありました。

語弊を恐れずに言うならば、対向システムの表現は 単なるラベルでしかない、という割り切りを持っておいた方が 変に言葉に流されないし 良いと思います。
また極端な話ですが、言語がアラビア語のような 不慣れなものだった場合、変に英語に翻訳して管理(アラビア語⇔英語⇔自システムドメイン)をするのか?ということを想像したら「まぁ、無いな。だったらアラビア語を翻訳して、それを ドメインで どう変換するのか考えるだけで十分」と思う気がします。

コードの中にマルチバイトが含まれることを嫌う人や、国際化対応ガーー、という人からすると、不快に思うかもしれませんが、少なくとも 私は この Converterにおける割り切りは 良い割り切りだと思いました。

Adapter

MyBatisは使っていないので分からないけど、DataStoreでDomainObject(Entity)に変換するというのは納得です。
この辺りは、自分がDataStoreを考えるときに参考にすると思います。
例えばJPAコンバーターでValueObjectから変換を頑張るんじゃなくて、がつっとAdapterで変換してしまった方がイイよね、というくらいの感じだと思っています。
なお、既存システムがあって DBの構造が複雑だと DxOを素直に準備する(作業的であったとしても)と割り切った方が、精神衛生上よろしい、と私は考えているので それとも合致すると思いました。

ユースケースという表現について

利用者において価値のある振る舞いはビジネスユースケースといい、システムにおける一連の振る舞いはシステムユースケースといっているとのことです。

この辺りは、私は

ユースケースじゃなくてサービスとしよう - システム開発で思うところ

として、割り切ってしまったところです。
そして、とりあえず このままの整理で進めてみようと思っています。

まぁ、この辺りは どう表現するのか?というところだと思うので良いのですが、いずれにしても ユースケース という言葉が意図するものについて 何かしら補足をしないと、人を悩ませるということは 再認識しました。

境界づけられたコンテキストとチーム構成

複数のチームにまたがって、 1つの境界づけられたコンテキストを扱うことはあるのでしょうか?
その場合は、どうやって認識を統一しているのでしょうか?

Ans.
またがって開発はしている。
リードエンジニアが、それぞれいるので そこで一旦検討して、それが 個々のチームに反映されて、そこから個々のチームで共有・設計をする感じ。

ということでした。
設計をチーム全体で、ということだったので どこまで「全体」とするのかな?ということでの質問でした。
なんだかんだいっても、やっぱり 何かしらの粒度で まとめ役(調整役)がいないといけないよな、という確認的な質問でしたが丁寧に答えていただきました。

ちなみに独自言語って?

詳細は、勝手に公開するのは良くないと思いますので臥せますが、本当に独自なものでした(笑)。

言語というよりも、定義という感じでしょうか?

定義を読み込むところは、何かしらの「言語」だと思いますが、少なくとも ロジックを表現するものは 独自言語という名前の定義 という感じだと思います。

増田さんの本 読みました?

読んだ人もいれば、読んだことのない人も という感じでした。

逆に EvansもIDDDもCleanArchitectureも増田さんの本も、その他多数 読んでいる人もいました。

私は 増田さんの本は読みましたが、他のDDDネタは ネットで まとまったものを見て それっぽくかじっただけなので まだまだ ですね*2

他にも・・・

西 秀和さん と帰り道に色々話をさせていただきましたが、私自身が まだ整理できていないことを 聞いて頂いた という感じで、どう文章にしたら良いのか難しく・・・。

全体を通じて

実際のロジックを示して話をしてくださったおかげで、具体的に色々と考えることが出来ました。
概念で「こんな感じ」ではなく、やっぱり コードで表現されたものを見ると、それを踏まえて さらに考察が深まるので本当に ありがたいです。
準備する側は、自分たちの実装を晒すわけで、結構 準備が大変だったりする(これは出してよい、出してはいけない。出すとしたら 分かりやすいように手直しをしようとか)と思います。
その準備のおかげで、とても分かりやすかったです。

改めてありがとうございました。

増田(@masuda220) さんと

isolating-the-domain のメンテナンスは?

github.com

増田さん自身としてメンテナンスは どういう感じでされているのでしょうか?

Ans.
教育用のライブラリとしてメンテナンスはするんだけど、 そのタイミングは 次の教育用に使う時に、がーっと見直しをする感じ(けど、それは今じゃない)

期待してます

上述のプロダクト以外で、個人的に 温めているものもあるみたいです。
公開される日を楽しみに待っています。

その他

「チームの中核になった人や立ち上げメンバーが去ってしまう…」というところについて 別の勉強会にて 会場を提供いただいた際にも仰っていましたが*3、なかなか難しいところです。
私のSIerにいた経験から勝手に考えたところとしては

社歴で役割を強制しない方が良いかも

例えば、X年目だからそろそろリーダーを経験しよう、みたいな空気の強制。

もちろん、組織として役割を委譲したいという本音も分かるし、立場を変えることで分かってくることも沢山あるし、その経験をもって改めてメンバーの立場になることで よりよいチームにもなるという経験則はあります。
例えば後輩育成(メンター)とか 結構大事で、今まで他の人がやってくれていたことに対して不満ばかり言っていた人が「あれ?あの上司は意外とやってくれていたな」とか「なんで わかんないの!→あぁ、以前の自分もそうだったのか・・・」とか経験を通じて学べることも多々あります。

POになりたい人もいれば、PMになりたい人もいるし、アーキテクトになりたい人もいれば、開発プロセスをやりたい人もいると思います。
いずれにしても 他者との関わりは増えるし、立場の違いによって シンドイけど学べることを経験しておいて 損は無いでしょう。

ただ、組織として経験をしてもらうことで肉厚なチームになるということと、社歴による役割設定 は必ずしもイコールではないように思います。
研究者タイプにゼネラリストもどきのことを長期にわたって強要すると、研究者タイプは 研究出来うるところにステージを変える可能性があるかもしれません。
むしろ 組織(この場合は チームやプロダクトよりも大きい意味)において、並行して考えておくのは 多様なキャリアパスを実現できる土壌づくりかもしれません。

経験を積ませて視野を広げてもらうという趣旨でのリーダー配置である場合、例えば スクラム風に 期限を設けて その期限の中で 精一杯 失敗を含めて試行錯誤してもらって、期限になったら続けたいか 他に取り組みたいことがあるか 改めて話す場を設ける ということを先に宣言してしまうというのもアリかもしれません。
目に見えない空気の強制よりは、マシかもしれません。

この組織なら 搾取されていない(もしくは この程度の搾取なら まぁ大丈夫)という実感を得られるのであれば、結果として 組織から離れなくても良いかな?となるかもしれない という勝手な想像です。

なお、待遇、報酬、裁量 については 別の話としておきます。
逆に 待遇、報酬、裁量 を求めるけど、他の人への影響力の実績を積むつもりが全く無いとなると、あとは組織(企業)としての在り方なので まさに一期一会になるのではないでしょうか。*4

プロダクトの成長を自分の成長にしてしまう

残った(?)側のメンタリティーとしては プロダクトの成長を、自分たちの成長として 協力してもらった人達は、それぞれの別のステージに行ったんだ という一種の割り切りかもしれません。
あとは、もし何かあれば戻ってきて経験をフィードバックしてもらえる土壌を並行して作っておくよ、という感じでしょうか?

組織に所属しても出来る外部発信

今回の勉強会のように、外部に対して情報発信をすることで 自分の組織の良いところ 改善するためのヒント みたいなものを聞く機会を増やすというのも良いかもしれません。
ひっそりと勉強会にいって、その熱に当てられて スッとフェードアウトされるよりは マシな気がします。
これは 昨今言われている副業禁止を止めて 別の業務経験をする機会を与える、というのも一つの手段だと思います。
もちろん、副業先の方に 引き抜かれる可能性は十分ありますが、フェードアウトされるよりは マシだと思います。
BIGLOBEさんの場合、社内システムなので SESや受託開発と違って 軸足となる側の会社だと思うので、やりようはある気がします。*5

開発経験をコンサル(外販)してみる

社内システムに閉じた経験しかできないという不満・不安に対して、DDD&Javaによる基幹システムの開発経験を 他の企業へ外販することで 開発対象の多様化を図るというのも あるかもしれません。
外販するのは経験です。実際の開発者が傭兵よろしく 他企業に乗り込む感じです。
これまでの アジャイルで得た知見や、エンジニア育成経験とその継続的な取り組みノウハウ、実績のあるDDDを支える開発基盤の分は 少なくとも 外販先よりも10年近いのアドバンテージがあり、それを 余すことなく提供してもらえるということであれば、悪い話では無いと思います。
例えば、BIGLOBEさんが 増田さんを およびしたように、呼ばれてはいないけど 売り込みに行くという感じです。
言うまでも無く 失敗すると思いますが、Java未経験でも2回目で それなりの成果を出せた背景を含めて 先方にも「おそらく短期的には失敗はする。けど、経験とチームが残って それが次の成長につながるというのが大事なところ。能動的に失敗をしましょう、でも、我々には その失敗を乗り越えた経験もノウハウもあります。大事なのは失敗した後で、それを乗り切るための併走者が我々です。」という感じです。

実際、アジャイルやDDDの導入において、何かしら社内政治は必要になるわけですが、その社内政治を「売り込まれたものを試してみよう」という言い訳にしてもらって現場に導入するという感じです*6

なお、外販で得た失敗経験は、内部にフィードバックをして、「立ち上げ時を知らないメンバーに疑似経験をしてもらう素材」として活用することで、立ち上げメンバーしか知らない暗黙知となった経験を 新たに作ることが出来ることも期待できます。

ちなみに

私は 待遇、報酬、裁量だけでなく、そもそも評価にも興味が無かったので 上長の方々にとっては非常にやりにくい部下だったと思います。*7

私の場合、石の上にも10年的*8な発想で 逆に時期が来たら 始めから会社は辞めると決めていたので、余計に 扱いにくいところがあったと思います。

なお、私の場合「リーダーをやりなさい」「火消に行きなさい」と言われたら、基本的に二つ返事で応えていました。断ったのは 昇級をエサ(?)にして組織としての役割を求められたケースだけです*9

とまぁ、そんな変な考えの人の思い付きなので、的を射ていない と思いますが せっかくなので のこしておくことにしました。

謝辞

会場を貸していただき、また貴重なノウハウを開示していただいたBIGLOBEさんには感謝感謝です。
エンジニアを募集されているとのことですので、興味があるかたは

crecruit@ml.biglobe.co.jp

まで、とのこと。
SESじゃなくて、自社プロダクト開発に関わりたい&組織としてDDDに取り組んでいるところ を考えている人は、話をするだけでもされてはいかがでしょうか?

さいごに

勉強会そのものから、随分と外れたことばかり書いてしまいました。
その辺りも含めて刺激を受けた良い勉強会だったと思います。

追記

Serviceについて、自分なりの整理をしてみた。

vermeer.hatenablog.jp


*1:無駄にドキドキした。見知らぬ人と話すのは やっぱり気が張る

*2:とはいえ、今後 読むかというと・・・

*3:http://vermeer.hatenablog.jp/entry/2018/06/19/133913

*4:転職時に 見せかけで売り込むことは出来るかもしれませんが、結局 渡り鳥になるだけのように思います。それが目的であれば それは それで良いとは思いますが

*5:SESや受託開発メインの場合は、傭兵的なビジネスモデルなので 軸足側になるには 自社プロダクトを持つなどしないと難しいというのが個人的な理解です

*6:そもそも外販の場合、買い手は、相手企業の社長だから必然的にトップダウンです。

*7:個人的には概ね上司の方々には恵まれていたと思っていますが、逆の立場で考えたら まぁまぁ扱い辛かったと思います

*8:3年だと仕事を覚えることは出来ても、サラリーマンの辛さ的なところを実感するまでには至れないと思っていたので、もう辞めたい!と思っても 10年は頑張ろう という感じの期限設定でした。実際 8年目くらいの時と、実際に辞めることを決める直前、それぞれ種類は違いますが「もう限界かも」という局面を経験できるくらいまでは そこそこ走ったつもりです

*9:最終的に、自分が昇級しないと 上司や後輩の昇級に悪影響があるかも、という別の理由で ある一定のところまでは昇級しましたが、本質的には本意ではなかったという感じです

【雑記】銀行の相談窓口

という、ちきりんさんのツイートを見ていて思ったこと。

結論

常設の相談窓口は店舗から無くしてしまって良いと思う*1

どうやって

ざっくりいうと、ネットを活用する。

それぞれ、または組み合わせて

  • ビデオもしくはテキストチャットで相談
  • 予約制を設けて、お互いに会議ルームにログイン
  • 融資審査データの入力による簡易診断
  • 支店にビデオ会議ブースを設けて、相談担当が複数支店を受け持つ
    (対面相談を望んだり直ぐに書類提出ができる支店利用希望者に応える)

なぜ

相談したい利用者の時間に合わせられる

融資を求める利用者は、働き盛りの中年という仮定をした場合、日中の銀行営業時間に合わせるのは難しい。
かといって、銀行も常設で ずっと張り付くのは辛い。
ということで、ネットで予約をして相談時間を確定する、という感じ。

相談時間も30分とか決めて超過した場合は、改めて予約を取り直す。
利用規約として謳っておいて、後は担当者が切断するというよりも システムがタイムキーパーをして ある程度 機械的に ただし 担当者負担が少ないようにするという感じ。

相談窓口の効率が図れる

飛び込み用に複数支店の掛け持ちもできるし、上述の予約を前提として 夕方以降のシフト勤務も出来る。
少なくとも 相談が無い場合は、他の業務をかけもつ(実情は分からないけど、銀行員の雑務量は かなりあるという認識)。

ただし、掛け持ちは仕事をし過ぎる可能性があるので、緊急性のないものなどマネージメントは重要。

ノウハウが蓄積できる

個々の支店、個々の担当者に蓄積されてしまうノウハウが システムを介することでデータ化できる。
コールセンター型であっても、地域性などを考慮して相談に乗ることも出来る。

AI相談で負担も軽減

基本的にネットで相談をするということは、PCかスマートフォンを使った相談であるという前提が成立している。
ということは事前に色々な情報を入力しておいてもらうことが可能。
上述のピークシフトというだけでなく、入力した情報をもって 融資可否の基本判定を行えば、絶対ダメと確実にOKの いずれでもないパターンについてのみ 疑似対面相談をすれば良くなるだろう。

即応性は心地よい

個人的に、以前 とある企業において(銀行ではない)、チャットで質問をできるというのを試してみて、その即応性が非常に心地良かった。
これまでだったら、電話で待たされるところが チャットだと すぐにつながるし、必要な情報のリンクを示してくれるので その場で細かく聞かずとも「いったん、読んでみます。分からなかったら また相談します」ということが簡単にできる。

システム構築は大変?

その他として後述しているのとは別のアプローチは、ProBankプロジェクトのように 地銀基幹系ではなく、地銀情報系としてF社が自社のクラウドを活用してサービスとして立ち上げる(ように、地銀が 持ち掛ける)。

収益

人件費が浮く

掛け持ちできれば、人件費は浮く。
相談時間を集約すれば、担当者の業務稼働率を高く保てる。
また、ぽろぽろと仕事が追加されることも無く 残業が減る。

全てのやり取りをネットのみというよりも、ネットで出来る人は そちらにシフトしてもらうだけでも 随分と負担は分散する。
例えば、ネット予約者を優先するという形でシフトさせるということもあるだろう。

こじれそうな利用者の場合は、ビデオチャットではなく 店舗に来ていただく という風に絞り込むだけでも負担は軽減するし、そういう特殊な利用者には 相応の経験者が始めから応対した方が無難。

震災時にも対応できる

震災時に収益を上げるのか?というところがあるかもしれないけれど、企業だと 事業継続の調整が長引くのは非常に厳しい。
例えば、ネットによる相談基盤が構築されていれば、例えば「相談窓口車」があったり、単純にネットにつながるだけで すぐに返済計画の見直しについて 相談できる。
相談できることによる心理安全は被災時に希望にもなるし、債権者としても 債務者が生存していて 返済可否状況を 早く確認できることは とても大事。

おまけ(?)的なところとしては、人は困った時に助けてくれた人を大事にする。
地銀は 地元企業や個人から 贔屓にしてもらって なんぼ の世界。
対応してくれた銀行への恩義は、多くの場合、その先の長い付き合いにも繋がるだろう。

利用者が増えるかも?

融資相談の敷居が低くなれば、借り手も増えるのでは?という安易な発想。
また返済が厳しいということで、勝手に自己破産されるよりも返済期限を調整して確実に利益を取り続ける仕組みとして相談の敷居が低くて悪いことは無いだろう。

マッチングが出来るかも?

ローン融資を前提としているが企業融資も視野に入れるのであれば、シェアオフィスによる人材交流のようなことをネット上で出来るかもしれない。
銀行側に蓄積された情報を元に顧客と顧客をマッチングする感じ。
相互の都合の良い時間だけを調整すれば良いですし、新しい事業において「カネ」を握る銀行が初めから話に参加しているのは悪い話ではない。
その銀行と取引のある企業同士という縛りはあるけれど、銀行から見て融資相手として認められているという相手同士であるということは それだけでも安心感につながる(利用企業にとって信用調査コストが浮く)。

その他

別に銀行がシステムを構築しなくても良いと思う。
マッチングサービス的な役割として、どこかのシステム開発会社が構築して 利用者の最寄の銀行とマッチングをするプラットフォームを作るイメージ。

「保険の窓口」のような商品説明は一切せず、あくまでマッチングとセキュリティに注力すれば 銀行法の範疇外だと思うので 参入障壁も低いんじゃないかな?

システム会社側の収益については手数料モデルもあるし、広告モデルもあるし、データ活用もある(個人情報に抵触しないレベルで)。
例えば、人材派遣や転職サービスを生業にしている(〇ズリーチ)みたいな、既存で個人情報を管理している会社がシステムを作ったら、銀行からすれば一番神経質になるセキュリティと個人情報の扱いについてノウハウ(先見性)があるということで喜んで使うんじゃないかな?
転職希望者にとっても、転職=収入だし、ひいてはローン返済も気になるところだから、まとめて相談できる(マッチングしてくれる)というのはありがたい話。
銀行側からしたら、出来ないとは思うけど(個人情報のことがあるから)その人の融資審査に 返済可能な人材であるか?みたいな信用調査も期待できるかも。

さいごに

とはいえ、もしローン融資の規模が大きくなったとしても、結局はゼロサムゲームなので銀行自体の数も減るし、銀行員の数についても 減っていくと、個人的には思います。
あくまで、上述の考えは 人が減ったとしてもサービスレベルを維持もしくは上げられますよ、というだけの話なので。
あとは早い者勝ちの世界。

それと、実際の銀行業務を分かって言っているわけではありません。*2

追記

上述のツイートの返信の中に、以下のような返信が。
システム化しているか どうか わかりませんが、サービスとしては すでにやっているみたいです。

私が世間知らずだったようです。


*1:厳密には窓口機能は残して、対人常設は無くす

*2:金融システムの中の人だったことはあるけれど、銀行の中の人だったことは無いです