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

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

Java EE(JSF)でDDDのようなことを考えてみる(2)

vermeer.hatenablog.jp

から、多少の肉付けをした版。

やったこと

  • Validation

  • ValidationExceptionの制御

  • メッセージ(出力まで)

  • ConversationScope制御

  • Redirectの強制

基本は過去の記事などで取り扱った要素を組み込んだ感じです。

DDD的なところ?
あんまりありませんね(笑)

どっちかというと、JSFCDIによる肉付けをしました。という感じです。

パッケージ構成

まだまだ試行錯誤中

やろうとしていることは

  • 設計思想に関する関心事

  • 汎用実装に関する関心事

  • プロダクトに関する関心事

を それぞれコンテキストとして分けて、最終的には それぞれを別のプロジェクトにする、というイメージです。

設計思想と汎用実装を オレオレフレームワークにして、今後のプロダクトは 使っていくという感じにしたいなぁと。

あえていえば、今回のDDD的なところ、になるのはパッケージ構成くらいでしょうか。

Validation

概ね、こんな感じで良いだろう、というところまで出来た気がします。

public class DateOfBirthForm implements DefaultForm<DateOfBirth>, Serializable {

    private static final long serialVersionUID = 1L;

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

    public DateOfBirthForm() {
    }

    public DateOfBirthForm(String dateOfBirth) {
        this.value = dateOfBirth;
    }

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

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

}

Formの主な関心事であるValueObjectのアクセッサ(getXXX)に@Validを付与して、検証対象としておく。

public class DateOfBirth {

    @DateTimeFormat
    private final String value;

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

    public Age currentAge() {
        return new Age(this.date());
    }

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

    public LocalDate date() {
        return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }

}

ValueObjectのプロパティに対して、不変条件である@DateTimeFormat(自作)を注釈。
今回はしませんでしたが「過去の日付であること」とか「年齢が20歳以下」とか 要件によって条件を追加するイメージ。
もし「過去の日付であること」とする場合、標準アノテーションで検証をしようと思ったら プロパティの型がLocalDateじゃないといけないので、そこは悩ましいところ。
現状のアイディアとしては、戻り値の型がLocalDate検証用アクセッサを準備しておいて そこに検証アノテーションを付与するか、もともと「現在日付」については テストの容易性を考えて アプリケーション側で制御できるように 別途 インスタンスを設けるつもりもあったりするので、検証対象のクラスの型をStringとした自作検証アノテーションを設けるかもしれないです。
そのあたりは、実際に そういうことを実装するときに肉付けすれば良いかな。

@Documented
@Constraint(validatedBy = {ddd.domain.validation.annotation.DateTimeFormat.Validator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface DateTimeFormat {

    String message() default "{ddd.domain.validation.annotation.DateTimeFormat.message}";

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

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

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

        DateTimeFormat[] value();
    }

    public class Validator implements ConstraintValidator<DateTimeFormat, String> {

        @Override
        public void initialize(DateTimeFormat value) {
        }

        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            try {
                LocalDate.parse(value, DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT));
            } catch (DateTimeParseException ex) {
                return false;
            }
            return true;
        }
    }

}

自作の検証アノテーションについては、アノテーションと検証実装をペアとして1つのクラスで管理することにしました。
アノテーションはインターフェースみたいなものとして、別に実装をして 検証クラス(Validator)を独自に拡張できるように してみては?とも考えましたが、検証アノテーションでの検証クラスの指定の仕方が クラスパス直接指定であったため 分けても意味がないと考えて 1つにしました。
もし、検証クラスで検証ロジッククラスをDI出来るのであれば そういう方式で 拡張性が良さそうな仕組みになりそう、と思ったのですが。。
Hibernateなどインターフェースと実装を分けているので、出来るは出来ると思うのですが 私の力不足で ここは断念しました。

@Controller
public class UserRegistrationAction {

    private UserRegistrationPage registrationPage;

    private UserService userService;

    private Validator validator;

    public UserRegistrationAction() {
    }

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

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

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

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

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

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

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

        this.registrationPage.update(responseUser.get());
        return "persistcomplete.xhtml";
    }

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

}

Contorllerは こんな感じ

検証実行は

validator.validate(registrationPage.getValidationForm());

で やっていて、その対象情報は Formクラスから生成している、という感じです。
詳細は以下。

vermeer.hatenablog.jp

アドバイスもいただいたのですが、力不足でjavax.inject.Providerを使った方法では 出来ませんでした*1

ValidationExceptionの制御

上述の検証ロジックの

validator.validate();

の実装は

@Named
@Dependent
public class BeanValidatorProducer {

    @Produces
    public BeanValidator getBeanValidator() {
        return new BeanValidator();
    }
}
public class BeanValidator implements Validator {

    /**
     * {@inheritDoc }
     */
    @Override
    public void validate(Object validateTarget) {
        javax.validation.Validator validator = new GroupSequenceValidator(ValidationPriority.class);
        Set<ConstraintViolation<Object>> results = validator.validate(validateTarget);
        if (results.isEmpty() == false) {
            throw new BeanValidationException(results);
        }
    }
}

GroupSequenceValidatorは自作の拡張Validatior。

詳細は こちら

vermeerlab / beanvalidation / source / — Bitbucket

ですが、やっていることは

通常のValidatorは、validateする際に 摘要する validation group を指定しますが、 本クラスはインスタンスを生成する際に 適用する GroupSequenceを保持させておくことで validateの実装簡素化を図れます.

です(JavaDoc引用)。

で、検証例外が発生した時の挙動ですが

  • ExceptionHandler

  • Interceptor

の2段構えとしました。

通常で考えれば、Interceptorだけで良いと思います。
本当は、アノテーションを強要しないExceptionHandlerだけで解決したかったのですが、標準出力にエラーログが出力されてしまうので断念。
ログだけだったら、まぁ良いのですが(良くはないけれど)、ConversationIdが無くなったりとか まぁ、結局のところFacesContextsの参照とかしたいし、@Controllerの実装漏れを検知できて、ユーザーには「ちょっと変」くらいの印象を与えるだけで収まるセイフティーネットとしてExceptionHandlerを残して、Interceptorを基本とする方式にしました。

メッセージ

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

    @Inject
    UrlContext urlContext;

    @Inject
    MessageHandler messageHandler;

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

        String currentViewId = urlContext.currentViewId();

        try {
            return ic.proceed();
        } catch (BeanValidationException ex) {
            Set<ConstraintViolation<Object>> results = ex.getValidatedResults();
            messageHandler.appendMessage(results);
            return currentViewId;
        }

    }
}

InterceptorでBeanValidationException からメッセージ出力をしています。

で、実際のメッセージ出力クラスは以下。

public class JsfMessageHandler implements MessageHandler {

    /**
     * {@inheritDoc }
     */
    @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);

    }

}

メッセージ出力自体の制御は上述と同じく、自作のメッセージ編集ライブラリを使っています。

版数は少し古いですが、やっていることは

vermeer.hatenablog.jp

の通りです。

これを使うことで、標準検証アノテーションのメッセージに対して、「Formのラベル(表示用表現)+ 検証結果」を組み合わせた内容編集を 程良き感じで実現しています。
日本語だけだったら 検証アノテーションmessageで直接指定しても良いんでしょうけど、オレオレとはいえフレームワーク的なものを考えているので 国際化対応は初めから考慮したいところ、ということで組み込みました。

ConversationScope制御

vermeer.hatenablog.jp

を 余計な標準出力とかを除外して ほぼ そのまま移植。

Redirectの強制

vermeer.hatenablog.jp

これも余計な標準出力とかを除外して ほぼ そのまま移植。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

E2E test

vermeer_etc / jsf-ddd-e2etest / source / — Bitbucket

さいごに

これまで部分的に試行錯誤していたものを組み込んで1つの形にしたという感じです。
まぁまぁ形になった気がします。
まだまだ、やりたいことは山積みな訳ですが、Validationとその結果出力まで、というところで 一段落はついたので、そこまでを まとめてみました。
次は、絶対に避けれないセキュリティって思ったりするけど、先に出力メッセージに関する制御を やろうかな、とか正直 気分次第です(笑)。

*1:日本語情報が少なかったし、JSFでの実例も無かったので「おっ、これは良いかも(功名心)」と思ったり思わなかったりしたのですが、そんな下心で立ち向かうことは出来ず(苦笑)

SelenideでcheckedなRadioButtonの情報を取得する

ぱっと調べて、意外とヒットしなかったので記録しておこうと思います。

Selenideで、RadioButtonに値を設定するという実装例はネットで すぐに見つかったけど、逆が無かったので。

    public SelenideElement selectedGenderValue() {
        ElementsCollection items = $$(By.name("gender"));
        Iterator<SelenideElement> iterator = items.iterator();
        while (iterator.hasNext()) {
            SelenideElement item = iterator.next();
            if (item.has(Condition.checked)) {
                return item;
            }
        }
        return null;
    }

namegenderという、性別ラジオボタンの値を取得する、という感じです。

テスト用の部品なので、値が設定されていない場合は null にして、テストクラス側で異常終了をワザと発生させる感じにしてみました。

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で実装している時点で 限定はされているので

【考察】何を自動生成すると良いのかな?

自動生成というと エクセルなどのドキュメントから 超高速開発のようにコードを自動生成したり、雛型コードを生成して 後から詳細の実装を追記したり 雛型コードを継承して差分実装をしたり という*1感じだと思います。
そして、私自身、自動生成できるんだったら、それが一番良いんじゃないか? 設計から実装について強い関連付けがあれば 仕様と実装の乖離が無くなるのでベストじゃないか?と思っていました。

でも、それは違うのかもしれない、と 以前から ぼんやりと思っていて、一度 自分の中で整理しておいた方が良いのでは?と思って まとめてみました。

なお、今回 まとめよう と思うに至った直接のきっかけは

vermeer.hatenablog.jp

の神崎さんのRDRAによる要件定義手法の お話を伺ったことです。

要件定義から全体を俯瞰する情報を生成するということと、全てを書こうとしないという話から 色々と自分の中で波及していったという感じです。

要件定義や設計からソースコードのような実装を自動生成しようとすると、原則として全てを書かないといけないという前提があります。神崎さんのお話を伺って、必要なものを作るための手段であって、ソースコードのような実装資産を作ることだけが自動生成ではない、という結構 当たり前のことに立ち戻れて、自分なりに きちんと整理しておこう、となった次第です。

した方が良い(しても良さそう)

粒度とか項目毎の考察のボリュームはまちまちです。
思ったままに書いていきます。

一通り、書いてみて思った結論を先に示すと

  • 編集不可を保証できるものだけにする
  • 仕様と振る舞いが明確であること
  • 実装ではなく検証を扱うものの方が安全

と考えます。

ワークフロー定義

ワークフロー的な、BPMとか、ルールエンジンとかユーザーがビジュアルで確認しながら定義・設計するものは自動生成しても良いように思います。
ユーザーが要件として定義したものは、そのまま実現したいこと直結しています。
また、この手のツールはビジュアルでフローを見やすくしているなど工夫がされているのでユーザーにとっても 要件を表現しやすいと思います。

ただし、定義のやり方によっては、フローというよりもプログラムの分岐レベルの粒度の設計をしてしまって、疑似プログラミング言語による実装になりかねませんので、そこは注意が必要だと思います。
加えて、全てのフローを同一粒度で表示するため、規模が大きくなると 1つのフローがやたらと大きくなったり、逆に細かく部品化しすぎることで 全体の見通しが悪くなるなど、実際に使っていく中で 理想と現実の違いに悩まされることにもなりそうな気はしています。

ですが、それは粒度の問題です。自動生成に向いている向いていないという意味だと、生成した定義(XMLなど)を 開発者が編集する余地はなく 割と向いている と思います。

詳細情報から全体を把握するドキュメント

個別から全体を組み立てて全体を見通す資産を自動生成の対象として有効だと思います。整合性検証ツールや影響調査のレポートも類似です。
生成される資産は実行資産に対して直接の影響がないので安心して扱うことができます。
SIerの大規模プロジェクトだと、このためのチームが作られたりするように思います。*2

設計書という詳細情報から

自分のイメージに近いのは RDRAです。 *3

個々の設計書から、それらをまとめて俯瞰できるドキュメントを自動生成する、というのは有効だと思います。
トップダウンで作成するから、そんなものは いらないという考えもあると思います。
ですが、実際のところ 詳細の検討をしている中で 分かってくる関連性や それにあわせた修正を図りたいということは 多々あります。改修案件なども、その一例でしょう。
必要になった時に、積み上げて作成すれば良いかもしれませんが、これもまた大変で機能間で関連があると思っていたのに無いということになっていたり、その逆だったり。 それは記述誤りなのか、作成時のミスなのか、いずれにしても 詳細から積みあがったものと差異があると、そもそも検証・確認・検討ができません。
全体像が 正しく積みあがっているという証明は、想像以上に要件知識を必要します。 というか、規模が大きくなると、それが「確からしい」というレベルでも有識者じゃないと判断が難しいというのが、私の経験則です。
全体俯瞰を作るだけなのに、ドメインマスターの時間を必須とするのは 良い仕組みとは言えません。

長々書きましたが、部分から全体の作成は機械的にやりましょうよという事です。
そして、それが出来うるように ドキュメントには そういう仕掛けを始めから仕込んでおきましょう、ということです。 あとから仕込むのは大変です。

設計書の書式を検討する場合、何を どの工程で設計として記載していくのか?ということを考えると思います。
私としては、それだけでなく、例えばRDRAのように詳細から全体を組み立てる仕組みを 設計書でどうやって記載するようにしておくか ということを案件初期から検討しておくことをお勧めします。

実装という詳細情報から

同じく、ソースコードからクラス図を生成するみたいなものは、一定の有効性はあるように思います。
ただ、コードからクラス図を作るというのについては、「と思います」以上のことは 私の経験としては言えません。*4

例えば、JavaDoc や Swagger のようなものです。

JavaDocAPI仕様ということで、事前に実装無しで作成して それに合わせてテストをつくっておき、その上で実装するということもあるでしょう。そういう流れだとJavaDocは設計に相当します。
ただ、私の場合だと ちょっとそこまで理想的ではないというか、結構 です。頭の中で だいたいこんな感じのクラスを作っていきますか、と考えてから コメントなしで実装をザックリして、レビューしながら補足としてJavaDoc追記している という感じなので 仕様ではありますが 設計とは違うという感じです。
あとは、テスト作りながらを JavaDocを記述していくという感じです。テストしていく中で APIの説明が無いとか 分かりにくいと自分が感じたら、その感覚にあわせて 記述していくという感じです。*5

JavaDocにせよ Swaggerにせよ、私の理解としては、実装から生成した 今動いている機能のAPI仕様であって設計書ではない、と思っています。

そして実装と乖離の無いAPI仕様が いつでも作成できるというのが良いところです。*6

コメントいらない コードを読め、というのは良いですが、外部ライブラリを使ったりしていると そのコード自体を参照できないので API仕様は大事です。

実装から実装(または実行資産)

Pluggable Annotation Processing API

Doma*7 のようなAnnotation Processorにより実装から補助実装を作る仕組みも自動生成の1つだと思います。

Annotation Processorによるコードの自動生成のポイントは

  • コアとなる実装に対して 対となる補助的な実装が 常に同期を取って生成される
  • 生成されたコードは 編集できない*8

というところです。

ちょっと違いますが Lombok*9 も近しいと考えています。
最近、Lombokは避けられつつあるような気がしなくはないのですが、単純なアクセッサーであれば Lombokが生成するコードで問題ないと考えています。*10
注意した方が良いと思うところとしては、Lombokでアクセッサ以外のコードを扱おうと思った場合は、自分でDelombokして生成されるコードの確認はしておいた方が良いでしょう。Annotation Processorソースコードを生成するので デバッグなどで追跡する事が出来ますが、Lombokはクラスを編集しているようで ソースコードが無く デバッグが やりにくかったりします。

他にも、破壊的な事も出来そうなので 気をつけて使った方が良いレベルものではあります。例えば、JPAのEntityクラスからスキーマーを自動生成する(というか ALTER TABLEレベルの変更をする)というものは 便利ですが破壊的でもあります。

個人的には、Domaのように 自ライブラリを利用するための補助コードを自動生成するくらいの影響度が丁度良い気がしていますが、分かった上であれば 結構思い切ったものも生産性向上という意味では 面白いかもしれません。

ちなみに、そんなことを思って 自作したのが以下の「messege propertieからEnumクラスを作成する」です。*11

vermeer.hatenablog.jp

DSL

DSLから実コード生成するのを自動生成するというのも厳密には その範疇かな?と思ったので、思考の出し切りということで記載しておきます。

SQLのように標準化されたものだけでなく、オレオレDSLも有効なツールだと思います。
やはりポイントは、DSLで作られたソースコードを直接修正をしない(やる人いない気がするけど)というところです。

E2Eテストスクリプト(もしくはデータ)

E2Eテストについて、やりすぎ注意というのは それなりに分かっているつもりです。
大きい問題はテストスクリプトのメンテナンスコストだと思っています。
なので「すべてのテストケースを自動生成せよ」というつもりはありません。

IT(とか ST)は、ユーザーが「こうあってほしい」という要件の確認でもあり、開発側からすれば担保すべきものでもあります。*12
ユースケースの主たる経路シナリオ(基本・代替・例外)について、テスト設計を通じて合意をして、その合意の取れた資産でテストを実行する、としておくと、ユーザー・開発側 ともに安心感をもって先に進めることができます。
少なくとも 全く頓珍漢な方向には行っていないということを相互に確認できる仕組みは あった方が良いとか、これらのテストは「常に満たされた状態にしておく」ということが重要なので いつでも実行できるようにもしておきたいとか、これ以上は テストそのものの話なので割愛しますが、良さそうだという雰囲気だけでも伝わればと思います。

また、ユーザー自身がテストスクリプトを記述できるのであれば 当然ですが自動生成よりも もっと効果はあると思いますが、今回はあくまで自動生成の是非ということで除外しておきます。

以下に、その理由を追記しますが、あくまで この適用は
自動生成したスクリプトで出来る範囲に留めるというのが原則です。
もし、スクリプトそのものに対して 何かしら手を加えないといけないのであれば、補助する仕組みを自作するか、より良いものを選定し直すか、自動生成の対象外にして別管理とするか、再検討した方が良いです。
ユーザーと開発側の仕様認識を一致させることを優先すべきで、その上で 自動生成できる範囲であれば自動生成したスクリプトでテストする、という割り切りは必要です。

手順を示して結果を確認するだけだから

内部でどのようなことをしていようと関係なく、ユーザーにとって外してほしくないことは「こういうシステムであってほしい」というアウトプットです。

これは設計というよりも、要件定義に近しいです。

それって基本設計じゃないの?というのは適切な指摘だと思うところですが、ポイントは実現したい業務シナリオの方なので要件定義に近い物だと思っています。*13

なので、「画面項目のバリデーションチェックの確認」みたいなE2Eテストについては、やっても良いけど、あんまりやるとしんどくなるよ、という感じです。やったとしても、代替ルートの確認の一環として数ケースを扱うだけにした方が良いでしょう。
ポイントは実現したい業務シナリオの充足であって 網羅的な緻密さではありません。
ユーザーと開発間で、ユースケースに対する理解に齟齬が無い環境を作る ということが重要です。

ユーザーがメンテナンスできる

エクセルなどのドキュメントはユーザー自身がメンテナンスできます。
そして、内部実装の詳細を分からなくても、実現したい操作手順を記述することは ユーザーが実際にやりたいことなので イメージが しやすいところです。

また、ドキュメントを通じてシステムで出来ないことを、事前に開発側から伝えることも出来ます。
こんなことがあるか分かりませんが、テストシナリオで「ここでYahoo検索」という記述をしたとして「開発するシステムはプライベートネットワーク上で動くものなので外部ネット情報は検索できません」と言えます。その上で、なぜユーザーがYahoo検索をしたいのか というところから、当初見えていなかった要望を把握する事ができるかもしれません(例えば住所入力で郵便番号を調べたかったとか)。

条件ありで しても良い

設計から実装

後述で、しない方が良い として実装の自動生成については 色々と示したいと思っていますが、その中でも こういうものだったら 対象としても良いかもしれない というのを先に示したいと思います。

いずれにしても、大前提として生成した資産を直接編集しないということを守れる仕組みがある場合に限る と考えています。 必ずと言って良いほど この手の「触るな危険」運用は守られないというのが 私の経験則です。

<<ちょっと私の昔話>>

とあるベンダーのSAStrutsをベースとしたフレームワークが自動生成をするものでした。
何もないところから作り上げることと比べると生産性を高めるものであったとは思います。少なくとも、ある程度の形が整うくらいまでは。
詳細は割愛しますが、それには私の直感というか経験を踏まえた「多くの割り切り」を適用していったから、というのも少なからず貢献したと思っています。

それでも、帳票出力のところで JasperReportを使っていたのですが、その定義の自動生成は そこそこしんどかったですね。

自動生成で作れる範疇の開発で収まれば良いのですが、細かい要件取り込みが必要となる帳票系は 外部に提出する資料だったりするので ユーザー自身が割り切ることが出来ない(公的資料のレイアウトとか)ところがあります。

まぁ、一番つらかったのは、ベンダー提供のフレームワークなのに プロジェクト内で 一番理解しているのが私だけだったというところ。ベンダーのフレームワークチームへの問い合わせは出来たので 諸々解決はしていけましたし、開発に関わってくださったリーダーの方自体は 実装経験もある方だったので 全く頓珍漢ではなかったのですが、流石に VBクラサバだけの経験で、SAStrutsなにそれ?では ちょっと…というのは あったりなかったり。

特に佳境に入った際、実コードはこうなっていて、コード上でこういうことをしたかったら 設計書をこういう風に書かないとみたいな局面になると、私の自動生成コードの編集待ちが ボトルネックになりかねず。*14

正直、開発規模とメンバー数が 自分で制御可能な規模だったから出来たようなものです。すべての要件定義について、それがシステムの どの機能で どのように実現されるのかというところまで 私自身が語れる、というものでなかったら無理だったでしょう。*15

<<昔話はここまで>>

触るな危険の運用を守る方法は1つです。
責任を取る人以外は触らない運用を徹底するです。
これは生成されるコードそのものも含めます。

上述の昔話では、設計書と生成されるコードについては、チェックアウトして編集不可にしました。

責任者によるコードチェックアウトは、実際は結構な事件・事故の元になりえますが、そのくらいのリスクを孕んでいると思って運用しないと、ほぼ確実に それ以上の破壊*16が待っていると個人的には考えています。

つまり規模が大きくなったら運用困難だと思った方が良い、というのが素直な印象です。

定数

恐らく通常だったらやるであろう、定義ファイルから 生成した定数クラスコードを使って 実行環境に反映させる、というような運用ですが、こちらは 後述しますが しない方が良い に分類したいと考えています。

直感的には、定義情報から定数クラス(Enumクラスや、message.properties)を生成するのは良いと思いますし、機械的な作業という意味では しても良いと思います。

ただし適用には条件があります。

  • 対象定義書をクラスパス上に配置して上述のAnnotationProcessorなどの ビルド都度 上書き生成するような 強制力のある仕組みとペアにする
  • 更新者を限定的にするというケースに限った運用の徹底(上述)

わざわざ手で転記するのではなく自動生成をした方が良いのでは?というのは分かります。 であれば、実行資産を直接編集して、必要であればユーザーに提示するドキュメントに反映する仕組みを設けた方が良いと思います。これは上述の「詳細情報から全体を把握するドキュメント」でOKとしたパターンに準じます。

しない方が良い

以下で、色々と例示していきますが、した方が良い の裏返しです。
生成した資産に手を加える仕組みの場合は、しない方が良いし 可逆性を求めるとなると、更にしない方が良いです。

こちらも、一通り書いた 振り返りを先に書くと
上述の前提に加えて 適切なサンプル実装があれば十分じゃないですかというのが 私の総括的なところです。

定数クラス

上述の「条件ありで しても良い」とした「定数クラス生成」は、典型的なところなので 総括的に先に記載しておこうと思います。

一番の理由は、生成した資産に対して 誰でもできそうだからです。

そういうものは たいしたことはないと 誰もが思うために 設計書と実体の乖離が容易に発生してしまいがちです。

運用ルールがあり、引継ぎ作業もして、手順書を作っても、誰でもできそうの誘惑は大きく、何よりも管理者側にとっても、誰でも編集できると思っていることは さらに困ったことにつながります。
ちょっとだけ、今だけ、緊急だから、あとから同期を取れば良いから。
一番悪いのは、最後のところ。
それ、本当に責任もってやります?
だったら 始めから ちゃんと定義書から定義ファイルを作って更新しましょう?

してはいけない とまでは言いませんが、するんだったら ちゃんとしましょう という感じです。現実問題として、あなたが誘惑に負けた管理者に「ルールはルールなので徹底してください」と言えるのであれば問題ありません。*17

雛型コード

全く何もないところから、設計書を元に雛型となるコードを生成して、そのコードに対して実装を肉付けして開発をしていくパターンです。

良さそうですよね? 私も良さそうに思いました。

では なぜ しない方が良いのか?

可逆性がない、だけど設計書との整合性を保とうとしてしまうからです。

開発側で あくまで雛型だと分かっていても、設計者やユーザーからすれば 雛型ではなくて 整合性の取れたものとして扱われます。誰だって自分が関わったものを 使い捨て にするのは嫌です。 何よりユーザーに 使い捨てになります と開発側が言えるでしょうか?
では 設計書であっても、リファクタリング対象とすれば 捨てることは無いという理屈があったとしましょう。
でも生成されるのは あくまで雛型です。
設計書を修正する度に 業務要件を追記したソースコードへ雛型コードをマージすることを求められるとしたら? マージなんて差分チェックして転記するだけだから簡単でしょう と無邪気に言われたら?
難しい・簡単の話だと、もちろん簡単ですが、この煩雑さは求めている生産性と品質を担保している開発プロセスでしょうか?

こうやって当初の計画は簡単に崩れます。

幸運にも、ユーザーが設計書から作成されるコードが雛型であることを理解しているパターンでも油断はできません。
雛型コードだけれど、設計書から作成できると分かったら 出来るだけ無駄をなくしたいと思ってしまいます。 そうすると、設計書から作成される雛型コードを より「完璧にしよう」と躍起になってしまいます。 さっさと実装して動く状態で確認をしたくても、設計書を こねくり回してコード生成の確定を後ろに倒していきます。

そして、設計書からコードを出力して 早く動くものを確認できるようにするための仕組みのハズが、気が付くと逆方向に向かってしまうのです。

結局、作ったものを 捨てるのは 誰にとっても 心苦しいのです。

私の経験としては、実装に関わる設計変更は同期をとりましょう ということになりました。 といっても、実装されているコードに都度 取り込みたくはありません。仕方が無いのでくコードに合わせて設計書を編集しました*18
もうこうなると自動生成とは?って感じですし、謎のスキルです。

さて、そもそも雛型コードの生成によって、何を求めたのでしょうか?
満たそうとしたのは、まっさらな状態から Mavenで基本となるパッケージの生成にプラスアルファで設計書からコードも作ってしまいましょう、ということだけです。
これって スキルの高い人が丁寧に作成した、実際に動いているプロジェクトさえあれば ほとんど満たされます。
自動生成ツールを作っている人は 良かれと思って 何もない状態からコードを作ってくれていますが、理想と考える枠組みを示すくらいなら、いっそのこと 俺たちの考える最も理想的な実装例 を示してくれたほうが嬉しいです。
ただし、こんな感じで簡単に動きますよ、というレベルのサンプルはダメです。求めているのは段ボールハウスではありません。モデルハウスです。

フレームコード

大きい枠組みとなる部分のソースコードを設計書から自動生成するというパターン。
例えば、個々のロジックは、テンプレートパターンなどによる差分実装で肉付けをしたり、必要なモジュールを呼び出して連携するやり方を刺しています。

これについては、BPMに近しいので そこそこいけそうです。

でも、これも提供するものが編集可能なソースコードである以上、 雛型コードで示した課題を解消するものにはなりえません。

BPMやルールエンジンにおいて、フローを制御する部分は 定義情報を読み込んで制御しており原則として直接編集をしない仕組みだから成り立っています。
もし、定義情報であるXMLについて 直接編集をしたとしても あくまで定義情報なので XMLを編集するツールで読み込むことができる(つまり可逆性がある)ので問題はありません。

基底クラス

設計書からソースコードを生成はするけれど、あくまで基底クラス(Abstract Class)であって、生成したコードには編集を加えないというパターンです。

これだと、生成したソースコードを 編集対象としていないので 良さそうです。

私も これについては良さそうに思っていました

私の浅い記憶では Entityクラスに対して 本方式を取っていたように記憶しています。

基底となるEntityとして スキーマー定義に一致するJavaBeanを生成して、プラスアルファの操作については 継承して差分実装するという やり方です。

正直なところ、今回の整理を改めてするまで、このパターンについては 良さそうだと思っていました。

たしかに、基底クラス方式は、雛型コードと違って 編集するのは継承後のクラスのみとしておけば、事故の起きる余地はありません。それに継承による実装はオブジェクト指向っぽくて良さそうです*19

また、スキーマー定義と一致するORMクラスを自動生成してくれる仕組みも ありがたいですし、私自身、Netbeansで自動生成したものを使っていて便利です。
加えて、スキーマーから再生成をした際も、そのまま上書きをするという運用を徹底すれば、継承でも問題は なさそうに思われます。

今現在でも、絶対にやってはいけないとは言いにくいと思っており悩ましいところです。
これまでの整理と違って、このパターンについては どちらかというと設計思想的な意味で しない方が良い という整理を考えているので 正直毛色が違います。

さて、そもそもですが、自動生成した実装を継承するというのは どういう設計思想になるのかな?ということです。
考えられるのは、継承先に対して横断的に行いたいことや 共通して持たせておきたい機能があるということで フレームワーク特有の実装を基底クラスに組み込んで、それを活用するという設計思想です。
操作の抽象と意図したインターフェースを持っていれば、そのあたりも 基底クラスに implilemtしておくというのも あるでしょう。
実装に対して、ある一定の強制を強いることで コーディング規約のような役割(作法)を示すことも 広い意味ではフレームワークの適用範囲だとするのは 間違っているとは言えないでしょう。

継承よりも委譲

継承を嫌忌するというよりも、継承する必然のあるクラスですか?というのを考えてみましょう、という感じです。

例えば、スキーマーから生成したコードは 継承をすべき概念・属性でしょうか?
スキーマーがドメインモデルに近しいものになる事はあると思いますが、でも 継承するもの とは違うように思います。
もしくは、スキーマー定義(構造)を ドメインモデルに対して強依存をさせるということは 良い設計?という言い方でも良いと思います。
ちょっとしたCRUDアプリで スキーマー=画面要素 みたいな密結合アプリだと 余計なものがなくて良い、ということはありそうですが、それはそれで かなり限定された要件というか・・・、それを汎用的なシステム基盤として採用するのは 本当に良いのかな?というように、現在では 考えるに至ったという感じです。

もし、今回例示した、スキーマーの写像である Entityクラスを自動生成するのであれば、finalなクラス であれば良いと思います。そして、それを委譲して使用するという設計思想です。 結局、全プロパティを単純に委譲するだけだから無駄じゃない?というケースもあると思いますが、そのあたりの あるある話 は 継承よりも委譲 のところで論じられているように思いますので、そちらに譲りたいと思います。

横断的な制御

これは上述の Entityクラスの自動生成に限った話ではなく、Actionクラスなど レイヤー毎に適用したい機能実装を横断的に行うというようなことを示しています。

基底クラスを用いて横断的な制御を組み込むパターンは、自動生成云々とは関係なしに見かけるように思います。
賛否があるように思いますが、間違っている という訳でもないでしょう*20

とすると、自動生成でも問題ないのでは?というところですが やはり しない方が良い というのが結論です。

横断的な制御 を目的と考えた場合、実装の約束事として基底クラスを必須としなくてはいけないケースが 私の想定の範疇では考えつきませんでした。それらの要件は、アノテーションとインターフェースを用いたAOPで解決できるし、その方がスッキリと やりたいことが実現できると思います。また、約束事として基底クラスを継承するよりも、こちらの方が柔軟です。

スクリプト

バッチスクリプトや、ビルドスクリプトを定義情報(エクセルの設計書とか)から自動生成する、というのは かつて やったこともあったりしますが、それをするのであれば、定義や設計を読み込むスクリプトを作って そのパラメータ情報として ドキュメントをインプット情報として使う というのが正しいやり方だと思います。

スクリプトそのものを作った場合、何も知らない運用者が 一度でも スクリプトを直接修正してしまったら、大体のケースで今 動いているスクリプトが正しいということで 自動生成から作られたスクリプトは使われなくなります。
私の観測範囲では、突貫でスクリプトを直接編集して対応した場合にも、ドキュメントを修正して 差分比較云々をして、最終的に生成したスクリプトで改めてリリースしていれば まだ良心的で*21、最悪なのは、設計書から生成したスクリプトと 実行されているスクリプトが 振る舞いも含めて乖離してしまっているケースで、もはや過去作業実施者は何を元にユーザーと合意を取ったのか?という疑心暗鬼になるということが発生しかねないということです。

いずれにしても、振る舞いに直結するような自動生成については、ソースコードと同じく やめた方が良いです。

ビジネスルールから定義実装

RDRAでいうところのビジネスルールに相当するものです。場合によってはディシジョンテーブルと言われるものも含みます。

かつて、私は、ルール設計書から、XML定義ファイルを出力するというツール作成をしたことがあります。そのまま定義資産として読み込めますし、リソースとして実行中でも置き換えれば反映されるので、良さそうだと思っていました

定義情報自体に対して、開発者が直接手を加えることは運用上まず起きない(そんな面倒なことをする開発者もいない)ということもあり、安全性も担保されていたと思います。

ですが、今は、しない方が良い、と思うようになっています。

これは、その時にも 少し違和感を感じていた感覚があったのですが、追加した要素が意図通りに制御判定に使われているか、意図以外のところへの影響はないのか、ということについては担保するものでは無いと思うようになったからです。

メリットと思われることが、さほどメリットとも言えない、というところと 開発思想として なんだかシックリこないということがあって そういう結論に至りました。

設計と実装の乖離が無い というメリットへの反論

自動生成のメリットの最たるものですし、実現もしているので良さそうです。
でも、開発者が自動生成した資産を使用している(呼び出している)クラス側で、そのルールが意図通りの挙動であるかテスト実装をして確認をしている様子を見たときに「あれ?」と思ったのです。

自動生成ツールとして、設計通りの意図の定義を出力はしますが、それが 想定通りのケースを満たすものなのか というのは別の話なんですよね。

ルール定義は、それなりに整合性チェックをツール側でもしていましたが、チェックをかいくぐるタイプミスがあった場合や参照クラスのバージョン違いなどによる関連障害などを勘案すると確認をしないというわけにはいきません

設計と実装の乖離は無い、ただし その実装が正しく動く保証はないということです。

正しい、というのは、期待した挙動、ということです。
いくら「このif文だと、それ以外の挙動はしない」といっても、動かしていなければ保証されたとは言えません。
実際に動かしてみたらNullPointerで実行時例外、なんていうのは良くある話です。

動的に入れ替えが出来る というメリットへの反論

リソース資産として、実行中にでもルールを入れ替えできる、というのもルール定義XMLのメリットでした。

ただ、それを実施することは一度も見たことがありませんでした。

ルールはビジネスロジックの中核です。ただのif文でも無ければ、ただの定数の集合でもありません。
そんな類の資産を実行中の稼働環境にいきなり適用するプロジェクトがあったら そちらの方が恐ろしいです。

動的入れ替えができるということは ビルドをしなくても定義の入れ替えができる というメリットもあります。大きなプロダクトだとモジュールをビルドでまとめるだけでも まぁまぁの時間が必要です。

でも、それをルール定義XML部分だけに適用しても効果は あまり見込めませんし、繰り返しますが ルールのようなコアな資産を確認もせずに適用することは有り得ません。

でも、その時は そこには気が付けませんでした。今回の整理をする中でメリットと思っていたことが 改めて考えて そうではない と気が付いたという感じです。

ロジックによる改善余地が無い

やっていることは、ifやcaseで記述に準ずることと、スコープ上のインスタンスをDIして参照する、ということを おそらく内部的に行っていたと思います。

そのうち、条件分岐がXMLの主な役割で、あとはDIする対象資産の表明もしていたと思います。

表で記載された設計を元に単純な条件分岐を実装する、という単純な実装なので 自動生成でも良さそうですが、例えば状態遷移図のような 大きめの条件表記や、設計書の書き方によっては 冗長な判定となるようなXML定義書を出力ということが分かっていました。

例えば 早期リターンできそうなところも、XML定義だと しっかり最後まで全ての条件を総当たりする、みたいなことです。

例えば、性能面で課題があった場合に 改善の余地は XML読み込み部品に全て委ねられるか、XML定義の作成論理を全要件に最適なものとなるようにチューニングするしかありません。仮にそれが出来たとしましょう。では それまでに作成したXML定義は どうなるのでしょう?、ツールによって再生成したものが 正しい挙動を担保してくれる、ということをどうやって保証するのでしょう?
保証できるとしたらテストだけです。

一度 生成ツールの改修を行ったことがありますが、まぁまぁに大変でした。設計書の書き方の見直しがメインで生成するXML自体の変更は無かったので、前後のツールで資産を出力して xpathで比較するツールで検証する、みたいなことをして変更前後の保証をしました。これが内部の判定構造を変えるようなものだったら比較が出来ないものだったら・・・、考えるだけで ぞっとします。

もし、するのであれば・・・

対象ルールクラスのテスト用のスクリプトおよびテストデータと、ルールクラスのインターフェースを 自動生成するというのが 良かったように思います。

その上で、条件判定用のXML定義を生成するもしくは、生成した実装をDIでInjectionする、みたいな仕組みであれば 良いかもしれない という感じです。

ここまで面倒なことをする?というところはありますが、こうしておけば 正しく安全に変更できる仕組みになると思います。

設計からテストも作るし、実装も作る、ということについては 無駄では?それで正しい挙動の保証になるの?というという考え方もあると思います。どちらかというと、振る舞いに関する実装をするのであれば テストを優先したうえで 実装を生成すべきではないでしょうか?というのが 私の考えです。

びっくりしたのですが、ちょうど本記事を書いている時に神崎さんのツイートで似たようなことをつぶやかれていました。

余談

超高速開発ツール

自動生成といえば 設計書からコードを生成する、超高速開発ツール系についてですが、私は決して反対をするつもりはありません。

使用者において

  • やりたいこと(目的)が明確である

  • そのための振る舞い(手段)も明確である

  • 生成されたコードそのものには興味がない(手を加えない)

ということが明確であれば、使っても問題ないと思います。

極論ですが、エクセルの表で定義情報を まとめておいて Scratch*22でロジックを実装する、くらい明確な意図であれば 問題は無いと思います。

ただし、設計書からノンプログラミングでシステムが出来ますという事を鵜呑みにして導入するのは 止めた方が良いと思います。

まず、システムに対して その程度の認識であることが危険です。
使用者は むしろシステムというものを良く分かっている人です。

個人的には、セールストークとして、それを標榜するのは売り手の論理なので仕方なし、と思っています*23
なので、私が言いたいのは 買い手が賢くなりましょう ということです。
システムは ある面では 簡単に見えるところもありますが*24、簡単そうに見えて そうでもないところもある、そういう塩梅が分かっている人であれば、超高速開発ツールは 多分というか かなりのところ有益なツールだと思います。

EUC*25やRPAも同じ範疇だと思います。

疑似言語からコード生成(ネタ枠)

元々はDSLのところでネタ的に書いていたのですが、雑音になるので 余談に移動させました。

疑似言語というのは、自分の経験で関わったことがあるBAGLES2というCOBOL用の疑似言語です。ほとんどの人が知らないんじゃないかと思うのですが、まぁネタとして挙げてみた以上の意味はありません。

プログラミング工程がカットできるので工数削減できます、ということのようですが、BALGES記述は ほぼイコール COBOLの実装を そのまま日本語で記述したもので、しかもCOBOLで出来ることを表記表現可能な範囲で限定したものです。
プログラミングレスというよりも、実行可能なBAGLES設計書を ユーザーが要件定義として書いてくれるのなら、それは ユーザーがプログラミングをしているのと同じ、です。 ようは、設計書の形をした日本語で書けるプログラム言語です。

これは、汎用機開発において、プログラミング設計という工程で疑似プログラミング記述して、それをCOBOLに書き換える(コーディングする)という開発プロセスならではの話だと思います。
この COBOLに書き換える っていうのは無駄だよね、っていう意味にはなりますけど、これをもって業務仕様から自動生成って言って良いのかなぁ、という思いはあったりなかったり。

前向きに言えば この日本語で表現した疑似言語を通じて、情報システム部の人たちは実装を把握しようとしてくれていましたし、その土壌にはなっていたと思います。もし、これが C とか Javaで 書かれたものだったら 丸投げになるか、Javaなのに COBOLみたいな実装を強要された可能性はあります。*26
各種定義情報を分類して記述していくので、神クラス(モジュール)ができにくい仕組みです。知らない方も多いと思いますが、設計ツール、分類ツールとして学べるところはあると思います*27

さいごに

もともとの記事は「自動生成はソースコードではなく、テストを対象にするべきでは?」でした。
ただ、それだけだと 自分が考えていることとしては ちょっと足りていない気がして、コードの自動生成は 止めた方が良いというのも書いておこうと思いました。

思いついたときに追記追記をしていったので思った以上に時間がかかってしまいましたが、実際に自動生成をするか しないか 別にして、いざ何かしらやろうと思ったときに「あっ、これは 止めておいた方が良いと思って整理したものだ」という回帰ができる資料にはなった気がします。


*1:某ベンダーのフレームワーク

*2:ちなみに、私は そういうチームに所属して、こつこつVBAでコードを書いていました

*3:言い訳としては、そもそもです 私はRDRAおよびツールを使ったことはありません。あくまで先日の お話とネット上の資料による印象だけです。なので、ピント外れかもしれません。モデルベース要件定義テクニック は先日購入しましたが、未読です。。

*4:個人的にソースコード粒度のクラス図を見て、何かを考えるということをしたことはありません。細かすぎて、ありがたみがないし、ラフなものだったら クラス図を見なくても想像できるし、という感じで必要性を覚えたことも無ければ 欲しいと思ったこともありません

*5:カバレッジを埋めるだけということもありますが、一応 テストは作るようにしています。でもTDDはしてません。

*6:コメントが実装と乖離しているというのは、別の問題なので割愛します。

*7:https://doma.readthedocs.io/ja/stable/

*8:正しくはビルド都度 作り直される

*9:https://projectlombok.org/

*10:ただし独自ライブラリの中に入れるのは、やめておいた方が良いかもと最近は思っています。

*11:まだ実践投入できていないのが残念なところですが。。。

*12:ユーザーの要望通りの要件を満たしているから 検収OKしてねという。

*13:分類に意味は無いので、意図が一致していれば どちらでも良いです

*14:タスク調整をしてリスクは散らしましたが、全くなかったと言ったら多分嘘でしょう。そもそも自動生成のインプットって「設計書」だから開発工程以降の設計書修正って 設計なの?実装なの?っていう根源的な問題もあったりと。。

*15:ちなみに、このプロジェクトだけではありませんが、諸々思うところと 諸々のタイミングがあって、最終的に 私にとってSIerの実質最後の開発案件になりました。

*16:開発プロセスが壊れてしまう、システムが動かなくなる、改修困難になる

*17:ちなみに、私は言う人。だから嫌われる(笑)

*18:上述の昔話のプロジェクトでやりました。

*19:継承がある イコール オブジェクト指向とは思っていませんし、その手の宗教論的な話をするつもりはありません。

*20:テンプレートパターンも そういう側面をもったデザインパターンです。

*21:正直 この 差分比較をして同期を取る という工程自体も面倒でしたし、地味に神経を削られるものでした。

*22:https://scratch.mit.edu/

*23:売り手も生活がありますし、別に詐欺ではないです。嘘は言っていません。

*24:そして条件次第では実際に簡単

*25:VBAやエクセルの関数による業務効率化

*26:正しくは、ユーザーがコードレベルでも責任を取ろうする以上、自分たちが理解できるものにしてもらわないと許容できない

*27:公開されている情報が皆無に近しいので、学びようがないところはありますが

越境する基幹システム 〜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をつなげて考えることはありませんでした。

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

さいごに

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