この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

スレッドローカルを使うハメになるのは設計ミスだと言ってみる

スレッドローカルは便利なクラスです。
いつでもどこでもスレッドセーフなグローバル変数を定義することができます。
ごく稀にですが、スレッドローカルを使わないと解決できない問題もあるでしょう。


しかし、思うのです。スレッドローカルを使うのは基本的に「負け」なのだと。

例えば

例えば、こんなコードがあったとします。

public class FooAction implements WebAction {

	// 入力フォームのバリデーションを行う。
	@Override
	public void validate(Form form) throws ValidationException {
		if (form.get("name") == null) {
			throw new ValidationException("name is null");
		}
		
		if (form.getInt("age") < 0) {
			throw new ValidationException("invalid age");
		}
	}
	
	public void processRequest(Request request) { ... }
}


さて、この仕様だと、ひとつでもエラーが発生した時点で、エラーとなるので、
残りのバリデーションは実行されません。ではどうすればよいでしょうか?

改善すると

手戻りは嫌なので、validateメソッドのシグネチャ等は変えたくない。
ではどうするか。


例えばこんなやり方があります。

public class ValidationErrorMsg {

    private static final ThreadLocal<List<String>> msgs = 
        new ThreadLocal<List<String>>() {
            protected List<String> initialVaule() { ... };
        };

    public static void addValidationError(String msg) {
        msgs.get().add(msg);
    }

    public static List<String> getValidationErrors() {
        return msgs.get();
    }
}


public class FooAction implements WebAction {

	// スレッドローカルで、エラーメッセージを保持する。
	@Override
	public void validate(Form form) {
		if (form.get("name") == null) {
			ValidationErrorMsg.add("name is null\n");
		}
		
		if (form.getInt("age") < 0) {
			ValidationErrorMsg.add("invalid age\n");
		}
	}
	
	public void processRequest(Request request) { ... }
}


で、フレームワーク側でValidationErrorMsg.get()の結果を処理する、というわけです。

個人的正解

しかしながら僕は、次のようにするのが、設計上の正解なのではないかと思います。
(これは実際に、ほとんどのバリデーションフレームワークが採用している方法でもあります)

public class ValidationContext {

    private final List<String> msgs = new ArrayList<List<String>>();

    public void addValidationError(String msg) {
        msgs.add(msg);
    }

    public List<String> getValidationErrors() {
        return msgs;
    }
}


public class FooAction implements WebAction {

	// ValidationContext引数を追加
	@Override
	public void validate(ValidationContext context, Form form) {
		if (form.get("name") == null) {
			context.addValidationError("name is null\n");
		}
		
		if (form.getInt("age") < 0) {
			context.addValidationError("invalid age\n");
		}
	}
	
	public void processRequest(Request request) { ... }
}


引数にValidationContextクラスを追加しています。
このようにしておくことで、また何か機能追加があった時でも柔軟に対応できます。


何らかのフレームワークを構成するとき、最初の設計では不十分であり、
後から必要に応じて機能追加していくことが多いと思います。

そのとき、如何にスマートに対応していくか、という観点からすると、
将来の拡張を見越して、Context変数を設計の最初から用意しておくべきだと僕は思います。


その意味において、安直にスレッドローカルへ逃げてしまうのは、設計上の「負け」だと思うのです。


ちなみに今、r42fwでスレッドローカルを使っている箇所が2つだけあります。
だから僕も偉そうなこと言えないんですけどね…(-_-)