VOインターフェイスでアプリケーション層とビジネスロジック層を疎結合に

ActionFormとビジネスロジック層のインターフェイスはStrutsの悩みどころの一つです。

(個人的に)

ActionFormをビジネスロジック層に渡すのは論外ですが、

DTOパターンを使ってActionForm-ビジネスロジック層を疎結合にしようとすると、

DTOオブジェクトにActionFormのプロパティを転記する、といった処理が必要になり、それがなかなかめんどくさかったりします。

「BeanUtils#populateでActionFormからDTOにプロパティを転記する」方法が有名ですが、DTOに転記するプロパティのうち、一つでも型 が違うと例外が投げられてしまいます。

独自のConverterを使って型変換を行なうという方法もありますが、

たかだかDTOの転記でConvertUtils#register使うのも・・・

「DTOをActionFormがプロパティとして保持しておいて転記するプロパティ数を減らす」

という方法もありますが、

DTOをビジネスロジック層に渡した場合、渡したDTOが状態変更される可能性があります。

保守の観点だと、

「ビジネスロジック層がActionFormに及ぼす影響が大きい」

=「一箇所の修正に対して注意すべき範囲が複数のレイヤにまたがる」

という事があるので、Actionクラスを見ただけではActionFormの状態変更がどの程度行なわれているかわかりません。

もちろんビジネスロジッククラスを見ただけでも、

ActionクラスでActionFormがどのように変更されるかはわかりません。

というわけで、ちょっと微妙な方法かもしれませんが、

こういう解決方法もあるよ!という事で。

VOインターフェイスを実装したActionFormをビジネスロジックに渡す。

まず、ビジネスロジックでこんなインターフェイスがあるとします。

/**
* 注文に対する操作を定義するインターフェイス。
* @author zetta1985
*/
public interface OrderService {
    /**
    * 指定された顧客CDのCustomerVOを返す。
    *
    * @param customerCd
    * @return CustomerVO
    */
    public CustomerVO getCustomer(String customerCd);
    /**
    * 指定されたCustomerVOが注文した商品のリストを返す。
    *
    * @param customer
    * @return 注文した商品のリスト
    */
    public List findItems(CustomerVO customer);
    /**
    * 指定されたOrderVOを登録する。
    *
    * @param order
    */
    public void entryOrder(OrderVO order);
    /**
    * 指定されたOrderVOを更新する。
    *
    * @param order
    */
    public void updateOrder(OrderVO order);
    /**
    * 指定されたOrderVOを削除する。
    *
    * @param order
    */
    public void deleteOrder(OrderVO order);
}

次は、OrderServiceの引数となるVOのインターフェイスを定義します。

CustomerVOの実装は今回重要ではないので略します。

(ちなみにVO=イミュータブルという考え方です)

/**
* 注文を表すオブジェクト。
* @author zetta1985
*/
public interface OrderVO {
    /**
    * このオブジェクトが表す注文の顧客オブジェクトを返します。
    * @return 顧客オブジェクト
    */
    CustomerVO getCustomer();
    /**
    * このオブジェクトが表す注文の商品の変更不可能なListのビューを返します。
    *
    * @return 商品List
    */
    List itemsValues();
}

OrderServiceの具象クラスは、このOrderVOを利用してビジネスロジックを実行します。

メソッドのJavadocコメントにあるとおり、OrderVOはイミュータブルなので、

OrderServiceの具象クラスはOrderVOの状態を変更する事はできません。

(CustomerVOもイミュータブルと仮定)

そして、ActionForm。

/**
* 注文ActionFormクラス。
*
* @author zetta1985
*/
public class OrderForm extends ActionForm implements OrderVO{
    static final long serialVersionUID = 78743827874387238L;
    private String customerCode;
    private String customerName;
    private List itemList = new ArrayList();
    private CustomerVO customer;
    /**
    * このオブジェクトのプロパティを保持したOrderVOを返す。
    * @return
    */
    public OrderVO getOrderVO(){
      return this;
    }
    /* (non-Javadoc)
    * @see jp.co.zetta.vo.order.OrderVO#getCustomer()
    */
    public CustomerVO getCustomer() {
        return customer;
    }
    /* (non-Javadoc)
    * @see jp.co.zetta.vo.order.OrderVO#getOrderedItems()
    */
    public List itemsValues() {
        return Collections.unmodifiableList(itemList);
    }
    public void setCustomer(CustomerVO customer) {
        this.customer = customer;
    }
    /**
    * JSP用
    * @return
    */
    public List getItemList(){
        return itemList;
    }
    // 以下、略
}

ポイントはOrderFormクラスがOrderVOインターフェイスを実装している事、

OrderForm#getOrderVOで自身のインスタンスを返している事です。

OrderServiceが必要とするOrderVOをOrderFormクラスがわざわざインスタンス生成するのではなく、OrderFormクラスがOrderV OとしてOrderServiceに渡されます。

OrderServiceは渡されたOrderVOがOrderFormクラスだと知る必要は無く、

また、OrderServiceによって

「OrderFormインスタンスの予期せぬプロパティ変更が行なわれない」事が保障されます。

(OrderVOはゲッターしか定義していないので)

Actionクラスは以下のようになります。

※よく使われるであろうLookupDispatchActionを継承して、より実践的?にしてみました。

色々余分なものが混じってます。

/**
* 注文に対する操作を実行するActionクラス。
* @author zetta1985
*/
public class OrderAction extends LookupDispatchAction {
    private static final String success = "success";

    /* (non-Javadoc) */
    protected Map getKeyMethodMap() {
        Map keyMethodMap = new HashMap();
        keyMethodMap.put("button.find", "find");
        keyMethodMap.put("button.entry", "entry");
        // It doesn't recommend it. "regist" doesn't exist in English.
        //keyMethodMap.put("button.regist", "regist");
        keyMethodMap.put("button.update", "update");
        keyMethodMap.put("button.delete", "delete");
        return keyMethodMap;
    }

    /* (non-Javadoc) */
    protected ActionForward dispatchMethod(ActionMapping mapping, ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response,
        String name) throws Exception {
        // 共通前処理
        if (request.getSession().getAttribute(User.class.getName()) == null)
            throw new ServletException("User is null");
        OrderForm orderForm = ((OrderForm)form);
        orderForm.setCustomer(getService(request).getCustomer(orderForm.getCustomerCode()));
        // dispatchMethod
        ActionForward actionForward = super.dispatchMethod(mapping, form, request, response, name);
        // 共通後処理
        ActionMessages messages = new ActionMessages();
        messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("正常終了しました。", false));
        saveMessages(request, messages);
        return actionForward;
    }

    /**
    * 「find」的な処理
    */
    public ActionForward find(ActionMapping mapping, ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        OrderForm orderForm = ((OrderForm)form);
        orderForm.setItemList(getService(request).findItems(orderForm.getCustomer()));
        return mapping.findForward(success);
    }

    /**
    * 「entry」的な処理
    */
    public ActionForward entry(ActionMapping mapping, ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        getService(request).entryOrder(((OrderForm)form).getOrderVO()); // OrderVOを渡す(実はOrderForm自身)
        return mapping.findForward(success);
    }

    /**
    * 「update」的な処理
    */
    public ActionForward update(ActionMapping mapping, ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        getService(request).updateOrder(((OrderForm)form).getOrderVO());// OrderVOを渡す(実はOrderForm自(ry
        return mapping.findForward(success);
    }

    /**
    * 「delete」的な処理
    */
    public ActionForward delete(ActionMapping mapping, ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        getService(request).deleteOrder(((OrderForm)form).getOrderVO());// OrderVOを渡す(実は(ry
        return mapping.findForward(success);
    }

    /**
    * OrderServiceを取得する。
    * @param request
    * @return OrderServiceオブジェクト
    */
    public OrderService getService(HttpServletRequest request){
        OrderService service = (OrderService)request.getAttribute(OrderService.class.getName());
        if (service == null){
            OrderServiceFactory factory = (OrderServiceFactory)getServlet().getServletContext()
              .getAttribute(OrderAction.class.getName());
            service = factory.getService();
            request.setAttribute(OrderService.class.getName(), service);
        }
        return service;
    }
}

OrderServiceにOrderFormを直接渡さず、わざわざOrderForm#getOrderVOを呼び出しているのは、

将来の変更で「やっぱDTO作って転記したインスタンスをOrderServiceに渡したくなった!」

って時に少しでも修正コードを減らすためです。

(みみっちいですが)

Scalaをちょびっと触り、

「イミュータブルって自分が思っている以上に効果があるのでは・・・」と考えた結果がコレです。

VOのインターフェイスを作る、ってのはあまり一般的じゃ無いかもしれませんが、

「あるオブジェクトの状態を変化するクラスを制限する」

というのは意外と重要になってくるんじゃないかなぁ、と思いました。

結合テストで異常が出た場合のバグシューティングがしやすくなったりとか。

複数人での開発チームの結合テストを傍から見てて、こんなやり取りがあったりなかったり・・・

A「ん?hogeになんか変な値が入ってるよ?」
B「このオブジェクト、お前の作ったモジュールでhogeを設定してたよな?」
C「え、そーだっかなー・・・ちょっと調べてみる・・・」
A「しっかりしろよー」
(10分後)
A「C、どーお?」
C「・・・やっぱ、ちげーし!俺じゃねーし!Bだし!たぶん!」
B「は!?いやいや、俺、hogeいじった覚えねーもん!」
A「いちお、調べてみたらー?」
(また10分後)
A「B、どーお?」
B「・・・Σ( ̄□ ̄)!!」
C「…( ゚д゚)、ペッ」
A「しっかりしろよー」

楽しく開発する為にも、

自身が作るクラスが本当にミュータブルである必要があるのかどうか

一考してはいかがでしょうか?

(なんだこのまとめ方・・・)

このエントリーをはてなブックマークに追加
comments powered by Disqus