1. ホーム
  2. java

[解決済み] クラス内のすべてのメソッドが特定のコードブロックから始まるようにするエレガントな方法はありますか?

2022-05-14 18:27:19

質問

どのメソッドも同じように始まるクラスがあります。

class Foo {
  public void bar() {
    if (!fooIsEnabled) return;
    //...
  }
  public void baz() {
    if (!fooIsEnabled) return;
    //...
  }
  public void bat() {
    if (!fooIsEnabled) return;
    //...
  }
}

を要求する(できれば毎回書かない)いい方法はないでしょうか? fooIsEnabled 部分を要求する良い方法はありますか?

どのように解決するのですか?

エレガントかどうかは知りませんが、Javaの組み込みの java.lang.reflect.Proxy という を強制する に対するすべてのメソッド呼び出しが Foo をチェックすることから始まります。 enabled の状態を確認します。

main メソッドを使用します。

public static void main(String[] args) {
    Foo foo = Foo.newFoo();
    foo.setEnabled(false);
    foo.bar(); // won't print anything.
    foo.setEnabled(true);
    foo.bar(); // prints "Executing method bar"
}

Foo インターフェイスを使用します。

public interface Foo {
    boolean getEnabled();
    void setEnabled(boolean enable);

    void bar();
    void baz();
    void bat();

    // Needs Java 8 to have this convenience method here.
    static Foo newFoo() {
        FooFactory fooFactory = new FooFactory();
        return fooFactory.makeFoo();
    }
}

FooFactory クラスがあります。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class FooFactory {

    public Foo makeFoo() {
        return (Foo) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{Foo.class},
                new FooInvocationHandler(new FooImpl()));
    }

    private static class FooImpl implements Foo {
        private boolean enabled = false;

        @Override
        public boolean getEnabled() {
            return this.enabled;
        }

        @Override
        public void setEnabled(boolean enable) {
            this.enabled = enable;
        }

        @Override
        public void bar() {
            System.out.println("Executing method bar");
        }

        @Override
        public void baz() {
            System.out.println("Executing method baz");
        }

        @Override
        public void bat() {
            System.out.println("Executing method bat");
        }

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

        public FooInvocationHandler(FooImpl fooImpl) {
            this.fooImpl = fooImpl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Foo.class &&
                !method.getName().equals("getEnabled") &&
                !method.getName().equals("setEnabled")) {

                if (!this.fooImpl.getEnabled()) {
                    return null;
                }
            }

            return method.invoke(this.fooImpl, args);
        }
    }
}

他の人が指摘したように、心配するようなメソッドがほんの一握りしかない場合は、必要以上にやりすぎのように思えます。

とはいえ、確かに利点はあります。

  • ある種の懸念の分離が達成されます、なぜなら Foo のメソッドの実装が enabled をチェックする必要はありません。その代わり、メソッドのコードはメソッドの主な目的が何であるかについてだけ心配すればよく、それ以上は必要ありません。
  • 無実の開発者が、新しいメソッドを Foo クラスに新しいメソッドを追加し、誤って enabled をチェックします。 そのため enabled チェックの動作は、新しく追加されたメソッドに自動的に継承されます。
  • 別の横断的な関心事を追加する必要がある場合、あるいは enabled のチェックを強化する必要がある場合、一箇所で安全に行うことができ、非常に簡単です。
  • Java の組み込み機能でこの AOP のような動作が得られるのは、とても良いことです。のような他のフレームワークを統合することを強制されることはありません。 Spring のような他のフレームワークを統合することを強制されることはありません。

公平に見て、デメリットもあります。

  • プロキシ呼び出しを処理する実装コードのいくつかは、醜いものです。のインスタンス化を防ぐためにインナークラスがあることも、 不快だと言う人もいるでしょう。 FooImpl クラスのインスタンス化を防ぐために内部クラスを持つことは醜いという人もいるでしょう。
  • に新しいメソッドを追加したい場合 Foo に新しいメソッドを追加したい場合、実装クラスとインターフェースの2箇所を変更する必要があります。 大したことではありませんが、それでも少し手間がかかります。
  • プロキシ呼び出しは無料ではありません。一定のパフォーマンスオーバーヘッドがあります。しかし、一般的な使用では、それは顕著ではないでしょう。参照 を参照してください。 をご覧ください。

EDITです。

Fabian Streitelのコメントによって、私は上記のソリューションの2つの問題点について考えさせられました。

  1. 呼び出しハンドラでは、"getEnabled" と "setEnabled" メソッドで "enabled-check" をスキップするために魔法の文字列を使用します。これは、メソッド名がリファクタリングされた場合、簡単に壊れる可能性があります。
  2. もし、"enabled-check" の動作を継承しない新しいメソッドを追加する必要があるケースがあった場合、開発者がこれを間違ってしまうのはかなり簡単で、少なくとも、さらにマジック ストリングを追加することになるでしょう。

ポイント1を解決し、ポイント2の問題を少なくとも緩和するために、私はアノテーションを作成します。 BypassCheck (または類似の何か) を作成し、それを使って Foo インターフェイスのメソッドをマークするために使用します。 この方法では、魔法の文字列はまったく必要なく、開発者がこの特別なケースで新しいメソッドを正しく追加することが非常に簡単になります。

アノテーションのソリューションを使用すると、コードは次のようになります。

main メソッドを使用します。

public static void main(String[] args) {
    Foo foo = Foo.newFoo();
    foo.setEnabled(false);
    foo.bar(); // won't print anything.
    foo.setEnabled(true);
    foo.bar(); // prints "Executing method bar"
}

BypassCheck というアノテーションがあります。

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BypassCheck {
}

Foo インターフェイスを使用します。

public interface Foo {
    @BypassCheck boolean getEnabled();
    @BypassCheck void setEnabled(boolean enable);

    void bar();
    void baz();
    void bat();

    // Needs Java 8 to have this convenience method here.
    static Foo newFoo() {
        FooFactory fooFactory = new FooFactory();
        return fooFactory.makeFoo();
    }
}

FooFactory クラスがあります。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class FooFactory {

    public Foo makeFoo() {
        return (Foo) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{Foo.class},
                new FooInvocationHandler(new FooImpl()));
    }

    private static class FooImpl implements Foo {

        private boolean enabled = false;

        @Override
        public boolean getEnabled() {
            return this.enabled;
        }

        @Override
        public void setEnabled(boolean enable) {
            this.enabled = enable;
        }

        @Override
        public void bar() {
            System.out.println("Executing method bar");
        }

        @Override
        public void baz() {
            System.out.println("Executing method baz");
        }

        @Override
        public void bat() {
            System.out.println("Executing method bat");
        }

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

        public FooInvocationHandler(FooImpl fooImpl) {
            this.fooImpl = fooImpl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Foo.class
                    && !method.isAnnotationPresent(BypassCheck.class) // no magic strings
                    && !this.fooImpl.getEnabled()) {

                return null;
            }

            return method.invoke(this.fooImpl, args);
        }
    }
}