特定のユースケースに関連する複数のクラスを1つのパッケージに集める

特定のユースケースへの要件が少ない場合は、
ユースケースに対応した1つのActionクラスで基本的な処理は実装できます。


しかし、ユースケースへの要件が多い場合は、扱うクラスが増えてしまいます。
例えば、ユースケース専用のDto(ActionForm)やユースケース専用のLogic、セッション格納用Dtoなどです。


特定のユースケースに関するクラスが複数存在する場合は、
パッケージが分散しているよりも、1つのパッケージに集めた方が開発しやすいと思います。
(Teedaはこのような構成でした。)


そこで、ユースケース名が "hoge" の場合、以下のような構成が可能なように
SAStrutsを拡張してみました。


Java側:
<ルートパッケージ>.web.hoge.HogeAction.java
<ルートパッケージ>.web.hoge.HogeDto.java (ActionForm)
<ルートパッケージ>.web.hoge.HogeSessionDto.java
<ルートパッケージ>.web.hoge.HogeLogic.java

JSP側:
webapp/hoge/add.jsp
webapp/hoge/xxx.jsp
webapp/hoge/yyy.jsp



イメージとしては、こんな感じです。ネストしたユースケースも扱うことができます。


実際に作成してみたアクションクラスは次のようになります。

package tutorial.web.hoge;

import org.seasar.framework.container.annotation.tiger.Binding;
import org.seasar.struts.annotation.ActionForm;
import org.seasar.struts.annotation.Execute;

public class HogeAction {

    @ActionForm
    @Binding(value="hoge_hogeDto")
    public HogeDto dto;

    @Binding(value="hoge_hogeLogic")
    public HogeLogic logic;

    @Execute(validator = false)
    public String add() {
        return "add.jsp";
    }

    @Execute(input = "add")
    public String doAdd() {
        dto.result = logic.add(dto.cast());
        return add();
    }
}

actionパッケージではなく、webパッケージ(S2標準のサブアプリケーションルート)の
直下にhogeというユースケースのパッケージを作成して、
そこにアクションやDto、ロジックといったクラスを配置しています。


同一パッケージなので、HogeDto やHogeLogic のインポート文も存在しません。


@Bindingアノテーションが微妙にうっとおしくなったので、
HogeDtoの代わりにDto、HogeLogic の代わりにLogicというクラス名にする
大胆な案も試してみました。
プロジェクトにDtoやLogicという名前のクラスが多数できて、
気味が悪いけど、別パッケージなので、一応できてしまいます。
ちなみに、こんな感じ。

package tutorial.web.fuga;

import org.seasar.struts.annotation.ActionForm;
import org.seasar.struts.annotation.Execute;

public class FugaAction {

    @ActionForm
    public Dto dto;

    public Logic logic;

    @Execute(validator = false)
    public String add() {
        return "add.jsp";
    }

    @Execute(input = "add")
    public String doAdd() {
        dto.result = logic.add(dto.cast());
        return add();
    }
}

さっきのHogeActionを比較するとずいぶんとスッキリとした印象を受けます。
確かに、ソースコードは読みやすいけど、
Eclipseで検索した時に同じ名前のクラスがたくさん出てくるのはちょっとイヤだし、
あまり前例がなく大胆すぎるので、心残り(*)だが自分としてはボツと考えることにしました。
(* 「書き易さ < 読み易さ」という意見を考慮すると…。)
(@Bindingアノテーション無しで public HogeDto dto; と記述できるとベストなんだけどなぁ。)


実際のSAStrutsの拡張方法をブログに書こうかと思ったけど、
次の5つのクラスをそれぞれ継承したクラスを作成して、
web.xmlとcustomizer.dicon の修正が必要なので説明が結構大変そうだ。

  • RoutingFilter
  • S2ActionMapping
  • S2ModuleConfig
  • S2ModuleConfigFactory
  • ActionCustomizer


頑張ってSAStruts拡張の説明をするよりも、
この仕組み自体をSAStruts本体に組み込まれてもよさそうな気がしてきた。

スケールするアクション

SAStrutsで、アクションの規模が大きくなっても、
ガタガタになりにくいであろうパターンを検討してみました。


まずは、簡単な足し算のサンプルプログラムを使って順を追って説明します。

【アクション】

  • アクションロジック
    • クラス名は、アクション名Logic。
    • publicフィールドにて用意します。
    • オブジェクト名は@Bindingアノテーションを使って簡潔にします。(推奨は、"logic")

アクションのサンプルコード

public class HogeAction {

    @ActionForm
    @Binding(value="hogeDto")
    public HogeDto dto;

    @Binding(value="hogeLogic")
    public HogeLogic logic;

    @Execute(validator = false)
    public String add() {
        return "add.jsp";
    }

    @Execute(input = "add")
    public String doAdd() {
        dto.result = logic.add(dto.cast());
        return add();
    }
}

【アクションフォーム】

  • アクション名Dtoという名前にします。
  • スコープはリクエストにします。(何もしなくて良い。)
  • RawDto
    • RawDtoという名前の内部クラスを作成します。
    • RawDtoは、アクションフォームのString型ではない適切なデータ型のプロパティで構成します。
  • castメソッド
    • アクションフォームをRawDtoに変換するためのcastメソッドを用意します。


今回はサンプルコードに登場しませんでしたが、必要に応じて以下の役割のメソッドを定義します。

  • copyメソッド
    • 引数として受け取ったセッションDtoへアクションフォームの値を詰め込みます。
  • conditionsメソッド
    • アクションフォームからS2JDBC用のBeanMapを生成するためのメソッドです。

アクションフォームのサンプルコード

public class HogeDto {
    
    public Integer result;
    
    @Required
    @IntegerType
    public String arg1;
    
    @Required
    @IntegerType
    public String arg2;
    
    public static class RawDto {
        public Integer result;
        public Integer arg1;
        public Integer arg2;
    }
    
    public RawDto cast() {
        return Beans.createAndCopy(RawDto.class, this).execute();
    }    
}

【アクションロジック】

  • クラス名は、アクション名Logicとします。
  • 複数のアクションにまたがる処理を記述する通常のロジッククラスと区別するために、特定のアクションでのみ使用するロジッククラスをアクションロジックと呼びます。
  • アクションロジックメソッドの引数にアクションフォームを使いたい場合は、代わりにRawDtoを使います。


アクションロジックのサンプルコード

public class HogeLogic {
    
    public Integer add(HogeDto.RawDto dto) {
        return dto.arg1 + dto.arg2;
    }    
}

今回は、登場しませんでしたが、セッションを使う場合は、セッションDtoを用意します。

【セッションDto

  • クラス名はアクション名SessionDtoとします。
  • スコープはセッションで、クラスに以下のアノテーションを付与します。
    • @Component(instance = InstanceType.SESSION)
  • 必要に応じて作成し、作成した場合はアクションにpublicフィールドとして用意しておきます。
  • アクションのpublicフィールドによってDIされたプロパティをアクションフォームのcopyメソッドへ渡すことでアクションフォームの値からセッションDtoへの値の詰め替えを行います。

課題は?

このパターンは、クラスの数が増加します。クラスの数が増加すると、
同一ユースケースのような関連クラスが分散していると開発効率が悪くなってしまいます。
そこで、次のエントリーでは、同一ユースケースに関連する複数のクラスを、
1箇所のユースケースに対応したパッケージで集中させる方法について説明します。