ラッチオブジェクトによる、一回だけ呼び出し

Heavy_WritableCheck() が重いため update() を呼びだす UI スレッドが固まるという問題が報告された。幸い isWritable() はワーカースレッドから呼ばれるので、そちらに Heavy_WritableCheck() の呼び出しを移そうということになった。

なんてことがあったり、する、よね?

private boolean mWritable = false;

public synchronized boolean isWritable() {
  return mWritable;
}

public synchronized void update() {
  ...
  if (...) {
    mWritable = Heavy_WritableCheck();
  } else {
    mWritable = false;
  }
  ...
}

MyBoolean クラスの定義

そこで値の取得を遅延させるために MyBoolean クラスを定義する。 value() を呼ぶと、その値を返すというインターフェースだ。(標準の Boolean を派生して booleanValue() をオーバーライドできるといいんだけれど final class のため無理。残念)

true と false は、あらかじめ static final なクラス変数として定義しておく。

private abstract class MyBoolean {
  abstract boolean value();

  static final MyBoolean True = new MyBoolean() {
    @Override
    boolean value() {
      return true;
    }
  }

  static final MyBoolean False = new MyBoolean() {
    @Override
    boolean value() {
      return false;
    }
  }
}

MyBoolean を利用した実行遅延

MyBoolean を使うと、最初のコードは以下のように変形できる。 update() の if 文 then 節では MyBoolean の無名オブジェクトを作成する。その値を value() で取得したときに Heavy_WritableCheck() が実行される。つまり isWritable() 呼び出し時まで遅延される。

private MyBoolean mWritable = MyBoolean.False;

public synchronized boolean isWritable() {
  return mWritable.value();
}

public synchronized void update() {
  ...
  if (...) {
    mWritable = new MyBoolean() {
      @Override
      boolean value() {
        return Heavy_WritableCheck();
      }
    };
  } else {
    mWritable = MyBoolean.False;
  }
  ...
}

これで一応当初の目的は果たしたことになるけれど、もとのコードは update() をかけるごとに一度だけ呼ばれていた Heavy_WritableCheck() が、このコードでは isWritable() を呼ぶたびに実行されるという望ましくない副作用が混入してしまった。

ラッチオブジェクトによる一回のみ実行

というわけで、次のようなラッチオブジェクトをつくって update() を書き換える。

public synchronized void update() {
  ...
  if (...) {
    mWritable = new MyBoolean() {
      @Override
      boolean value() {
        return latch.value();
      }

      private MyBoolean latch = new MyBoolean() {
        @Override
        boolean value() {
          latch = Heavy_WritableCheck() ? MyBoolean.True : MyBoolean.False;
          return latch.value();
        }
      };
    };
  } else {
    mWritable = MyBoolean.False;
  }
   ...
}

update() 呼び出し後の初回の isWritable() で mWritable.value() が呼ばれると、オブジェクト初期化時に作成された内側の MyBoolean 無名オブジェクトの value() が呼ばれる。

ここで Heavy_WritableCheck() を呼び出して値を確認し、その結果に従って latch オブジェクトを MyBoolean.True あるいは MyBoolean.False で置き換えてしまう。(そしてその latch オブジェクトの value() を返す)

以後の isWritable() の呼び出しでは置き換えられた MyBoolean.True、あるいは MyBoolean.False() の value() が呼ばれるため、 Heavy_WritableCheck() は初回にただ一度呼ぶだけで済む。

これは、 Java の無名オブジェクトは外側のコンテキストを触ることができるという特性を利用している。この場合、内側の無名 MyBoolean オブジェクトが、その外側の無名 MyBoolean オブジェクトの変数 latch を、何食わぬ顔で書き換えて参照しているところ。(少し前の C++ だと無名オブジェクトが利用できないため、名前を付けてクラスを定義してコンストラクターなりでポインターを渡さなければならず、ここまで短くは書けない)

参考

なおラッチオブジェクトを使わずに、構造化プログラミング方式で書くと mWritable に入れる無名 MyBoolean オブジェクトは次のようなものになるだろう。

 mWritable = new MyBoolean() {
   private boolean initialized = false;
   private boolean writable;

   @Override
   boolean value() {
     if (!initialized) {
       writable = Heavy_WritableCheck();
       initialized = true;
     }
     return writable;
   }
 };

このオブジェクトを利用した場合、 isWritable() が呼ばれるたびに initialized 変数の確認が走ることになる。ラッチオブジェクトでは、 initialized 変数を確認するコストは latch 変数の先にぶらさがる MyBoolean オブジェクトの仮想関数テーブル走査(どの value() 関数を呼ぶか)に転換されている。

if 文はできるだけ使わずにメッセージの受け取り手に判断させるのがオブジェクト指向らしくて美しい …という理由でラッチオブジェクトのようなパターンは好きなんだけれど、しかし、リーダブルコードを読んだあとで考えると、構造化プログラミング方式のほうがわかりやすいよね。

とくに初回の value() 呼び出しのとき、 latch.value() で呼ばれたその先で、当の latch 変数を書き換えて、書き換えた latch の value() を return しているけど、呼び出し元に戻ったときにはいったいどうなっているの? とか、考え始めるとワクワクが止まらないよね!

なんともはや。