Effective Javaを読むチャレンジ-項目21-

項目21 戦略を表現するために関数オブジェクトを使用する

関数ポインタ、委譲、ラムダ式

プログラミング言語の中にはプログラムが関数を保存して、その関数を呼び出すように受け渡すことができるものがある、という話です。その方法の一端がこの項目の文頭に出てくるこれらの単語です。

C言語などでは関数がアドレスを持っていて、そのアドレスへのポインタ(関数ポインタ)を参照して、関数を実行できます。関数呼び出しで関数を渡す、などといったことができます。委譲というのはC++のデリゲートのようなものでしょうか。「SampleDelegate sd = samplecalss.method1;」のように、メソッドを代入できる型です。ラムダ式はC++とかHaskellが有名?かどうか分かりませんが、関数を引数として渡したりする箇所に無名関数を記述するための記法です。

Java SE 8以降、Javaでも関数型インタフェースやStream API、ラムダ式、デフォルトメソッドなどを導入して関数を独立したオブジェクトのように扱えるようになりました。流れとしてはパラレル処理高速化のためにイテレータを使おうとして、内部イテレータならいけるかも?(Stream APIにつながる)となって、ただ無名関数の記述が面倒だね、それならラムダ式使えばいいよ(それでラムダ式のために関数型インタフェースができた)、という感じでしょうか。

Java SE 8については勉強しなきゃですねえ・・・。

それはともかく、本書ではC言語のqsort関数を例に、関数を受け渡す例を示しています。C言語のqsort関数はこんな感じ。

qsort関数はユーザが定義した比較を行う関数を使って、データのクイックソートを実行する関数です。第1引数がデータ、第2引数がデータの個数、第3引数がデータ1つのサイズ、そして最後の引数が比較を行う関数です。関数ポインタ「*compare」の後の()の中身は、比較を行う関数の仮引数です。

この比較のための関数は、中身のアルゴリズムはユーザに任せていますが、戻り値の結果パターンが決められています。

  • ソート結果のデータについて、第1引数が第2引数より先に並ぶ場合は負の値
  • ソート結果のデータについて、どちらが先に並んでもいい場合は0
  • ソート結果のデータについて、第1引数が第2引数より後に並ぶ場合は正の値

このパターンに沿っていれば、どんな方法で比較をしても期待どおりのソート結果になります。この引数と戻り値が決められているが中身のアルゴリズムは好きに定義していいという考え方は、JavaではStrategyパターンとして広まっています。Strategyパターンは、例えばソートのアルゴリズムという戦略(Strategy)を、クライアントと独立させて変更できるようにするデザインパターンです。関数ポインタをJavaで実現するにはStrategyパターンを使う、というのがこの項目の結論です。委譲とラムダ式については言語仕様から変えないとどうしようもないので、本書では冒頭以降、特に触れてません。Java SE 8以降ではどちらも実現できます。本書では、その関数ポインタをJavaで実現するためのStrategyパターンのサンプルと解説が続きます。

まず、本書ではこういった特徴を持つクラスを実質的にはメソッドへのポインタとみなせると言っています。

  • あるオブジェクトのメソッド呼び出しが、そのオブジェクトではなく、他のオブジェクトに対して操作を行う
  • 他のオブジェクトがメソッドの引数として明示的に渡される
  • 上記のメソッドを1つだけ公開する

こういったクラスを具象戦略(Concrete strategy)と言っています。また、典型的な具象戦略は状態がない、つまり、フィールドを持っていないという特徴も持っています。その場合はSingletonでのインスタンス生成が適しています。

Strategyパターン

Strategyパターンのポイントは、「ユーザがアルゴリズムを交換できること」です。そのため、それぞれの具象戦略を、共通のインタフェースを実装することで交換可能にします。戻り値と引数がおなじということは、そのままインタフェースの使い方に通じますね。各具象戦略については前述の特徴を兼ね備えていることがポイントです。

それではStrategyパターンのサンプルです。本書のサンプルを使用しています。

まずは共通のインタフェースを定義します。本書ではjava.util.Comparatorインタフェースを使用していますが、もちろん独自のインタフェースでもいいです。本書ではこの戦略を交換するためのインタフェースのことを戦略インタフェース(strategy interface)と呼んでいます。

次に具象戦略です。具象戦略は先ほどの戦略インタフェースを実装します。

実行する場合はこうなります。

ここで、具象戦略が1つしかない場合は、戦略インタフェースを用意せず、無名クラスを使ってこう書くこともできるそうです。

これはさきほどの結果と同じになりますが、この書き方では、この処理が呼ばれるたびにインスタンスを生成していることに注意です。これを避けるためには、以下のようにprivate static finalのフィールドに保存しておきます。

この戦略インタフェースのインスタンスをpublic static finalフィールドで公開することで、具象戦略の実装部分を隠ぺいできます。また、上記のパターンではSerializableなど具象戦略に複数のインタフェースを実装することができません。その場合は以下のようにします。

この方法はjava.lang.Stringクラスの大文字小文字を区別しない文字列の比較を行うComparatorを提供している「CASE_INSENSITIVE_ORDER」フィールドに使われています。

本書のStrategyパターンとGoFのStrategyパターンとはまたちょっと実装方法が違うのですが、目的は同じです。最近ではGoFのStrategyパターンをラムダ式を使って書き直すとか、GoFパターンが現在のJavaでどう生きているか調べたとか、興味深いブログが多いですね。本サイトでもいずれやってみたいですが、そういうレベルに自分が達しているかと言われると・・・随分長くなりましたが、この項目は以上で終了です。

広告
  • LINEで送る