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

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

ユースケースじゃなくてサービスとしよう

何となく使っている ユースケースという言葉ですが、自分の中でも あやふやな使い方をしているし、実際 理解もあやふや。

ある種の あやふやさ というか 曖昧さ は残ることは分かっているけれど、ユースケースという表現をつかったシステム実装を採用したいと思っている以上、全く指針の無いという訳にはいかないと思って 改めて考えてみたら、段々と「あれ?」と思うようになって 今回の結論に至ったという感じです。

以前、扱った際は*1、以下のような感じ。

  • Domainに属する情報を扱う
  • トランザクション境界
  • ユースケース記述に近しい実装
  • Actorを必ず記述する(Annotationで表現)
  • ServiceおよびDomain(Repository)により業務フローを表現する
  • コンテキストや主たる関心事の単位でフォルダ分割をして見通しを良くしても良い。
  • 単位は動詞
  • ScopeはRequestScoped

と考えていました。*2

結論

システムの実装おける表現として、ユースケースは止めて、サービスにしよう。

考察/経緯

考えに至るまでの考察や経緯というかメモというか、そういうのをツラツラと。

総じて言えるのは、異なるものを同じように扱うと間違った認識につながるので使うのは止めよう、ということです。
特に自分自身が曖昧さを感じているものに対して、それっぽい理解で使ってしまうと、自分自身をミスリードしてしまうためです。

処理の集合ではない

イベントフローを処理とするならば、実装として ユースケースは処理の集合もしくはスクリプトになるだろう、というのが当初の発想でした。

でも、そもそも イベントフローは処理ではないし、シナリオはスクリプトではないので 理解が間違っています。

トランザクション境界ではない

1つのユースケースが終了すれば目的が達成され、かつ中断が行われないことを目安とする*3、とありますが、 ここでいう目的はアクターから見てシステムが達成してくれる機能を意図しており、トランザクションを意図したものではありません。

なぜ そもそも そう思ったのか、というと、中断しないこと というのを 同一トランザクションで扱うことは同値であると考えたからです。
今 思えば、SAStrutsにおいて、一連の操作(入力→確認→完了)をもってユースケースとするという整理があったりと トランザクションとは別物だと思えるヒント(経験)はあったのですが なぜだろう・・・

クリーンアーキテクチャでは使っているよ?

人は人、自分は自分。
というだけでなくて、Usecase*4という表現だと、ユースケース記述 を実装で表現しようと 自分がしてしまうから、という心理的な側面が大きいので避けよう、と。
また、そもそもクリーンアーキテクチャでいうところのUsecaseについて 自分は きちんと理解をしていないので、実装におけるUsecaseと 要件定義におけるユースケースを明確に別物として 扱えるようになっていたら別かもしれませんが、少なくとも現状では そうではないので使うのは止めておこうという感じです。

参考

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

ユースケース記述の作成 – ABC Blog

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

第8回 顧客の要求をユースケースに反映させる - ITmedia エンタープライズ

UseCaseの再利用性 - yoskhdia’s diary

上流工程における UML の活用

若手エンジニア必読!超絶分かるユースケース図-全知識と書き方5ステップ

業務フロー V.S. ユースケース記述(ユースケースシナリオ) - Ken's Blog

システムユースケースの概要

ビジネスでも使おう!ユースケース図のキホンを解説 - Cacoo JA ブログ

持続可能な開発を目指す ~ ドメイン・ユースケース駆動(クリーンアーキテクチャ) + 単方向に制限した処理 + FRP

Use case - Wikipedia

https://www.ibm.com/developerworks/rational/library/content/legacy/parttwo/1000/0670/0670_Schneider_Ch07.pdf

Use Case Examples -- Effective Samples and Tips

さいごに

ユースケースを使うのを止めて、サービスにするからといって、ユースケース的な発想を全て無くすというわけではありません。
あくまで分類するのではなくて、サービスと統一するだけです。
もし、サービスを集約したものについての分類が必要であれば、改めて考えますが、少なくとも その時にユースケースという表現は使わないようにするための戒めのメモです。


*1:http://vermeer.hatenablog.jp/entry/2018/03/18/172024

*2:本記事をもって、元の記事からUsecaseは削除しました

*3:UMLモデリング認定試験問題集より

*4:意図的に英語表現にしています

画面項目の順序にあわせたメッセージを出力する

vermeer.hatenablog.jp

の続き。
メッセージを任意の順番で固定で出力させました。

具体的にはメッセージを画面項目にあわせた出力をしています。

出力結果

FormValidation

Form側で行っているValidationは、入力必須検証 だけです。
それ以外の桁や型については後述のValueObjectにて検証をしています。

f:id:vermeer-1977:20180917102656p:plain

ValueObjectValidation

入力された内容を検証したメッセージです。

f:id:vermeer-1977:20180917102705p:plain

実装

順序の指定

/**
 * ClassのFieldの順序を指定します.
 * <p>
 * Javaの仕様としてFieldの順序は保証されないため、ClassのFiled順序を指定するためのAnnotationです.
 *
 * @author Yamashita,Takahiro
 */
@Target({FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldOrder {

    short value() default Short.MAX_VALUE;
}

順序指定に使うAnnotation はこんな感じで

    @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;

    }

FieldOrderで項目順序を指定します。

検証実施

検証実施部分は変更ありません。

    public String confirm() {
        validator.validate(registrationPage.getValidationForm());
        return "persistconfirm.xhtml";
    }

並べ替え

import ddd.domain.javabean.annotation.FieldOrder;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;

/**
 * ConstraintViolationを{@link ddd.domain.javabean.annotation.FieldOrder} で指定した順にソートする機能を提供します.
 *
 * @author Yamashita,Takahiro
 */
public class ConstraintViolationSorting {

    Set<ConstraintViolation<?>> beforeItems;

    private ConstraintViolationSorting(Set<ConstraintViolation<?>> constraintViolations) {
        this.beforeItems = constraintViolations;
    }

    /**
     * インスタンスを構築します.
     *
     * @param constraintViolations BeanValidationの検証結果
     * @return 構築したインスタンス
     */
    public static ConstraintViolationSorting of(Set<ConstraintViolation<?>> constraintViolations) {
        return new ConstraintViolationSorting(constraintViolations);
    }

    /**
     * ConstraintViolationを{@link ddd.domain.javabean.annotation.FieldOrder} で指定した順にソートしたリストを返却します.
     *
     * @return ソート済みのリスト
     * @throws ConstraintViolationSortingException 順序取得の際にフィールドの情報を取得できなかった場合
     */
    public List<ConstraintViolation<?>> toList() {
        return beforeItems.stream()
                .map(this::makeSortKey)
                .sorted(Comparator.comparing(ConstraintViolationWithSortKey::getKey))
                .map(ConstraintViolationWithSortKey::getConstraintViolation)
                .collect(Collectors.toList());
    }

    //
    ConstraintViolationWithSortKey makeSortKey(ConstraintViolation<?> constraintViolation) {
        Class<?> clazz = constraintViolation.getRootBeanClass();
        List<String> paths = Arrays.asList(constraintViolation.getPropertyPath().toString().split("\\."));
        String key = this.recursiveAppendKey(clazz, paths, 0, clazz.getCanonicalName());
        return new ConstraintViolationWithSortKey(key, constraintViolation);
    }

    //
    String recursiveAppendKey(Class<?> clazz, List<String> paths, Integer index, String appendedKey) {
        if (paths.size() - 1 <= index) {
            return appendedKey;
        }

        String field = paths.get(index);
        String fieldOrder = fieldOrder(clazz, field);
        String key = appendedKey + fieldOrder + field;

        try {
            Class<?> nextClass = clazz.getDeclaredField(field).getType();
            return this.recursiveAppendKey(nextClass, paths, index + 1, key);
        } catch (NoSuchFieldException | SecurityException ex) {
            throw new ConstraintViolationSortingException("Target field or filedtype can not get.", ex);
        }
    }

    //
    String fieldOrder(Class<?> clazz, String property) {
        short index = Short.MAX_VALUE;

        try {
            Field field = clazz.getDeclaredField(property);
            FieldOrder fieldOrder = field.getAnnotation(FieldOrder.class);
            if (fieldOrder != null) {
                index = fieldOrder.value();
            }
        } catch (NoSuchFieldException | SecurityException ex) {
            throw new ConstraintViolationSortingException("Target field can not get.", ex);
        }

        return String.format("%03d", index);
    }

    //
    static class ConstraintViolationWithSortKey {

        private final String key;
        private final ConstraintViolation<?> constraintViolation;

        public ConstraintViolationWithSortKey(String key, ConstraintViolation<?> constraintViolation) {
            this.key = key;
            this.constraintViolation = constraintViolation;
        }

        public String getKey() {
            return key;
        }

        public ConstraintViolation<?> getConstraintViolation() {
            return constraintViolation;
        }

    }

}

今回のメインとなる実装です。

ソートキーを文字列で作りこんでソートをしています。

検証不正の起因となったプロパティパスを対象となるようにしているので、FormクラスのDomainであるValueObjectでの不正も出力対象にしています。

メッセージ出力

/**
 * メッセージ出力する機能を提供します.
 *
 * @author Yamashita,Takahiro
 */
public class JsfMessageHandler implements MessageHandler {

    /**
     * {@inheritDoc }
     */
    @Override
    public void appendMessage(Set<ConstraintViolation<?>> validatedResults) {

        FacesContext facesContext = FacesContext.getCurrentInstance();

        MessageInterpolatorFactory interpolatorFactory
                                   = MessageInterpolatorFactory.of("Messages", "FormMessages", "FormLabels");

        MessageInterpolator interpolator = interpolatorFactory.create();

        List<ConstraintViolation<?>> sortedValidatedResults = ConstraintViolationSorting.of(validatedResults).toList();

        sortedValidatedResults.stream()
                .map(result -> {
                    return interpolator.toMessage(result);
                })
                .distinct()
                .forEachOrdered(message -> {

                    FacesMessage facemsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
                    facesContext.addMessage(null, facemsg);
                });

        // リダイレクトしてもFacesMessageが消えないように設定
        facesContext.getExternalContext().getFlash().setKeepMessages(true);

    }

}

InterceptorでValidation結果からメッセージに変換しているところを 対応前は順序の無い Setから 指定順に並べ替えたListからメッセージを出力するように変更します。

メモ(振り返り)

今回の対応をするにあたって、順序を示すFieldOrderを注釈する以外の影響は exsample.jsf.*パッケージ配下のクラスにはありません。 意識したのは 主だった機能への影響が「メッセージの順序を示すこと」以外にならないようにしたところです。
まぁ、どうでも良い話といえば、そうなのですが、こういうところに一々こだわっていきたいなぁと思っている次第です。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

コミット
vermeer_etc / jsf-ddd / commit / f5edeb0a9361 — Bitbucket

参考情報

Java リフレクションで取得するフィールドの順番 | memorandum

アノテーションの値順でフィールドを出力 | 「Javaを復習する初心者」が復習・学習するブログ

さいごに

あくまで本サンプルで確認できるレベルのところしか確認をしていませんが*1、最低限の要件は満たせたかな という感じです。

ただ今回の実装方法では、厳密に画面の項目順通りかというと、ちょっと違うと思っています。
画面入出力項目で使用しているフィールドではなく、あくまで検証に使用しているリクエストのプロパティで順序を指定しているためです。
正直なところ、実際に実装を進めていく中で、これまでの整理と 実現方法可否の塩梅から、いくつか見直しをすべきかもしれない、というところも少し感じています。


*1:テーブル構造の場合の出力など、複数のFormをまとめたコンポーネントで構成した階層状態の場合の確認していません

画面項目とメッセージ出力順序

vermeer.hatenablog.jp

の続き。

メッセージ一覧の出力順序について もう少し具体的にイメージを整理して どのパターンのまで 実装のルール決めで出来そうか考えてみたいと思います。

項目毎にメッセージ出力する というパターンは今回は扱いません。

画面イメージのルールとして、順序は数字・アルファベットの順番 とします。

FromXXは、Formクラスの型とします。
ラベル(項目名)を、Formクラスで指定しても良い、してはいけない というのを判断するための材料にします。

メッセージ編集は

vermeer.hatenablog.jp

を前提としています。

ざっくりの仕様

  • Formのラベルをプロパティを管理
    Formのクラスパス+[.label]をキー、値にFormのラベルを保持しておいて、変換時に使用する。
    ラベルを一括で管理できるので、表記の統一がしやすい。
    国際化対応時に対応漏れが起きにくい
  • ラベルアノテーションで直接指定
    Formのラベルを使用するクラス側で指定する。
    クラスパスで指定したラベルがあった場合は、直接指定側が優先される。
    独自のラベルを設けたいときに使用する。
    指定したラベルがプロパティのキーと一致すれば、キーと一致したプロパティの値を使用する。 (なければ指定した文字列が使用される)
    これにより国際化対応(ラベルのルール化)ができる。

これに加えて、順序指定についても考えていきたいと思います。

フラット

AAはXXです。
BBはXXです。
CCはXXです。
DDはXXです。

順序はフィールドに対して指定すれば良く、ラベルについても画面内で重複する表現もないので、Formのラベルをそのまま使って特に問題無さそうです。

複数コンポーネント

同じ組み合わせのコンポーネントが複数あるパターンです。

階層を使って実現

メッセージは、以下のような感じになることを想定します。

コンポーネント1 AAはXXです。
コンポーネント1 BBはXXです。
コンポーネント1 CCはXXです。
コンポーネント1 DDはXXです。
コンポーネント2 AAはXXです。
コンポーネント2 BBはXXです。
コンポーネント2 CCはXXです。
コンポーネント2 DDはXXです。

順序

class Page {

  @Order(1)
  コンポーネント1 component1;

  @Order(2)
  コンポーネント2 component2;
}
class コンポーネント1 {

  @Order(1)
  FormAA formAA;

  @Order(2)
  FormBB formBB;

  @Order(3)
  FormCC FormCC;

  @Order(4)
  FormDD FormDD;

}

というように、各フィールドの順番を指定しておいて階層を踏まえて順序を評価すれば出来そうです。

ラベル

コンポーネント型クラスにラベルを付与して、配下のクラスにラベルがある場合はプリフィックスとしてForm型クラスのラベルと組み合わせて表現します。

コンポーネント1と、2で、構造が同じでも、それぞれに相当する型クラスを準備する必要がありますが 型が同じでもインスタンスとしては別物なので、これは こうするしかないかな?という感じです。

message.properties としては

validate.XX.message = {label}はXXです。

コードとしては、上位階層にラベルがあったら 順番に連結をする という仕組みになるでしょうか。

たとえば、階層を考慮した表記について

コンポーネント1の AAはXXです。

と表現したい場合は、コンポーネント1のラベルに

コンポーネントの

というように助詞まで含めるか、上述のように スペースをライブラリ側で挟むようにするというのを標準仕様としておけば良いかな、という感じです。

メリットは、ラベルを Formやコンポーネントの単位で考えれば良いというところ。
あとは単純結合をして{label} と置換をしてメッセージを完成させます。

ただ、これは日本語だったら良さそうに思えるのですが、英語など文法が異なると、、どうでしょうかねぇ。。
of とか GestUser's Name みたいに、単純な結合では ちょっと駄目な気がします。

あと、ラベルの仕様として上位階層のもので上書きする としているので、上書きではなく プリフィクスとして追記させる、というような変更も必要になるでしょう。

コンポーネントでFormラベルを指定

コンポーネントクラスでフィールド毎に独自のラベルを指定するパターン。

上述のパターンと同じことも 下のような項目名だけで表現することができます。

AAはXXです。
BBはXXです。
CCはXXです。
DDはXXです。
EEはXXです。
FFはXXです。
GGはXXです。
HHはXXです。

Formでデフォルトのラベルを設けていても、それを使えず いちいち 全てのフィールドに対してラベルを指定しないといけませんが 表現力としては、こちらのパターンの方が良さそうです。

ただし

コンポーネント1にエラーがあります。

というような、コンポーネント単位のラベルを用いることは出来ません。
コンポーネントクラス自身、または上位となるページクラスから ラベルを指定すると 配下のFormクラスのラベルが上書きされてしまうためです。
コンポーネントなどの「領域」に対するメッセージを付与したい場合は、そのために使用するフィールドを設けておいて、それに対して コンポーネント配下のFormでエラーをまとめる というような工夫が必要になりそうです。

フラットとコンポーネントのミックス(垂直)

順序

階層指定で問題なさそうです。
(記述例は省略)

ラベル

ラベルもコンポーネントで指定する、ということで問題無さそうです。
(記述例は省略)

フラットとコンポーネントのミックス(水平)

順序

階層指定で問題なさそうです。
(記述例は省略)

ラベル

ラベルもコンポーネントで指定する、ということで問題無さそうです。
(記述例は省略)

テーブル

便宜上 セルの色を変えていますが、今回はセル色の制御については扱いません。

TitleAはキー項目という前提です。

TitleAは入力必須です。
TitleA(A3)のTitleC は 10桁以内にしてください。
TitleA(A5)のTitleB に重複した値が指定されています。重複しない値を指定してください。
TitleA(A6)のTitleB に重複した値が指定されています。重複しない値を指定してください。

という結果になることを想定しています。

キー項目の入力必須については、特定のために 行番号など 当該項目に関連する情報を付与したいケースも考えられます。

例えば

2行目のTitleAは入力必須です。
4行目のTitleAは入力必須です。

のように。

明細を1つ1つ表示していますが、そもそもテーブルのような大量な情報量に対して1つ1つエラーメッセージを一覧形式で表記すること自体が良くないかもしれません。

例えば

XX表にエラーがあります。

とか

TitleAは入力必須です。
TitleC は 10桁以内にしてください。
TitleB に重複した値が指定されています。重複しない値を指定してください。

のような 列単位、エラー分類単位で集約した表記をして、 個々のエラー情報はセルに対してツールチップで表現する というのが妥当かもしれません。

今回は順序だけに絞って整理をしたいので、ツールチップについては保留にしておくとしても、集約については もう少し考えておきたいところです。

順序

詳細を出力するときは階層指定で問題なさそうですが、集約表記における優先度順についてはどうでしょう?

たとえば

TitleAは入力必須です。
TitleC は 10桁以内にしてください。
TitleB に重複した値が指定されています。重複しない値を指定してください。

が、桁チェックの結果を最後にしたい というのが要件であった場合
(タイトル順ではない)

TitleAは入力必須です。
TitleB に重複した値が指定されています。重複しない値を指定してください。
TitleC は 10桁以内にしてください。

のような表記順序になる、という感じです。

正直な意見としては、同一優先度内であれば順不同を仕様としたい、ところです。
ただ、これだと出力順が一致しないというのが なんとなく嫌です。*1

集約単位の出力メッセージについては

  • MessageTemplateの文字列ソートをデフォルトとする
  • 表示順序を制御したメッセージのプロパティーキーと優先度をControllerで指定
  • 未指定と指定が混在している場合は、指定しているものの優先度を高くする

という感じでしょうか。

では順序指定は どうやって指定するのが良いでしょう?

メッセージは実行時例外をInterceptorで捕捉して処理をするという前提なので、Annotationで宣言的に指定しておくと 情報として取得しやすいように思います。
ただし、Controller毎に発生するかもしれない例外を全て列挙しなくても良いような工夫は必要だと思います。
優先度とプロパティキーを列挙するだけであれば静的な情報なので、通常のAnnotationで扱えそうな気がしますが、継承的なことや 委譲的なことをして分類をしたものを使用したいとなった場合を考えると 動的な指定も考えておきたいところです。 *2

ラベル

グループ集約表記に関するラベルから考えてみたいと思います。

通常のラベルを そのまま使うと上書きされるというのが、現状の機能仕様です。
ということで、通常のラベルとは別に集約用のグループラベルを別途設けて対応をするのが良さそうに思います。

Formのアノテーション指定としては

class TableRow {

  @GroupLabel("TitleA")
  FormA formA;

  (省略)
}

メッセージ置換用のプロパティは

FormA.NotBlank.message.group={groupLabel}は入力必須です
FormA.NotBlank.message={targetItem}の{groupLabel}は入力必須です

メッセージの指定は

@NotBlank(groups = FormValidation.class, message="FormA.NotBlank.message")
private final String value;

メッセージは グループを示さないキー値にしておいて、メッセージ変換をする際に @GroupLabel があったら、「メッセージキー」+「.group」でpropertiesから 検索をしてヒットしたら それを使うという感じです。

ここまでは、なんとなくできそうな気がしますが、{targetItem} という変数は どうすれば実現できるのでしょうか?

これは、キー項目(ここだとTitleA)における 行番号を含めた情報を メッセージにどうやって反映すれば良いのだろう? にも関連します。

実現性の可否は分かりませんが、とりあえず思いつく方策を考えてみたいと思います。

Annotationで動的情報取得
class Page{

  FormA formA;

  @TargetItem(this.getTargetItems())
  FormB formB;


  private String getTargetItemId(){
    return this.formA.value();

}

こんな感じのイメージ。
実際は @TargetItem(this.getTargetItems())のような動的な指定はできないです。
でも、できたら結構広がりのある仕組みになると思うんですけどねぇ。
ただ、こうなってくると宣言的とは、なんなのか意味が良く分からないことにもなっているような気もします。

isValidで編集

全ての標準Validationについて、キー項目を統一的に渡せるようにTable用のものを新たに作り #isValid で編集をするという方法。

#isValidのパラメータConstraintValidatorContextを使うことで、messageの一部({targetItem})だけを事前に置換するというやり方です。

雰囲気先行で考えているので、実際に出来うる やり方なのか。。。

まとめ

  • フィールドの順番は、階層的に指定する。
  • ラベルはコンポーネントで指定する。

ということで テーブル表記および それに準ずる階層構造の場合は除いて、 概ねカバーできそうです。

テーブル表記のメッセージ出力の課題は

  • メッセージ優先度の指定方法を どうやるのか?
  • 表形式でキー項目の値をメッセージに出力できるようにするのか?
  • グループラベルの置換をどうするのか?

という感じでしょうか。
結局、何も解決に向かっていない気がします。

参考資料

SpringのBean Validationでエラーメッセージを動的に切り替える。

Java8でAnnotationを動的に差し替えたい時

アノテーションを動的に上書きする - 丸いタイヤを四角く作る日記

私のBeanValidationの使い方(Java EE Advent Calendar 2013) — 裏紙

さいごに

次は、今回分かった課題を1つ1つ解消すべく実装を考えるか
各項目とFormクラスのフィールドとの関連付けの やり方を考えるか、どっちにしようかな。

しかし、どんどん沼にはまっている気がする。。
BeanValidationを使うということ自体が制約(目的)となって複雑度が上がっている気がしています。
なんか、もっとシンプルなことなのかもなぁと思ったり、変に難しく考えてしまっているような気もしています。

とりあえず、まとまらないなら まとまらないなりに吐き出した という感じです。


*1:こういう細かいところを、統一性の無さ ということで嫌がるユーザーもいたり いなかったり

*2:現時点では、Annotationのパラメータを動的に編集するという実装知見は持ち合わせていないので 出来るか 出来ないか すら分かりませんが

メッセージ出力順序を整理

vermeer.hatenablog.jp

の続き

今更ですが 特に明示はしていませんでしたが メッセージとは エラーメッセージです。

順序はPresentation層で

表示要件はDomainの主たる関心事というよりも 各ページ毎の仕様です*1
メッセージの出力順序を制御するのであれば、Presentation層で制御すべきでしょう。

もっと言えば、それ以外の層で順序を制御すべきではなく、Presentation層だけで制御すべきだと考えます。

ありがちなところでは SQLの ORDER BYで 表示順を制御する ということをしていないでしょうか? *2
やりやすいかもしれませんが、表示順という仕様が別の層に漏れ出しています。
(この例だと、Infrastructure層に漏れている)

メッセージの優先度

優先度というのは以下のようなケースを想定しています。

名前は10文字以内です。  
回線が混雑しているため、しばらく待って操作してください。

こんなメッセージが出力をしてはいけません。

この場合は、「回線が混雑して……」とメッセージ出力したエラーページに遷移するか、操作画面上のメッセージとして「回線が混雑して……」だけを出力するべきです。
少なくとも、項目検証不明のメッセージを混ぜるのはNGです。

発生したエラーに関連するメッセージを単純に かき集めて出力するだけという訳にはいきません。

きちんと優先度を考慮した仕組みを設けるだけでなく、優先度を指定できる仕組み またはルールも あわせて準備しないといけないでしょう。

優先度の順位付けは、パッと考えると単純ですが 似て非なるものであったり 要件次第で変わったりもするように思うので、意外と面倒かもしれません。

表示項目と表示順序

画面の表示項目順と揃っていないと可読性の悪いメッセージに感じてしまいます。
したがって、まず基本的な表示順序に影響するのは 表示項目順(レイアウト)ということになります。

表示項目で集約した上で、詳細メッセージの粒度も併せたいところです。

全てのメッセージ詳細の粒度を一致させるのは 項目が2つくらいならできるかもしれませんが、3つ以上になると 妥協点を考えないといけません。

例えば、

名前は10文字以内です。
名前に絵文字は使えません。
住所は40文字以内です。
住所に絵文字は使えません。
住所に半角文字は使えません。  // 住所にだけあるメッセージ

こうだと問題無さそう。

名前は10文字以内です。
住所は40文字以内です。
名前に絵文字は使えません。
住所に絵文字は使えません。
住所に半角文字は使えません。  // 住所にだけあるメッセージ

これだと、読みにくい。

名前は10文字以内です。
名前に絵文字は使えません。
住所は40文字以内です。
住所に半角文字は使えません。  // 住所にだけあるメッセージ
住所に絵文字は使えません。

これは許容範囲としたい。

という感じです。

ぱっと見たら、表示項目を一番前したようなメッセージにしておいて、詳細は自然順ソートをすれば 良さそうも思えますが、実際の画面レイアウトによる表示順序が関係するので、そこまで単純ではありません。
また、そのようなメッセージ順序は言語が異なると不自然な場合があるかもしれません。*3

いずれにしても、表示項目順にあわせた並べ替えをすることは必要です。

なかなか面倒くさそうです。

表示項目に直接付与

表示項目に関するフィールドに直接メッセージを関連付けて表現するやり方です。

名前【    】 名前は入力必須です

のような感じ。

この場合、項目に対して複数メッセージを出力することは、あまり良いデザインではないように思います。
そこをあえて対応したとしたら

名前【    】名前は10文字以内です。
        名前に絵文字は使えません。
住所【    】住所は40文字以内です。
        住所に絵文字は使えません。
        住所に半角文字は使えません。  // 住所にだけあるメッセージ

もしくは、明示的に分かっているラベルは外して

名前【    】10文字以内です。
        絵文字は使えません。
住所【    】40文字以内です。
        絵文字は使えません。
        半角文字は使えません。  // 住所にだけあるメッセージ

という感じでしょうか。

横じゃなくて、下というパターンもあるでしょう。

名前【    】
10文字以内です。
絵文字は使えません。

住所【    】
40文字以内です。
絵文字は使えません。
半角文字は使えません。  // 住所にだけあるメッセージ

こんな感じ。

いずれにしても、表示項目によるグルーピング と メッセージ詳細の順番 について考慮は必要そうだということは確実です。

仕組み的なところをいうと、より具体的な位置までマッピングする必要がありそうです。

これは テーブルレイアウトにおける、セル毎のツールチップ表示も 同じ考え方で良いでしょう。

画面レイアウトの下部に、メッセージ一覧を表示するよりも、さらに面倒くさいですね。*4

画面の構造と表示順序

順序に関する情報は、Formクラスではなく ページ全体を示すPageクラス もしくは レスポンスクラスのプロパティに、検証不正を関連付けるべきでしょう。
Formクラスは あくまで画面を構成する部品の型だからです。

複数のコンポーネントで構成されている場合は最上位となるクラス(基本はPageクラス)に順序を示して ツリー構造で表示順序を構造化するイメージです。

あとは、過去の考察の通りです。

vermeer.hatenablog.jp

Presentation層以外で発生したエラーを、どうやって関連付けるのか、もしくは割り切っていくのか というところが難しそうです。

WebAPIだったら順序は不要?

WebAPI(RestAPI)のレスポンスに付与するエラー詳細にも順序は必要でしょうか?
SPAなどのクライアントレンダリングに使用する場合は必要で、そうではない場合は不要であると考えます。
ただ この切り分けをするとWebAPIの良さが軽減してしまいます。
そうなると 基本的にWebAPIでも順序を考慮すべきでしょう。

まとめ

メッセージ出力順序を 実現するにあたって必要な要件をまとめると

  • 優先度を考慮したハンドリング
  • 画面表示に応じた出力順の指定
  • 画面項目とエラーの関連付け
  • エラー起因のメッセージの順序付け

を満たす仕組みを考える、ということでしょうか。

結構 きついな……

過去の経験としては、

  • 要件自体がシンプルなので仕組みにあわせて実装をする
  • 仕組みを一切使わず 泥臭くControllerにガシガシとスクリプト的な実装をする

の2択がメインでした。

なお、オレオレライブラリで対応した際は、レイヤーを跨って強依存な情報を持たせるような仕組みを作ったのですが*5、 DDDとか学ぶ過程で得た発想と反省(?)から、今回は、もうちょっと ちゃんとしたものにしようと考え至りました。

多分、今回も何かしらの割り切りをすることになるでしょう。
できれば 宣言的な実装と スクリプト的な実装を ほどよくミックスできる柔軟な仕組みを考えたいとは思っていますが……

その他

実行時例外以外の表現

参考資料の中のScalaに関する資料をみて、Eitherとか 面白い考え方だな と思いました。

実行時例外という副作用について 仕組みを分かっていれば問題ありませんが、何も知らない 新しく参画したメンバーした際、そういった暗黙知ベースなところから フレームワークの本来の設計思想が綻びていく切欠になるというのは良くある話です。

従来のリターンコードと同じだと思って 始めは興味が無かったのですが、戻り値に対して統一的な型を持たせておくという設計(実装)思想は 似て非なるものだと思うようになりました。
チェック例外にも似ていますが、やっぱり違っていて 呼出し元への強制が弱いというか、統一した型を用いるというところだけというのが良さそうです。
Javaで Optionalを使うのと ほとんど同じ感覚で 例外に準ずるものを明示的に扱えるのは良いように思います。

より具体的には、@makingさんのYAVI*6 で Either を使ったライブラリの使用事例をみて理解が深まったという感じです。

とはいえ、まだ理解の乏しく、JavaでEitherを使った仕組みを作りこむよりも、例外を用いた実装の方がJavaの言語仕様として馴染まれている*7ということで、いったん「面白い考え方だ」というところで留めておきたいと思います。

参考資料

内部向けWeb APIでエラーレスポンスどう表現するか?

DDDの仕様パターン - pospomeのプログラミング日記

マイクロサービスのロギングベストプラクティス

WebAPIでバリデーションチェックエラーの際、HTTPステータスは何を返すのが適切か

www.slideshare.net

Goのエラー処理とpkg/errorsの使い方

speakerdeck.com

speakerdeck.com

エラーメッセージはフォームのどこに表示するべきか | UX MILK

さいごに

色々と整理したわけですが、当然 現時点で できているわけではありません。
これから、そこを目指そうというところを整理したということです。
さて、実際に実装していく中で どれくらい見直さないといけなくなるのか……
恐ろしくもあり、楽しみでもあり。*8


*1:Domainの主たる関心事の一部ではありますが、全部ではない

*2:かくいう、私は SQLでソートしていたんですが……

*3:まぁ、英語でも 主語が一番始めにくるので、そうそう無さそうな気もしていますが

*4:でも、ありがちな要件です

*5:仕様・設計・実装 全てが自分だけというシステム開発だったので特に問題はなかった

*6:https://github.com/making/yavi

*7:私自身が馴染んでいる

*8:恐ろしさの方が強め

絵文字のメモ

絵文字関連のメモ

これ、まともにやろうとしたらかなりシンドイぞ。。

リンク

Unicode 絵文字にまつわるあれこれ (絵文字の標準とプログラム上でのハンドリング)

UTF-8時代の環境依存文字チェック ~そこに文字はあるか~

Javaでお手軽に絵文字を使うライブラリ emoji-java の紹介

絵文字を入れたら文字数カウントが狂った時に見るページ

絵文字を支える技術の紹介

【考察】Validationと結果出力

vermeer.hatenablog.jp

の続き的なところ。

結論としては、メッセージ出力順についての検討を次にやろうかな、に至るまでの経緯というか そのあたりの考察を まとめたものです。

メッセージ表示順序は検証優先度とは別物である

まず、検証順序ではなく、あえて優先度としています。
理由は、BeanValidationによる検証要件の順序が、スクリプトなどで実現するような順序というよりも、Groupによるグルーピングであるので、優先度 という表現の方が 個人的に しっくりくるからです。

同一優先度内における順序は保証されません。検証順序性を厳密に行いたいケースの扱いについては後述しますが、従来の設計思想のままでは ちょっと馴染みの悪い感覚になるのは間違いないと思います。

次に 表示順序についてですが、以前 検証結果のメッセージ変換を扱ったのと同様の理由で表示順序についても 検証する順序とは別の論理で実現するものであると考えます。

そもそもですが、例えば、画面の表示項目順に

名前は必須入力です。
生年月日は必須入力です。

のように出力したくても、BeanValidationの結果をそのまま使用すると 結果の出力順は検証都度変わります。
そもそも検証結果の戻り値の型が Set なので 順序を保証していません。

ところで、表示順と検証優先度は一致させるべきものでしょうか?
BeanValidationの設計思想が良くないということでしょうか?

おそらく NOです。

メッセージの遅延変換のところでも考察したところですが、主たる関心事が違うので 検証結果を表示要件と一致させる前提が 正しくないと考えた方が良いと思います。

なお、表示順序を考慮するために行った 私の経験としては、基本的に類似しているというところもあって スクリプト的な検証ロジックを用いて対応しました。
今 改めて考えてみると、本来は別物であるので 表示順の要件を検証優先度とするのは避けるべきであったかもしれませんが、現場の泥臭い世界は、大体こんなものなのかもしれません。

メッセージ表示順については、本質的には検証優先度とは別物であるということを念頭に 改めて別の機会で整理および実現方法の検討をしてみたいなぁと思っています。 (少なくとも、単純ソートで 表示メッセージが常に同じ順番になっています、というだけでは無いので)

検証要件と検証実施は別物である

例えば、Presentation層の 特に Controllerで 長々と検証要件を列挙せず、あくまでサービスの実行前に、#validate メソッドにより、検証実施をするだけにして、検証要件自体は DomainObjectで表現しましょう、ということです。

加えて、検証要件は 宣言的もしくは 振る舞いの事前処理として強制しないことで、ロジックの主たる関心事に着目しやすい実装になると思います。

検証要件を振る舞いの事前処理として強制する、というのは、防御的プログラミングを指しています。つまり、防御的プログラミングをしない方が良い、ということです。防御的プログラミングの発想自体は NullPointer対策など 大変重要な実装方法だと思いますが、Nullチェック済みのValueObjectに対して、毎回毎回 防御的にNullPointerのチェックをするのは 冗長です。

Presentation層で受付した際に、FormからDomainObject(ValueObject)に変換したところで保証する という設計指針を持っておけば十分であると思います。

以前、私自身の書評*1にて 契約による設計 について 色々と批判的なことを述べていたわけですが、書籍が言おうとしていたことが、もしここで言っている検証要件と検証実施は別物という趣旨であったとするならば、おそらく私自身が記した批判は 的外れです。防御的プログラミングの前提の多くは ユーザー型(ValueObject)を用いることで ほとんど解消されます。

ということを丁度書いていたところで、@masuda220 さんが…

思わず、即質問してしまいました。

スクリプト的に実装したいが…

ちょうど(?)@making さんがライブラリを公開されました。

細かいハンドリングを考えた場合、このライブラリのようにスクリプト的な実装をした方が Annotationによる宣言的な実装よりも 分かりやすいというのは同感です。
加えて、依存ライブラリが全くないというもの 利用側からすると非常にありがたいところです。

使ってみようかな?という思いもありつつ、いったん 保守的に BeanValidationを軸に考えていきたいと思います。

今のところのアイディアとしては

SpringのBean Validationで複数の入力項目にまたがってチェックする。

のように @AssertTrueによる実装をして、検証優先度はGroupで、メッセージはmessageで 実装する、しかないかな?というところです。

求めている スクリプト的な検証 は
「こうじゃなかったら、エラーとして、こうだったら継続検証。メッセージとしては途中で発生したものを全て出力する」
というような、検証実施とメッセージ出力の粒度を調整することを求めているわけですが、@AssertTrue 方式だと、少なくとも メッセージ出力粒度にあわせて検証要件を分割しなければならないと思われます*2

つまり、従来の手続き的な発想から、検証要件を宣言的に設計する、というように発想の転換を強いられることは間違いないと思います。 また、宣言的であるがゆえに検証優先度の高い検証要件のパーツ(メソッド)を何度も呼び出すような冗長的なロジックが発生するような気もしています。

いずれにしても、結構、この発想の転換は難しいと思っていて、やっぱり従来のスクリプト的なものを求めたい、となった場合は、BeanValidationに縛られない仕組みを準備する と割り切った方が良いかもしれません。下手に混ぜるとプロダクトの設計思想として とっちらかるので、何かしら整理は必要になると思います。

なお、私の過去の経験としては、必須/型桁チェック くらいのシンプルなものはAnnotationで表現しておいて、スクリプトな検証は 検証例外を発行するメソッドを設けて実施するようにしていました。検証も 必須/型桁チェック/スクリプト的なメソッド の優先度で行い、上位優先で検証不正があれば、以降の検証は行わないという感じです。
結果としてですが、なんだかんだと 順序性や出力メッセージに対して要望を求められていくことになり、Annotationで行っている検証ロジックと ほぼ類似のUtilを作ることになった上で、Annotationによる検証が、全てとは言いませんが それなりに駆逐されることになりました。
この経緯については、私自身の知見の少なさとプロジェクトの開発期間の短さなどもあり、ライブラリに精通するよりも さっさとUtilを作って 実装した方が早いという判断をしたわけですが*3、DDD的な設計思想に触れる中で、ちょっと考え方を変えていった方が良いかもしれないと 思うようになったというのが現在であり 、本考察をしようかな と思うに至った経緯という感じです。

ということで、私自身としては、一度 発想の転換をするべく努力をしてみて、後のことは それを踏まえて考えることにしました。

検証要件は事前/不変 条件でありドメイン知識

事前条件 または 不変条件 は ドメインにおける重要な関心事であると考えます。したがって、機能要件として DomainObject(ValueObjectやEntity)に対して、直接 または 直感的に分かりやすい形で 関連付けておくべきものであると思います。
例えば プロパティに対して Annotationにより宣言的に実装するという やり方もありますし、条件ロジックを呼び出すメソッドを共通的に扱うようなInterfaceを設けるという やり方が考えられます。
緩い方式だと、同一パッケージ内でパッケージプライベートクラスで作成して 名称で直感的に分かるようにしておいて DomainObjectから参照する というやり方でも良いかもしれません。

また、そのままでは実現できませんが、個人的には 事後条件についても 宣言的な検証をする仕組みを設けたい気がしています。
ざっくりなイメージだと、@ValidPostCondition みたいなアノテーションがついていたら、配下の要素は #validatePostConditionの時だけ採用するみたいな感じです。

イメージは @kawasima さんの

の事前条件、事後条件のロジックの所在は DomainObjectまたはServiceに属する形で実装をして ドメイン知識として集めておきたい という感じです。

メッセージ表示順序はドメイン知識?

表示順序は 機能要件という意味ではドメイン知識ではあると思いますが、DomainObjectで扱うものかというと ちょっと違うかなというように思っています。

画面の表示項目順が変わる都度 DomainObjectを変更するか?というと 多分 それは筋が悪いと思います。
変更すべきは表示用のクラス(Form)や Controllerなどの Presentation層に依存するものに集めるべきだと思います。

さいごに

偶然にも、私が本記事に関することを考えているタイミングで 有効な情報が舞い降りてきて 有難い限りでした。 そのおかげで なんとなくイメージしていたものが 自分の中で まとまっていった気がします。

次は メッセージ表示順について 色々と考えていこうなぁと思っています。

上述でも別途 少し述べたように、簡単そうで 結構面倒くさいところが多いです。

単純ソートはダメというだけではなく、たとえば ControllerでFormに対して行った検証結果だけであれば良いのですが、レイヤーを跨った Application層(Service)で発生した例外かつ その内容が特定の表示項目に関連する情報だったら どうでしょう?
表示順だけではありませんが、表現力を上げようとすると Contoller(Presentation層)への負担は大きくなるのは仕方ないことかもしれません。そういった要件をできるだけ フレームワーク(ライブラリ)や設計思想をもって スッキリさせたいという 個人的な思いがあります。

どこまでのことが出来るか全くわかりませんし、その中で「こういう割り切りをしよう」となるとも思いますが、考えた上での割り切りであれば それを元にした設計方針となるので いずれにしても無駄にはならないはずです。


*1:http://vermeer.hatenablog.jp/entry/2017/08/16/124109

*2:途中で実行時例外を発行すると、おそらく他のvalidateが行われない

*3:そして、それは多分 正しい判断であったと今でも思います。実際 そうなるであろうことは 過去の宣言的実装の経験から ある程度 想定をしていました。

【雑記】コンテナというかマイクロサービスというか そんなところについて

vermeer.hatenablog.jp

あとブログはかけませんでしたが

bmxug.connpass.com

当たりの話を聞いて、なんとなく、思ったところがあって、もやもやしたままだったところを、現時点の感想レベルで 自分なりに残しておこうと思います。

結論

  • 現時点で 分散管理が必要な状況ではないので知見は増やすとしても、手を動かすことを含めて静観。

  • ビルド環境(E2Eテスト含む)のコンテナ利用は良さそうだから検討したい

理由

知見だけで静観

今は分散システムよりも、ちゃんとしたプロダクトを作るための基盤を自分なりに考えているので、そちらを優先しておきたい という 大変 個人的な理由です。

コンテナ利用は試したい

Javaのバージョンアップを考えると、気軽にテストを実行できる環境を作っておきたいから、です。
ちゃんとバージョンが代わっても動くのか?というのを きちんと確認するのなら、複数バージョンのJavaが導入されている環境ではなくて 独立した環境にしておきたいです。
そして、そういった環境相違をスクリプトで制御できるDockerをはじめとしたコンテナの利用は有効だと思っています。

思うところをツラツラ

デファクトとは?

とある記事で、k8s やIstio があっという間にデファクトスタンダードの地位を確立するから、今のうちに勉強をしておいた方がよいでしょう、というのを見て「確かに」と思う反面「デファクトになるから勉強する」というところには疑問も。

デファクトになるか ならないか を予見して勉強する分野を決めるのは、それっぽいけど 結構 博打だと 私が思っています。

学びたいから、学ぶ。今 面白そうだから 学ぶ。仕事で使うから 学ぶ。という感じのところを 拠り所にしないと「また外れた、、( ;∀;)」となりそうな気がするからです。

分散システムを構築したいという要求があって、そのときに自分の方式思想にフィットするものを選定する、もしくは 今後 分散システムを構築する際の知見を増やしておきたいので、今 知見が集まっているk8sやIstioを使ってみる というのは良い考えだと思います。

進むであろう標準化

より良い仕組みであればあるほど、もう しばらくすれば、統一インターフェース的なものが策定されると思います。
そうすると、標準実装としてk8sやIstioは扱われるかもしれませんが、引き続き デファクトであるか というと それは分からない という感じです。

数年したら k8sもIstioも、組み込まれた基盤 として存在することはあっても、それ自身の形のままで デファクトとして存在はしていない気がしています。

現時点においても、GCPを基盤として開発している人は、知見としては必要だけど、それはDevOps環境として認識しているだけという感じで、既に そういう立場にいる人たちだというのが 私の理解です。
もし明日、GCPの基盤が k8s以外になったとしても、DevOpsインターフェースが変わらずリリースが無事できるのであれば なんら問題は無いでしょう。
やりたいことはDevOpsであって、k8sやIstioを使うことではない、ということです。

どこを目指す?

立場と目的によって、それを学習するか、利用するか、気が付きもしないのか、みたいな感じで 分かれるところじゃないかな?と。

結局は、技術者としての立ち位置と優先度だとは思います。
作り上げられている過程に参加することで、多くの知見が集まり 学べるから ということで先行者になる(なりたい)という人もいれば、ツールの1つとして 安心して利用したいだけ という人もいます。
私は、先行者に対する羨望はありつつも、実体として(実力として)利用者のレベルな人です*1

良く分からないけどコマンド叩いたらアプリが動いたので一旦それで良しという人もいれば、内部の仕組み(思想)を分かってこそ技術者であるという人もいます。
アプリに興味がある人と、OSのようなモノが動く仕組みに興味がある人、違って当たり前かな、というところです。

知見は定期的に集めておこう

実際のところ、分散システム云々以前でも、システムとして 集約(集中管理)を基盤とした モノリシックといわれるものであっても セッション共有やDBレプリケーションという形で複数サーバーの論理集約はありました。
雑な整理ですが、セッション共有・DBレプリケーションの基盤が自己完結管理できるものであったところから、外部サービス(マイクロなサービス)が相手になる、という意味で複雑度が増しているという感じです。
例えば、サーキットブレーカーが必要になるというのが その一例です。

機能要件(業務仕様で決める)・非機能要件(フレームワークで決めてくれている)くらいの分類で十分というか それ以上は分からないという人もいれば、エンジニアたるものフルスタックエンジニア*2であるべしという人もいます。
DBレプリケーションについて考えたことが無いなんて信じられないという人もいれば、レプリケーション何それ?という人もいます。

私としては、ちょうど中間くらいの感じで、頭の体操含めて 知見は定期的に集めておこう という感じです。

進む抽象化(Javaを使う立場として)

Javaというか Jakarta EE を使う立場としては、Payara(PayaraMicroを含めて)のような EEサーバーの内部に k8sやIstioのような仕組みが組み込まれて 抽象化されて標準インターフェースで操作できるようになるんだろうな(してほしいな)というのがオチです。

これは Cloud FoundryなどのPaasも同じく。

Cloud Foundry におけるNATS(ルーティングをするところ)や、EEサーバーにおけるDAS(管理サーバー)のように、冗長化できない単一障害点を解消するために、Istioのような仕組みが使われるのではないかな?というのが 私の勝手な想像です。

本音

私は、別にTomcatやPayaraの設定値で はまったり、チューニングをしたいわけではなく、ただ システムを構築したいだけです。

ということで、一番求めているのは「Javaで作ったマイクロなサービスをイイ感じに論理集約してくれるようなPaas」です。
当面は マイクロなサービスを連携するほどの規模の開発はしないし、自分がやりたいことを優先して そっち系は静観していたら 求めるものが世に現れるだろう、と、楽観的に構えることにしました。

さいごに

どうでも良いところですが、言い訳として。
「マイクロなサービス」というのは、「マイクロサービス」というものに対して、私は厳密な理解をしていないというだけです。*3
雰囲気だけでも伝われば、まぁ良しという割り切りです。

*1:そして、まぁ、それでいいかな、と

*2:一昔の表現ですが

*3:なので マイクロサービス風 でも良いくらいです。