ログインユーザごとに動作の異なるサービスをどうやって実装するか


Springを何度か実戦投入してみたが、まだどうやって使うのが正しいのか分からないところがある。
例えば「ログインユーザごとに動作の異なるサービス」をどうやって実現するか。
同じサービスであっても、誰が実行するかによって実際に行われることが異なる、というのは普通にあること。
それをDIではどうやって捌くのがよいか、というのが知りたい。

お題

例:
購買の申請画面で「申請」ボタンを押すと

  • 平社員なら「権限がありません」エラーになる
  • 申請担当者なら、支払申請をDBに書き、上長に承認依頼メールを飛ばす
  • 各部門長なら、
    • 100万円以下なら支払承認をDBに書く
    • 100万円以上なら支払申請をDBに書き、経理部長に承認依頼メールを飛ばす
  • 経理部長なら、金額がいくらでも支払承認をDBに書く

これをSpring管理下のサービスとして実装する場合、どうするか。


使う道具は以下の通り:
購買申請サービスインターフェース:

public interface KoubaiService {
    // 購買申請実行
    public void shinsei(Object param);
}

ログインユーザを表すインターフェース:

public interface User {
    // 申請担当者ならtrueを返す
    public boolean isShinseiTanto();
    // 部長ならtrueを返す
    public boolean isBucho();
    // 経理部長ならtrueを返す
    public boolean isKeiriBucho();
}

あと、Struts + Springで実装するということにする。

やりたいこと

例えば、申請実行の Struts Action の中で、こんなことはしたくない:

    User user = request.getSession().getAttribute("user");
    KoubaiService service;  // 購買サービス
    if (user.isKeiriBucho()){
        // ユーザが経理部長
        service = beanFactory.getBean("購買サービス_経理部長");
        
    } else if (user.isBucho()){
        // ユーザが部門長
        service = beanFactory.getBean("購買サービス_部門長");
        
    } else if (user.isShinseiTanto()){
        // ユーザが申請担当者
        service = beanFactory.getBean("購買サービス_申請担当");
        
    } else {
        // ユーザが平社員
        throw new XXXXException("権限がありません");
    }
    // 申請実行
    service.shinsei(param); 

当然、こうしたい:

    // ↓実装型はログインユーザが誰なのかによって切り替わる。同じ"購買サービス"というキーを渡されていても
    KoubaiService service = beanFactory.getBean("購買サービス");
    service.shinsei(param);

Spring にはアプリケーションの権限レベル実装(ユーザには「申請担当」「部門長」「経理部長」「その他」の4種類がある、とか)
なんて分からないから、Spring 自体が上記の「こうしたい」を実現してくれるわけがない。
しかし「同じキーを渡しても、ログインユーザによって違うBeanを返してほしい」というニーズはある。
では、落としどころはどこか。


これはダメな解

以前作ったシステムでは、アプリケーションのセッションを表す SessionService というサービスを作って、サービス層の各オブジェクトの中でユーザごとに異なる情報を参照できるようにしてみたが、これはあんまり良い解ではない。
その仕組みを使って、上記のお題を解くと、KoubaiService の実装はこんなコードになってしまう。

public class KoubaiServiceImpl implements KoubaiService {

    // この中にログイン中の全ユーザのUserオブジェクトが収容されている
    SessionSerivce sessionSerivce;
    
    public void sinsei(Object param) {

        /*
         * 単一の実装の内部で激しくif分岐して、全権限の動作をサポートする。
         */
        
        User user = sessinService.getUser(); // 現在のスレッドに関連付けられたUserオブジェクトを返す
        if (user.isKeiriBucho()){
            // ユーザが経理部長
            (承認実行のコード)
            
        } else if (user.isBucho()){
            // ユーザが部門長
            if (金額 <= 100万円) {
                (承認実行のコード)
                
            } else {
                (経理部長に承認依頼メールを飛ばすコード)
            }
            
        } else if (user.isShinseiTanto()){
            // ユーザが申請担当者
            (上長に承認依頼メールを飛ばすコード)
            
        } else {
            // ユーザが平社員
            (権限がありませんエラー)
        }
    }
}

Open-Closed原則違反てやつですか。これはダメだ。
正解は、アプリケーション(この場合は Struts の Action クラス)はフレームワークが提供する BeanFactory をナマで参照するのではなく、
アプリケーション独自の権限実装を理解するBeanFactoryラッパーを作って、それ経由でサービスを取り出すことなのかな。
世の中の人たちはどうしているのだろう?
Spring とか Seasar でこの問題を解いているサンプルアプリをご存知の方、ぜひご教示願います。