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

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

ExceptionHandlerWrapperでInjectできないけど、ExceptionHandlerFactoryにはInjectできる

BeanValidationの結果をExceptionHandlerでメッセージ出力させようと思って色々やっていた足跡みたいなものです。 とりあえず、あとから振り返りが出来るようにするためのメモ。 (独自の実装クラスとか ありますが そのあたりの説明は省略します)

事象

ryoichi0102.hatenablog.com

全く同じ事象を経験されていた方がいました。

対処

そこで、ふと思いました。
生成するタイミング(つまりFactory)だったらInjectionできるんじゃないかな?と。

結論

ExceptionHandlerFactory にInjectしたインスタンス
ExceptionHandlerに渡せば できました。

実装

injectionしたいインターフェース

public interface ViewMessage {
    public void appendMessage(Set<ConstraintViolation<Object>> validatedResults);
}

injectionしたいクラス

public class JsfViewMessage implements ViewMessage {

    @Override
    public void appendMessage(Set<ConstraintViolation<Object>> validatedResults) {

        FacesContext facesContext = FacesContext.getCurrentInstance();

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

        MessageInterpolator interpolator = interpolatorFactory.create();

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

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

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

    }

}

Producer

@Named
@Dependent
public class ViewMessageProducer {

    @Produces
    public ViewMessage getViewMessage() {
        return new JsfViewMessage();
    }

}

ExceptionHandlerFactory

public class CustomExceptionHandlerFactory extends ExceptionHandlerFactory {

    private final ExceptionHandlerFactory parent;

    public CustomExceptionHandlerFactory(ExceptionHandlerFactory parent) {
        this.parent = parent;
    }

    @Inject
    ViewMessage message;

    @Override
    public ExceptionHandler getExceptionHandler() {
        ExceptionHandler handler = new CustomExceptionHandler(parent.getExceptionHandler(), message);
        return handler;
    }
}

ExceptionHandler

メッセージ出力の実装とか ちょっとしていますが、ポイントはコンストラクタで Factoryで取得したインスタンスを使うところ。

public class CustomExceptionHandler extends ExceptionHandlerWrapper {

    private final ExceptionHandler wrapped;

    private final ViewMessage viewMessage;

    CustomExceptionHandler(ExceptionHandler exception, ViewMessage viewMessage) {
        this.wrapped = exception;
        this.viewMessage = viewMessage;
    }

    @Override
    public ExceptionHandler getWrapped() {
        return this.wrapped;
    }

    @Override
    public void handle() {

        final Iterator<ExceptionQueuedEvent> it = getUnhandledExceptionQueuedEvents().iterator();

        while (it.hasNext()) {

            ExceptionQueuedEventContext eventContext = it.next().getContext();

            try {
                // ハンドリング対象のアプリケーション例外を取得
                Throwable th = getRootCause(eventContext.getException()).getCause();

                // 任意の例外毎に処理を行う
                this.handleBeanValidationException(th);

            } catch (IOException ex) {
                System.out.println(Arrays.toString(ex.getStackTrace()));

            } finally {
                // 未ハンドリングキューから削除する
                it.remove();
            }
        }
        getWrapped().handle();
    }

    void handleBeanValidationException(Throwable th) throws IOException {
        if (th instanceof BeanValidationException == false) {
            return;
        }

        BeanValidationException ex = (BeanValidationException) th;

        Set<ConstraintViolation<Object>> results = ex.getValidatedResults();

        viewMessage.appendMessage(results);

        FacesContext context = FacesContext.getCurrentInstance();

        String contextPath = context.getExternalContext().getRequestContextPath();
        String currentPage = context.getViewRoot().getViewId();
        context.getExternalContext().redirect(contextPath + currentPage);
    }

}

さいごに

という感じで Exceptionを扱うことは出来るには出来たのですが

警告:   #{userRegistrationAction.confirm()}: ddd.domain.validation.BeanValidationException
javax.faces.FacesException: #{userRegistrationAction.confirm()}: ddd.domain.validation.BeanValidationException
(以下、略)

標準出力に上述のログが出力される。。 (#{userRegistrationAction.confirm()} はトリガーとなったアクション)

このログは本来出力不要なものなので困った。。

Interceptorを使えば、当然 このようなログを出力することなく処理は可能。

デバッグで確認した限りでは、このログはExceptionHandlerの制御外で出力されているみたいなので、はてさてどうしたものか。。

結局、Interceptorでやるのが無難なのかなぁ。

追記

JSFとCDIとBeanValidationで想定外だったこと

そもそも「いや、それJSFだと亜流ですので」って終わりそうな話だと思いますが、誰かの何かに役立つかもしれないので。

環境

  • Payara5

  • Java EE7

  • Java8

事象


Controller(もしくは ManagedBeanのAction相当)

@Named
@RequestScoped
public class UserRegistrationAction {

    private UserRegistrationPage registrationForm;

    private UserService userService;

    private ViewMessage viewMessage;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, ViewMessage viewMessage) {
        this.registrationForm = registrationForm;
        this.userService = userService;
        this.viewMessage = viewMessage;
    }

    public String fwPersist() {
        this.registrationForm.init();
        return "persistedit.xhtml?faces-redirect=true";
    }

    public String confirm() {

        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

        //validateしても、対象のクラスフィールドに @Validと記述しても無視される
        //理由は、コンテナで生成したインスタンスのフィールドが nullだから。
        //ただし、アクセッサを経由したら値は取得できることは デバッグモードで確認済み
        Set<ConstraintViolation<Object>> results = validator.validate(registrationForm, ValidationPriority.class);
        this.viewMessage.appendMessage(results);
        if (results.isEmpty() == false) {
            return "persistedit.xhtml?faces-redirect=true";
        }

        return "persistconfirm.xhtml?faces-redirect=true";
    }

    public String register() {
        User requestUser = this.registrationForm.toUser();
        this.userService.register(requestUser);

        Optional<User> responseUser = this.userService.findByKey(requestUser);

        //とりあえず、登録要素が無いという有り得ない状況だったら 自画面にそのまま遷移させる(ありえないケース)
        if (responseUser.isPresent() == false) {
            return null;
        }

        this.registrationForm.update(responseUser.get());
        return "persistcomplete.xhtml?faces-redirect=true";
    }
}

Form(もしくはManagedBeanのActionFrom相当)

@Named
@SessionScoped
public class UserRegistrationPage implements Serializable {

    private static final long serialVersionUID = 1L;

    private UserId userId;


    private EmailForm userEmail;

    @Valid
    private NameForm name;

    @Valid
    private DateOfBirthForm dateOfBirth;

    @Valid
    private PhoneNumberForm phoneNumber;

    private GenderForm gender;

    public UserRegistrationPage() {
    }

(以下略)
public class NameForm implements DefaultForm<UserName>, Serializable {

    private static final long serialVersionUID = 1L;

    @NotBlank(groups = ValidationGroups.Form.class)
    private String value = "";

    public NameForm() {
    }

    public NameForm(String userName) {
        this.value = userName;
    }

    /**
     * @inheritDoc
     */
    @Override
    public String display() {
        return this.getValue().getValue();
    }

    /**
     * @inheritDoc
     */
    @Override
    public UserName getValue() {
        return new UserName(this.value);
    }
}

フィールドに@Validアノテーションしていても、対象クラス配下の@NotBlankが 有効にならず、次の画面に遷移してしまいます。

コード全量
Bitbucket

対応後


Controller(もしくは ManagedBeanのAction相当)

@Named
@RequestScoped
public class UserRegistrationAction {

    private UserRegistrationPage registrationForm;

    private UserService userService;

    private ViewMessage viewMessage;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, ViewMessage viewMessage) {
        this.registrationForm = registrationForm;
        this.userService = userService;
        this.viewMessage = viewMessage;
    }

    public String fwPersist() {
        this.registrationForm.init();
        return "persistedit.xhtml?faces-redirect=true";
    }

    public String confirm() {

        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

        //CDI管理外のインスタンスを引数として検証を行えば @Valid が有効(普通のBeanValidationの挙動)
        Set<ConstraintViolation<Object>> results = validator.validate(registrationForm.getValidationPersistUser(), ValidationPriority.class);
        this.viewMessage.appendMessage(results);
        if (results.isEmpty() == false) {
            return "persistedit.xhtml?faces-redirect=true";
        }

        return "persistconfirm.xhtml?faces-redirect=true";
    }

    public String register() {
        User requestUser = this.registrationForm.toUser();
        this.userService.register(requestUser);

        Optional<User> responseUser = this.userService.findByKey(requestUser);

        //とりあえず、登録要素が無いという有り得ない状況だったら 自画面にそのまま遷移させる(ありえないケース)
        if (responseUser.isPresent() == false) {
            return null;
        }

        this.registrationForm.update(responseUser.get());
        return "persistcomplete.xhtml?faces-redirect=true";
    }
}

Form(もしくはManagedBeanのActionFrom相当)

@Named
@SessionScoped
public class UserRegistrationPage implements Serializable {

    //@Valid を外す(意味がないので)

    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 Object getValidationPersistUser() {
        ValidationPersistUser obj = new ValidationPersistUser();

        //private インナークラスなので、外部から更新されることは気にする必要は無いと考え
        //コンストラクタ実装など手間はかけずにフィールドに直接設定
        obj.userEmail = userEmail;
        obj.name = name;
        obj.dateOfBirth = dateOfBirth;
        obj.phoneNumber = phoneNumber;
        return obj;
    }

    // 追加Actionで必須検証が必要なフィールドだけを抜粋して検証を行う.
    // 本メソッドを見れば、検証対象が分かるので groups による指定よりも見通しは良い(と思う)
    private static class ValidationPersistUser {

        @Valid
        private EmailForm userEmail;

        @Valid
        private NameForm name;

        @Valid
        private DateOfBirthForm dateOfBirth;

        @Valid
        private PhoneNumberForm phoneNumber;

    }

(省略)

コード全量
Bitbucket

動機

vermeer.hatenablog.jp

追記メモ

改めて コメント、本当に ありがとうございます。

javax.inject.Providerは所見だったので、関連しそうなところの勉強用のリンク

toydi/README.md at master · tokuhirom/toydi · GitHub

Google Guice 使い方メモ

[Dependency-injection] CDI注入ループ circular-dependency | CODE Q&A [日本語]

倖せの迷う森

さいごに

vermeer.hatenablog.jp

でも挙げた通り、これは私の実験場みたいなものなので、これがJSFの通常実装のパターンとは思わないでいただければと思います。

FormのプロパティをStringに

vermeer.hatenablog.jp

以前の記事では、どちらかというと Presenter パターンに近いイメージで実装しています。*1

集合の部品となるForm
その主たる関心事であるドメイン(ValueObject)を包含します。
インスタンス化およびgetterの際の型は基本的にStringです。
これはWebであるための物理制約です。

これは、これで正しいかな、と思っていたのですが Validation を考えた場合、Formのプロパティ自体はString にしておくべきでは? と考えに至りました。

プロパティをStringにする理由

Httpが文字列だから

もしくは、型クラスをプロパティとするためには、どこかで文字列から型クラスへの変換が必要だから、です。
これは、ValueObjectのプロパティが数値である場合、画面から入力した値に対して どうやって(もしくは どこで)数値保証すべき?というのが経緯です。
以前から クライアント側で HTML5であれ JavaScriptであれ 色々と実装するにしても、誰が どこで その検証を保証するのか?というところの落としどころで考えていました。
そこで まず、Formはクライアント入力情報を そのままの保持するもの、という整理を入口として 以降の設計指針を考えることにしました。

保持と検証のタイミングの違い

防御的プログラミングのできうるものであれば こんな考察は不要だとは思うのですが、Webシステムは 基本的に Validationをするにあたって、サーバー側は 何らかの方式で いったん保持した上で値を検証するという方式を取らざるを得ません。
つまり、どんな相手か分からないけど 一回受け入れて(保持して)から、その上で検証をしないといけない という前提があります。
そうすると 単純な落としどころしては「なんでも とりあえず 保持できそうな型」である文字列をFormのプロパティにしてしまうのが無難であるように考えました。

プロトタイプが やりやすい

これは 主たる理由というよりも、副産物なところですが FormのプロパティをStringにしておけば プロトタイプが作りやすいように思います。
HTMLでデザインをして、簡単なプロトタイプをつくってみるか、という際にプロパティを型クラスにすると、ラフにFormを作るのが少々面倒です。
HTMLデザイン→プロトForm→(並行してモデル分析)→FormにValueObjectを割り当て
という感じのことができます。

実装

以前のテスト実装では、ページ全体を表すクラスを Viewとしていましたが、Pageに変更しました。
同じように ページで使用するフィールドの値を保持するクラスを Formから ViewFormに変更しました。
理由はFormという表現をインターフェースで使うようにしたためです。

ページ

変更前

@Data
public class View {

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

}

変更後

@Data
public class Page {

    @Valid
    private ViewForm item;

}

Form

変更前

public class Form implements FormObject<String> {

    @Valid
    private final ValueObject value;

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

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

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

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

}

変更後

public class ViewForm implements DefaultForm<ValueObject> {

    @NotBlank(groups = FormValidation.class)
    private final String value;

    public ViewForm(String value) {
        this.value = value;
    }

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

    @Valid
    @Override
    public ValueObject getValue() {
        return ValueObject.of(value);
    }

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

}

実装を振り返って考察

参照だけならFormじゃなくても良いかも

参照だけの場合は Presenter パターンや DomainObject(ValueObject)をそのまま使っても良いと思います。

例えば 始めは 更新あり 参照のみ に関係なく通り一遍で Formを使ってデザインをしておいて、E2Eテストを準備した後でリファクタリングをしていくイメージです。

表示メソッドのDefaultをtoStringに

DefaultForm#displayのdefaultは#toStringにしました。

これはSpringMVCのFormマッピングtoStringを表示用に使用している「らしい」という知見からです。*2

つまり表示用の実装としてtoStringを実装するので、ついでにdisplayのデフォルトにもしよう、という感じです。

独自の型チェックが無くなる

ValueObjectをプロパティとする実装の場合、PageでNotBlank用に独自のValidatorを作成していましたが、今回のようにプロパティをStringとしたことで、標準のValidatorを使う実装に変更できました。

これが良いことか悪いことか それは分からないところではありますが、標準実装でカバーできるのであれば それに越したことは無いかな、と。

元の実装であれば「Pageクラス側で 対象のフィールドがNotBlank(必須)である」という文脈になりますが、改修後の文脈では「Pageクラス側で、対象フィールドを使用している」ということしか分からなくなります。 これについては、どちらの文脈が適当なのか、正直 まだ定まっていないところがあります。

Formのベースが直感的に分かりにくい

見ての通り というところはありますが、ValueObjectがプロパティでないということで、直感的にFormクラスが なんのValueObjectの表示用クラスなのか分かりにくいようにも思います。
あえて言えば、クラス宣言をする際の型アノテーションで示しているので どうにか という印象です。
ただ、プロパティによる定義では インターフェースによる強制が無いので 設計指針の強制という観点で考えると、今回の方が良いのかな?とも思うので、まぁ結果良し、という感じです。

参考リンク

decorator, presenter, exhibit という3つの実装パターンについて - pospomeのプログラミング日記

Code

Bitbucket

さいごに

個人的には、そこそこの納得感を覚えたわけですが、もともとの参考にさせていただいた

GitHub - system-sekkei/isolating-the-domain: Spring Boot : gradle, Spring MVC, Thymeleaf, MyBatis and Spring Security sample

SpringBootによる実装を見てみると、LocalDateをプロパティにして なんだかイイ感じにマッピングされているみたいなんですよね。

そうなると 今回の考察自体が ふんわりしてしまうところがあるわけですが、フレームワークに限定しすぎない設計を考えているわけだから、まぁ良いかな、という感じです。*3

*1:表示ロジックの実装クラスのプロパティの型をValueObjetにしている、というだけでPresentaterとしているので 厳密なところは 違うかもしれません

*2:なので正確なところは分かりません

*3:限定しない方式というつもりはありません。実際、私はJSFで実装している時点で 限定はされているので

越境する基幹システム 〜SoR 2.0 アプローチ〜 に行ってきました

SoR 2.0 って何だろうというのと RDRAに興味があるというのと 増田さんの お話を聞ける というところで いくつも興味があって

tagile.doorkeeper.jp

に行ってきました。

基幹システムと向き合いながら、SoEを構築する

  • 以前はスタートアップからの問い合わせが多かったけど 最近は大企業。
    アジャイルによる開発を試みることが前提になりつつあるということかな?)

  • SoRに最適化されているので 簡単にSoEにシフトできるわけではない。
    現状は社内受託開発みたいな感じでビジネスの中心にソフトがない。
    (残念ながら)本当の意味のCTOではないケースが多い。

  • 体制を つくるのにはコストが必要
    SESのように 人を埋めること(空要員をつくらないこと)が正しいという世界から いきなり変えられるものではない。

  • 大切を作るためのコストへの提案 SoEが既にできる傭兵チームを固定化する。
    大事なのはプロダクトではなくてチーム。
    プロジェクト毎にメンバーで総入れ替えをせず、傭兵チームにメンバーを割り当てて メンバーを成長させる。
    いきなり本丸(既存のSoR)を切り崩さない。後が続かない。
    傭兵チームが ずっといるわけではない。あくまで傭兵。
    SoRを保った状態で周囲がSoEを実現する力をつけてからSoEに取り組む。
    大事なのはチーム。
    傭兵チームは確実な進捗を保って、メンバーは同じチームにいることで学ぶ。

  • SoRとSoEは連携から始める 混ぜずに開発をすること。
    CSVファイル連携でも十分なので(WebAPIとか言わない)、連携をするところからスタート。
    (気持ちは分かるけど)いきなり 作り変える とか しない。
    まずはチームとして力をつけることが大事。
    また、力のない状態から始めるとSoEに重要なスピードが悪くなる。
    経験がないと(知識だけでは)スピードが出せない。

  • 傭兵チームも大変 技術と経験があるからといって楽ではない。
    そもそも 内製チームを抱えながら進捗を出すのは楽ではないし、仮説検証を担う重要な役割も。

  • SoEはハズれるかもしれない
    SoEは当たるとは限らない。
    (だから仮説検証をする。)
    でも大丈夫、チームが残っているし、経験が残っている。
    SoEを続けられる体制」を広げていくことが大事。

意思決定を加速するシステムの可視化方法

www.slideshare.net

  • RDRA2.0 = RDRA1.0 + ビジネスルール

  • ビジネスルール
    ビジネスコンテキストを抽出してバリエーションを可視化。
    こういうものはユーザー自身が既に持っていてエクセルで管理している資料になっていることが多い。
    (ので、発見もしやすいし、顧客とも会話がしやすい?)

  • 可視化に求められること
    影響度合いが高いこと。
    現場(ユーザー部門)で使えること。

  • 書くべきことが多すぎ問題への対処
    全部の業務要件を書こうとすると情報量が多すぎる問題。
    粒度を揃えることが難しい。
    メンテナンスが出来うる情報量であることが重要。

  • リレーション
    ユースケースで使っている情報リンクを把握できる表を自動でつくれる。
    (コンテキストのリンクを元にツールで作成する)

  • 変更点の把握
    入出力は物理的なものなので見つけやすい(画面変更とか)。
    ビジネスルールは見つけにくい。
    けど、ビジネスルールの特長は コンテキスト自体が増えるというよりもバリエーションが増えることが多いので、逆に バリエーションで判断するものを ビジネスルールとして個別に管理さえすれば要件の可視化がイイ感じにできる。

  • 分析可能であること
    個々の資料を関連付けるツールであるため 分析を重点的に実施したいところだけをピックアップして分析することが可能。
    一枚板で、過剰に完璧な資料だと 部分的な分析をするためのハードルが高くなる。

  • 正解ではなく俯瞰を重視
    要件をきちんと把握するために まず重要なのは俯瞰するところから始まる。
    そのためにやっているのが つながりの可視化(RDRA)

  • 全部を書こうとしない
    パワポから表を作る、とかいう話があると「じゃぁ、それからベースコードを自動生成すれば工数が削減できるし、ずれが無くなるじゃないか!」ということは良くある話。
    でも なかなか上手くいっているプロジェクトに出会ったことはなく。。(要件次第ではあるとは思いますし、ダメとは言いません。)
    RDRA自体からは 自動生成ではなくて「要件を分析する」に絞って考えましょうっていうメッセージを感じました。
    たしかに要件分析は 正確さはあった方が良いけれど スピード感の方を優先した方が良いケースが多いと思います。
    理想は要件定義時点でシステムテストシナリオが網羅できるくらいまで考えられれば良いのでしょうけれど、そちらを優先してしまったら そもそもサービスを立ち上げるスピードが遅くなってしまいます。
    SoR1.0脳の私は、発想としては そちら(要件定義時点でシステムテストシナリオつくるべきだよね)を優先すべきだという理想を追いがちなのですが、実際 自分だけでシステムを作る時に優先しているのは「何をしたいか」であって「それをどうやって担保するのか」は置いてけぼりだったりと自己矛盾アリアリ。
    なので「全部を書こうとしない」というシンプルな示唆は非常に響きましたし、実際 書き起こしてまで整理したい状態遷移はあるけれど、それ以外の自明なものは いきなり実装している実態にも合っているので 多分 正しいと思います。

  • MSOfficeユーザーじゃないんだよなぁ(ボヤキ)
    言っていることは正しいと思うし、パワーポイントで描いたもので表を作るのは良い仕組みだと思う。
    そして、私自身もそれに準ずるようなVBA開発プロセスというチームにて作っていたりもした経験があるので、その有効性についてはあると思う。
    ただただ残念(?)なのは、現状の私自身がMSOfficeを使っていないということ。。

「基幹システム」の再定義と再構築

www.slideshare.net

  • SoRをSoEに全面リニューアルはありえない
    SoEはSoRがありきで単純に代替できるものではない。

  • SoI(System of Insight)
    昔から手法や道具はたくさんあった。
    例えば 管理会計、DWH、BIなど。

  • SoE との関心と分離・連携
    SoEには AnytIme、Anywhere な情報があるというのがSoRとの違い。

  • SoR2.0に向けてやるべきこと モジュール性に対するセンスを磨くべし。
    昔は分解のしやすさだったけど、次は組み立てやすさを考えた設計をすべき。

  • プロジェクト管理の視点を変えよう SoR1.0は予実管理がマネジメントの基本だったけど、管理するのは成長差分に変えよう。
    予実管理は、正しいありかた が あった場合に有効だけど それが定まっていない中で行うべきは差分管理(というか できるのは差分管理だけ)。

  • 順番にSoE的なものにシフトする
    影響の少ない物から、順番にちょっとずつシフト。

事例紹介(BIGLOBE

内部的な話もされていたので 資料が公開されることはたぶん無いと思います。

  • DDDでサービスを作る
    BIGLOBEは30年近いシステム。
    しっかりモノリシックなので、それを改善したい。
    DDDにしたいよね、っていうことで 2013年に増田さんに来てもらってDDDに取り組む。
    まずは、影響が小さいところから着手。
    それを実施したチームを暖簾分けして、DDDが出来るチームを広げていく取り組みを実施。

  • RDRAでシステム分析
    システムはイイ感じにDDDで実装できて 順番に広がってきているけど サービス仕様書への属人性も解消したい。
    どうやらRDRAというものが良さそうだ。
    ということで、2017年4月に神崎さんに来てもらってRDRAに取り組む。
    サービス間の矛盾を見つけやすくなった(GOOD)。
    ユースケースとデータの可視化でテスト仕様書を起こすのがやりやすくなった(みたいな話をしていたような)。 まず、やりきることが大事(なので、RDRAが良いんだろうな)。

  • これって意味があるの? DDDにしろ、RDRAにしろ、メンバーとして「これって意味があるの?」という不安になることはある。
    初めてとりくむことだから誰もが不安。 そういうときには、増田さんや神崎さんのような外部の人に話をして聞かせてもらうと良い。

  • 辛いところ
    社員が成長すると離職してしまう。。
    BIGLOBEではDDDとRDRAに取り入れているので
     一緒に仕事をしたい人は是非」とのことです。
    DDDを新たに取り組むのも大事ですが、すでに取り組んでいるところに参加するのも良いですよって感じでしょうか。
    去る者はいるかもしれませんが、こういう情報発信は大事ですね。
    少なくとも、私はDDDとBIGLOBEをつなげて考えることはありませんでした。

離職リスクについては、確かに ある一定 起こりうるかなぁとも思いますが、それをしないことと比較して離職者が増えるか っていう比較をしないと何とも言えないですね。
嫌気を覚えて去るのか、未来を夢見て卒業するのか、後者だったら またどこかで会ったときに心から挨拶が出来るのだから、それだけでも良いと思います(個人的には)。
来るものもいれば 去る者もいるし、明日 それが自分自身になるかもしれない、まぁそんな感じです。

さいごに

久しぶりに勉強会の感想をブログを書きました。

E2Eテストのメモ

Selenide

Selenide: concise UI tests in Java

Try Selenide

Selenide~Javaで超簡単・簡潔にUIテストを書く~

Selenide入門

Spock

APIのテストを書くならspock + RESTClientが便利かも(Groovy)

JGiven

JGiven で 100% Pure Java BDD(導入編)

テストのありかた

Selenium(Java)でブラウザテストをするときにやっておくべき3つのこと - ISOROOT Tech Blog

E2Eテストの導入から学んだこと

E2Eテストについて考えてみた | MMMブログ

speakerdeck.com

WebアプリケーションでのE2Eテストの取り組みについて – Coiney Product Team – Medium

システムテスト自動化カンファレンス2017-2(2017/12/10)に行ってきました #stac2017 | ひびテク

www.slideshare.net

www.slideshare.net

https://www.slideshare.net/hirokotamagawa/20131201-lt

www.slideshare.net

www.slideshare.net

www.slideshare.net

www.slideshare.net

PageObject

Selenium入門その5[ページオブジェクトパターン(Page Object Design Pattern)を利用して変更に強いテストを作成する方法]

PageObjectデザインパターンを利用して画面変更に強いUIテストを作成する│ソフトウェアテストラボ|アプリテスト|スマートフォンテスト|株式会社SHIFT

Java EE(JSF)でDDDのようなことを考えてみるための土台

Formに関して色々と考えている中で、部分的に考えるのも良いけれど、一度、ベタで自分なりにJava EEJSF)で、DDDっぽい感じの実装サンプルをつくってみて もうちょっと全体を見渡せるようなものを作ってみた方が、もう少し具体的に考察出来るのかなぁと思うに至りました。

仕様・参考

仕様というか、要件というか、そのあたりについて 参考にさせていただいたものは

GitHub - system-sekkei/isolating-the-domain: Spring Boot : gradle, Spring MVC, Thymeleaf, MyBatis and Spring Security sample

です。

但書

  • 値検証・例外処理・セキュリティ とか全く実装していません。

  • 目下の目的がFormなので、JPA周りも いったん無しで、オンメモリで疑似的なものです。

  • 単純に画面からの入力を 疑似的にCRUDする ざっくりとしたものです。

  • 性能も当然(?)意識していません。

  • 考察のスタート地点としてlombokなど使わずに、ベタで実装したものです。

  • 考察のための入り口なので、これがJSFの普通の実装では確実にありません。*1

構成

過去に考えた、以下をベースに作成しています。

vermeer.hatenablog.jp

vermeer.hatenablog.jp

それ以外の詳細で、見直したところだけを以下に追記します。

Presentation

個々の部品となるFormはサブフォルダでまとめる

当初は、Domainのmodelパッケージと同様に 1つのフォルダに まとめれば良いだろう、と思っていました。
ですが 部品となるFormと画面ルートFormとActionが全て同じパッケージ配下に存在すると クラスが多すぎて わかりにくく感じました。
とりあえず、画面ルートとなるFormとActionというようなScopeを管理するクラスと 個々の部品は別パッケージにしました。
ちょっと悩んだのは、画面ルートとなるForm内で繰り返しの要素として使用するFormです。
部品と言えば部品ですが、集約と言えば集約です。
いったん、Actionなど 画面仕様に特化したものとして 画面ルートとなるFormと同じフォルダに格納しましたが、この辺りは また考え直す可能性があります。

xhtmlは会話スコープ

ブラウザから参照するxhtmlは同一会話スコープとなるものを同じフォルダで管理します。

といっても、今回はConversationScopedではなくSessionScopedを使って手抜きをしています。

ConversationScopedについては、以前 作ったライブラリを適用する形で 後日リファクタリングするつもりです。

とりあえずは、どういう感じで資産を管理するのか、ということを整理するに留めます。

templateを使う

ざっくりとはいえ、今回はtemplateを使って ある程度 JSFの基本的な作りに近しい感じにしました。

各ページの具体的なコンテンツはWEB-INF配下に整理します。

JSFで どの程度 有効なのか分かりませんが Servlet/JSPのように、直接コンテンツとなるxhtmlを参照できないようにしておいた方が セキュリティ的に良さそうに思っているので そうしました。*2

HTML Friendly な実装

xhtmlをEEサーバーを介さず、直接ブラウザで閲覧しても問題なくデザインを確認できるようにしています。

テンプレートエンジンは、thymeleaf だけじゃないですよ、というサンプルくらいには なるかな?と思います。

この後 類似部分について 更に 部品化していくと 結局 EEサーバーを介してデザインを確認しないといけなくはなるのですが、まぁ それは仕方ないかな、と。

とりあえず、初期デザイン時に作成したhtmlを ほとんど そのまま使えるというのは良いですね。

Action

validationもしていません。

仕様に合わない入力をしたら 即例外がスローされます。

今回は、あくまで実装の流れを整理する事だけしかしていません。

Application

特筆することは ありません。

単純なCRUDなアプリにアプリケーションサービスでやることって、RepositoryをCallすることだけ(と、Transaction)だけなんですよね。

Infrastructure

今回はDBを使わず、ダミーテーブル的なインスタンスApplicationScopedで設けました。

とりあえず、Repository周りのイメージを整理することだけをゴールとしています。

Code

Bitbucket

考察

仕様

仕様そのものについてですが、注文・発注など一般ユーザーが使う場合はタスクベースの画面構成であるのは正しいと思います。

ちゃんと2重Submitも制御しないといけない要件だと、この構成にしておかないと事故につながります(つまり正しい構成)。

ただ、自分が求めているのは もっと偏ったエクセルをそのままWebにしたみたいな画面です。

ざっくりいうと、一覧を直接編集して更新する、みたいな画面構成です。

同一画面上で、いくらでも好きなだけ更新もするし、2重Submitなんて気にせず 何度でも上書き更新するような雑な画面です。

もっといえば、大量の項目を入力する なんでも画面です。

最終的なオレオレラッパーフレームワークの仕組みとしては「正しい要件」を満たすのはもちろん、「使用者が限定的な業務システム」もカバーしたいという感じです。

冗長さが半端ない

過剰にも見えるクラス群と、右から左へのデータを写すだけのコードが異常に多いというのが 我ながらの感想です。

このあたりについては、AnnotationProcessorとか コード生成をするか、そもそも方式を含めて見直す必要もあるかも、と思っています。

viewActionが動かない?

なぜか、f:viewActionが動かなかったです。

仕方が無いので、とりあえずf:event type="preRenderView"を使いました。

他の お試し実装をするときには問題なく使えたんだけどなぁ、なんでだろ?

さいごに

まずは、なんとなく、こんな感じかなぁを作りました。

今後は、これに対して 色々と小細工を追加するか、「なんでも画面」のサンプルを先に作るか、どちらにしようかな?

*1:そもそも、JSFは画面単位でManagedBeanを作りましょう、というのが王道のようですから、私のような分割自体が なにそれ?と言われる可能性はあります。ただ、私にとっては こちらの構成の方が 分かりやすいのです。

*2:最終的に生成されるxhtmlの構造はクライアントで確認できるので、どのくらい効果的なのか不明です。

JSFのラジオボタン

ラジオボタン(SelectOneRadio)を使うことを個人的に あまり使うことがなくて 特に調べていなかったのですが、今 ちょっと やっていることで たまたま出てきて いざJSFで やろうとしたときに 色々と気になったので まとめてみることにしました。

あくまで、私が試した範囲ですが、正直な感想としては「出来そうで出来ないことが結構あるんだなぁ」ということが分かりました。

やりたいこと

  • JSFラジオボタンを使った画面操作をしたい

  • JSFのタグと、HTML Friendlyの両パターンの実装を比べたい

  • シンプルな選択

  • テーブル列の単一行選択

シンプルな選択

HTML

目標とする表現のHTMLは以下

<div>

    <h2>Pure HTML</h2>

    <div  style="float: left">
        <div>
            <input type="radio" value="MAN" name="gender" id="gender0">
            </input>
            <label for="gender0">男性</label>
        </div>
    </div>
    <div style="float: left">
        <div>
            <input type="radio" value="WOMAN" name="gender" id="gender1" />
            <label for="gender1">女性</label>
        </div>
    </div>
    <div style="float: left">
        <div>
            <input type="radio" value="OTHER" name="gender" id="gender2"/>
            <label for="gender2">不明</label>
        </div>
    </div>

</div>

JSF(Not HTML Friendly)

<h:form>
    <h:selectOneRadio value="#{genderForm.genderValue}">
        <f:selectItems value="#{genderForm.selectItems}"/>
    </h:selectOneRadio>

    <p>genderValue</p>
    <p>#{genderForm.genderValue}</p>

    <h:commandButton action="#{genderAction.update()}" value="更新"/>
</h:form>

ActionForm

私はActionとFormを分けて実装する派です。

xhtmlで参照できるように、SelectItemのListへEnumの情報を設定します。

@Named
@SessionScoped
public class GenderForm implements Serializable {

    private static final long serialVersionUID = 1L;

    private GenderType genderType;

    @PostConstruct
    public void init() {
        this.genderType = GenderType.MAN;
    }

    public List<SelectItem> getSelectItems() {
        List<SelectItem> items = new ArrayList<>();

        for (GenderType _genderType : GenderType.values()) {
            items.add(new SelectItem(String.valueOf(_genderType.getValue()), _genderType.getDisplay()));
        }

        return items;
    }

    public Integer getGenderValue() {
        return this.genderType.getValue();
    }

    public void setGenderValue(Integer genderValue) {
        this.genderType = GenderType.createGenderType(genderValue);
    }
}

Enum

public enum GenderType {

    MAN(0, "男性"),
    WOMAN(1, "女性"),
    OTHER(2, "不明");

    private final Integer value;
    private final String display;

    private GenderType(Integer value, String display) {
        this.value = value;
        this.display = display;
    }

    public Integer getValue() {
        return value;
    }

    public String getDisplay() {
        return display;
    }

    public static GenderType createGenderType(Integer value) {
        for (GenderType _genderType : GenderType.values()) {
            if (_genderType.getValue().equals(value)) {
                return _genderType;
            }
        }
        return GenderType.MAN;
    }

}

Action

リダイレクトもしないし、自画面遷移なので 何もしない空のアクションだけ。

Injectはコンストラクタインジェクションで。

@Named
@RequestScoped
public class GenderAction implements Serializable {
    private static final long serialVersionUID = 1L;

    private GenderForm genderForm;

    public GenderAction() {
    }

    @Inject
    public GenderAction(GenderForm genderForm) {
        this.genderForm = genderForm;
    }

    public void update() {
    }
}

JSF(HTML Friendly)

アクショントリガーとなるボタンについては、今回のメインではないので、JSFのタグで記述しています。

まずは あえて ui:repeatは使わずに、そのままブラウザで表示しても 出力イメージに近い結果になるようにしています。

<h:form>

    <div  style="float: left">
        <div>
            <input type="radio" jsf:id="#{genderForm.targetId(0)}">
                <f:passThroughAttributes value="#{genderForm.checked(0)}"/>
                <f:ajax event="click"  listener="#{genderAction.change}"/>
            </input>
            <label for="#{genderForm.targetFor(component.clientId, 0)}">
                #{genderForm.targetLabel(0)}
            </label>
        </div>
    </div>
    <div style="float: left">
        <div>
            <input type="radio" jsf:id="#{genderForm.targetId(1)}">
                <f:passThroughAttributes value="#{genderForm.checked(1)}"/>
                <f:ajax event="click" listener="#{genderAction.change}"/>
            </input>
            <label for="#{genderForm.targetFor(component.clientId, 1)}">
                #{genderForm.targetLabel(1)}
            </label>
        </div>
    </div>
    <div style="float: left">
        <div>
            <input type="radio" jsf:id="#{genderForm.targetId(2)}">
                <f:passThroughAttributes value="#{genderForm.checked(2)}" />
                <f:ajax event="click" listener="#{genderAction.change}"/>
            </input>
            <label for="#{genderForm.targetFor(component.clientId, 2)}">
                #{genderForm.targetLabel(2)}
            </label>
        </div>
    </div>

    <br />
    <p>genderValue</p>
    <p>#{genderForm.genderValue}</p>

    <br />
    <h:commandButton action="#{genderAction.updateRadio()}" value="更新"/>
    <br />
    <h:commandButton action="#{genderAction.confirm()}" value="確認"/>

</h:form>

f:passThroughAttributes

checkedvalueで値を設定できません。

仕方が無いので、ロジックで動的に出力します。

f:ajax event

ラジオボタンの選択がcheckedでしか表現できないために ボタンをチェックする都度、サーバー側のBeanを更新します。

checkedvalueで値を設定できないのと同様に、取得もできないためです。

仕方が無いパート2です。。

別にAjax操作の後に画面表記を変える必要は無いのでrenderは不要です。

シンプルな選択(ui:repeat)

繰り返しを使って動的にラジオボタンを出力します。

<div style="float: left" jsfc="ui:repeat" value="#{genderTypeItemsForm.items}" var="genderTypeForm" varStatus="stat">
    <div jsf:rendered="#{genderTypeItemsForm.renderChecked(stat.index)}">
        <input type="radio" jsf:id="radioOn" pt:name="radioJSF" value="#{genderTypeForm.genderTypeValue}" checked="checked">
            <f:ajax event="click"  listener="#{genderRepeatAction.change(stat.index)}"/>
        </input>
        <label for="#{genderTypeItemsForm.forTargetRadioOn(component)}" >
            #{genderTypeForm.display}
        </label>
    </div>

    <div jsf:rendered="#{genderTypeItemsForm.renderChecked(stat.index)==false}" class="designOnly">
        <input type="radio" jsf:id="radioOff" pt:name="radioJSF" value="#{genderTypeForm.genderTypeValue}">
            <f:ajax event="click"  listener="#{genderRepeatAction.change(stat.index)}"/>
        </input>
        <label for="#{genderTypeItemsForm.forTargetRadioOff(component)}" >
            #{genderTypeForm.display}
        </label>
    </div>
</div>
@Named
@SessionScoped
public class GenderTypeItemsForm implements Serializable {

    private static final long serialVersionUID = 1L;

    private List<GenderTypeForm> items;

    private GenderType genderType;

    @PostConstruct
    public void init() {
        List<GenderTypeForm> _items = new ArrayList<>();
        _items.add(new GenderTypeForm(GenderType.MAN));
        _items.add(new GenderTypeForm(GenderType.WOMAN));
        _items.add(new GenderTypeForm(GenderType.OTHER));
        this.items = _items;
        this.genderType = GenderType.MAN;
    }

    public List<GenderTypeForm> getItems() {
        return items;
    }

    public void setItems(List<GenderTypeForm> items) {
        this.items = items;
    }

    public Map<String, String> checked() {
        Map<String, String> map = new HashMap<>();
        map.put("checked", "checked");
        return map;
    }

    public boolean renderChecked(Integer itemIndex) {
        return Objects.equals(this.genderType.getValue(), this.items.get(itemIndex).getGenderTypeValue());
    }

    public String getDisplay() {
        return this.genderType.getDisplay();
    }

    public void updateGenderType(Integer index) {
        this.genderType = this.items.get(index).getGenderType();
    }

    public String forTargetRadioOn(UIComponent component) {
        return component.getParent().getClientId() + "-radioOn";
    }

    public String forTargetRadioOff(UIComponent component) {
        return component.getParent().getClientId() + "-radioOff";
    }

}

チェックのついたラジオボタンと ついていないラジオボタンを2つ準備しておき、出力するタグを分けます。

先と同じように checkedvalueで扱えないための苦肉の策です。

Label の for 指定をIDと同じ値にするためのメソッドforTargetRadioOnforTargetRadioOff も 引数を2つ以上指定出来なかったための苦肉の策です。

でも、別の実装お試しをしている時には出来たようなケースもあったので、多分 何か対応が不十分なだけな気もします。

テーブル列の単一行の選択

HTML

目標とする表現のHTMLは以下

<table>
    <thead>
        <tr>
            <th></th>
            <th>商品</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><input type="radio" value="AAAA" name="item" id="item0" checked="" /></td>
            <td>AA AA</td>
        </tr>

        <tr>
            <td><input type="radio" value="BBBB" name="item" id="item1"/></td>
            <td>BB BB</td>
        </tr>


        <tr>
            <td><input type="radio" value="CCCC" name="item" id="item2"/></td>
            <td>CC CC</td>
        </tr>
    </tbody>
</table>

JSF(Not HTML Friendly)

dataTableでの 良いやり方が見つけられませんでした。

pass-through でやろうとしたけど、selectOneRadio配下全てのname属性が全て同じになってしまった htmlコードをみて、たとえ うまく動いたとしても絶対に後々事故の元になると思ったので そっと諦めました。

ということで、実装したいなら input type="radio"だと思って試しましたが、今度は indexの取得で挫けました。

DataModel型を使えば出来るんでしょうけど、そのための実装量が多すぎるし 分かりにくいし。。

とにかく登場する要素が多すぎて これならdataTableを使わない方が良いや、となりました。

実際、私自身は、JSFを使う際、可能な限り HTML Friendly にするという指針なので dataTable自体を使っていませんでした。なので 諦めてもダメージが少ないというのも本音です。

JSF(HTML Friendly)

<h:form>
    <table>
        <thead>
            <tr>
                <th></th>
                <th>商品</th>
            </tr>
        </thead>
        <tbody>

            <tr jsfc="ui:repeat" value="#{itemsForm.items}" var="item" varStatus="state">
                <td>
                    <input type="radio" value="#{item.itemValue}" pt:name="itemJSF" jsf:id="radioOn"
                           rendered="#{itemsForm.renderChecked(state.index)}">
                        <f:passThroughAttributes value="#{itemsForm.checked}"/>
                        <f:ajax event="click"  listener="#{itemAction.change(state.index)}"/>
                    </input>
                    <input type="radio" value="#{item.itemValue}" pt:name="itemJSF" jsf:id="radioOff" class="designOnly"
                           rendered="#{itemsForm.renderChecked(state.index)==false}">
                        <f:ajax event="click"  listener="#{itemAction.change(state.index)}"/>
                    </input>
                </td>
                <td>#{item.itemValue}</td>
            </tr>
        </tbody>
    </table>

    <p>item = #{itemsForm.checkedItem}</p>
    <h:commandButton action="#{itemAction.update()}" value="更新"/>

</h:form>

「 シンプルな選択(ui:repeat)」の Label が無い版というところです。

その他

idとLabelのforの同値編集については、他にも色々なやり方があると思います。

例えば、idを直接指定するイメージで 以下のような書き方とかもあるでしょう。

<label>性別</label>
<div class="field">
    <div class="ui radio checkbox">
        <input type="radio" jsf:value="MAN" pt:name="gender" jsf:id="gender0">
            <f:passThroughAttributes value="#{userRegistrationForm.checked(0)}"/>
        </input>
        <label for="#{userRegistrationForm.targetFor(component,'gender0')}">男性</label>
    </div>
</div>
<div class="field">
    <div class="ui radio checkbox">
        <input type="radio" jsf:value="WOMAN" pt:name="gender" jsf:id="gender1">
            <f:passThroughAttributes value="#{userRegistrationForm.checked(1)}"/>
        </input>
        <label for="#{userRegistrationForm.targetFor(component,'gender1')}">女性</label>
    </div>
</div>
<div class="field">
    <div class="ui radio checkbox">
        <input type="radio" value="OTHER" pt:name="gender" jsf:id="gender2">
            <f:passThroughAttributes value="#{userRegistrationForm.checked(2)}"/>
        </input>
        <label for="#{userRegistrationForm.targetFor(component,'gender2')}">その他</label>
    </div>
</div>
public String targetFor(UIComponent component, String targetName) {
    return component.getClientId() + "-" + targetName;
}

参考

name attribute overriden when specifying input type="radio" as JSF passthrough element - Stack Overflow

JSF - JSF 2.2 selectOneRadioをテーブル一覧に出力させるには(43834)|teratail

「JSF selectOneRadio の同じneme属性のItemをテーブルの縦に割り振りたい。」(1) Java Solution − @IT

How to display the row index in a JSF datatable - Stack Overflow

Jsf datatable get row index program | JSF example code

[ H e p o n ' s T r i c k C u b e J a v a ]

Code

Bitbucket

さいごに

図らずも、JSFのBean参照のタイミングというか、出来る出来ないという範囲が 自分の直感的な理解とは異なるケースがあるというのが分かってよかった気がします。

「理屈としては、ここで参照できるはず」としないで、細かく確認しながら実装することが大事だと改めて思いました。

これはJSFが悪いとか そういう話ではなくて DDDでいうところの Domain以外のところは 実現性可否の調査をちゃんとしましょう、というだけの話だと思っています。