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

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

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のコンテナが立ち上がらなくなる