Java言語の設計は、その前身で指摘された問題のいくつかを回避するために、意図的に特定のカットを行いました。たとえば、Java言語の設計者は、C++の多重継承は複雑さをもたらしすぎると考え、その機能を含めないことにしました。実際、C++言語には拡張性のオプションはほとんど組み込まれておらず、単一継承とインターフェイスのみに依存しています。
他の言語には、拡張の大きな可能性があります。今回と次回の2回にわたって、継承を伴わずにJavaクラスを拡張する方法を探ります。この記事では、既存のクラスにメソッドを追加する方法について学びます。
表現の問題
式問題は、ベル研究所のPhilip Wadlerによる未発表の論文で最初に作成された、最近のコンピュータサイエンスの歴史ではよく知られた観察です。(Stuart SierraはdeveloperWorksの記事 "Solving the Expression Problem with Clojure 1.2 "でそれを説明する素晴らしい仕事をしています)。この記事でWadlerはこう言っています:
式問題は古い問題の新しい名前です。既存のコードを再コンパイルすることなく、静的な型安全性を維持したまま、データ型に新しいケースを追加したり、データ型の新しい関数を追加したりすることができます。
言い換えれば、型変換やif文に頼らずに、階層構造のクラスに機能を追加するにはどうすればいいのでしょうか?
簡単な例を使って、式の問題が現実の世界でどのように現れるかを示します。あなたの会社が、アプリケーションの中で常に長さの単位を仮定し、それ以外の長さの単位に対応する機能をクラスの中に持たないとします。しかしある日、あなたの会社は、常に長さの単位を仮定している競合他社と合併しました。
この問題を解決する1つの方法は、Integerを変換メソッドで拡張することで、2つのフォーマット間の切り替えを不要にすることです。最近の言語では、この目的のためにさまざまなソリューションが提供されています:
- オープン・クラス
- ラッパークラス
- プロトコル
GroovyのクラスとExpandoMetaClass
Groovyには、既存のクラスを拡張する2つの異なる方法があり、変更を実装するためにクラス定義を "reopen "する機能を使用します。
カテゴリー
カテゴリー・クラスは、静的メソッドを含む通常のクラスです。各メソッドは、メソッド拡張の型を示すパラメーターを少なくとも1つ受け入れます。例えばIntegerにメソッドを追加したい場合、リスト1に示すように、最初の引数としてその型を受け入れる静的メソッドが必要になります:
リスト1.Groovyのカテゴリクラス
class IntegerConv {
static Double getAsMeters(Integer self) {
self * 0.30480
}
static Double getAsFeet(Integer self) {
self * 3.2808
}
}
リスト1のIntegerConvクラスには2つの拡張メソッドがあり、それぞれがselfという名前のIntegerパラメータを受け入れます。これらのメソッドを使用するには、リスト 2 に示すように、参照コードを use ブロックでラップする必要がありました:
リスト2
@Test void test_conversion_with_category() {
use(IntegerConv) {
assertEquals(1 * 3.2808, 1.asFeet, 0.1)
assertEquals(1 * 0.30480, 1.asMeters, 0.1)
}
}
リスト2には、特に興味深い点が2つあります。1つ目は、リスト1の拡張メソッドがgetAsMeters()と呼ばれているにもかかわらず、1.asMetersと呼んでいることです。 GroovyのJavaのプロパティを取り巻く構文糖によって、asMetersというクラスのフィールドであるかのようにgetAsMeters()メソッドを実行することができます。拡張メソッドからasを省略した場合、拡張メソッドへの呼び出しは1.asMeters()のように空の括弧を使用する必要があります。一般的に、私はよりすっきりしたプロパティ構文を好みます。これはドメイン固有言語を記述する際の一般的な手法です。
リスト2で注目すべき2つ目の点は、asFeetとasMetersの呼び出しです。useブロックでは、新しいメソッドと組み込みメソッドを同じように呼び出しています。拡張は use ブロックのレキシカルスコープ内で透過的に行われます。
ExpandoMetaClass
カテゴリーはGroovyに追加された最初の拡張メカニズムでした。しかし、Groovyの語彙スコープはGrailsを構築するには制限が多すぎることが判明しました。Grailsの開発者の一人であるGraeme Rocherは、カテゴリの制限に不満を持ち、Groovyに別の拡張機構であるExpandoMetaClassを追加しました。
リスト3 ExpandoMetaClassによるIntegerの拡張
class IntegerConvTest{
static {
Integer.metaClass.getAsM { ->
delegate * 0.30480
}
Integer.metaClass.getAsFt { ->
delegate * 3.2808
}
}
@Test void conversion_with_expando() {
assertTrue 1.asM == 0.30480
assertTrue 1.asFt == 3.2808
}
}
リスト 3 では、metaClass ホルダーを使用して、リスト 2 と同じ命名規則を使用して asM 属性と asFt 属性を追加しています。メタクラスの呼び出しは、テスト・クラスの静的イニシャライザーの中に現れます。
カテゴリクラスとExpandoMetaClassの両方は、組み込みメソッドで拡張クラスのメソッドを呼び出します。これにより、既存のメソッドを追加、変更、または削除できます。リスト4に例を示します:
リスト4.既存のメソッドを置き換える拡張クラス
@Test void expando_order() {
try {
1.decode()
} catch(NullPointerException ex) {
println("can't decode with no parameters")
}
Integer.metaClass.decode { ->
delegate * Math.PI;
}
assertEquals(1.decode(), Math.PI, 0.1)
}
リスト4の最初のdecode()メソッド呼び出しは、整数のエンコーディングを変更するために設計された組み込みの静的Groovyメソッドです。 NullPointerException
通常、このメソッドは引数を1つ取ります。引数なしで呼び出された場合は、.NET Frameworkをスローします。しかし、Integerクラスを独自のdecode()メソッドで拡張すると、元のクラスが置き換わります。
Scalaにおける暗黙の変換
Scalaはこの式の問題を解決するために使います。クラスにメソッドを追加するには、ヘルパークラスにメソッドを追加し、元のクラスからヘルパーにメソッドを提供します。変換を実行した後、元のクラスからメソッドを呼び出す代わりに、ヘルパーから暗黙的にメソッドを呼び出すことができます。リスト5の例はこのテクニックを使っています:
#p#
リスト5 Scalaの暗黙的変換
class UnitWrapper(i: Int) {
def asFt = {
i * 3.2808
}
def asM = {
i * 0.30480
}
}
implicit def unitWrapper(i:Int) = new UnitWrapper(i)
println("1 foot = " + 1.asM + " meters");
println("1 meter = " + 1.asFt + "foot")
リスト 5 では、コンストラクターのパラメーターと、asFt と asM という 2 つのメソッドを受け付ける UnitWrapper というヘルパー・クラスを定義しています。変換後の値をヘルパー・クラスに持たせた後、新しい UnitWrapper をインスタンス化する暗黙の def を作成します。Scalaは、IntegerクラスにasMメソッドが見つからない場合、暗黙的な変換をチェックし、呼び出し元のクラスがターゲット・メソッドを含むクラスに変換されるようにします。Groovy と同様、Scala にも構文糖があるので、メソッド呼び出しから括弧を省略することができましたが、これは言語機能であって命名規則ではありません。
Scalaの変換ヘルパーは通常クラスではなくオブジェクトですが、コンストラクタのパラメータとして値を渡したかったのでクラスを使いました。
Scalaの暗黙的変換は、既存のクラスを拡張するための繊細で型安全な方法ですが、オープンクラスと同じように既存のメソッドを変更したり削除したりするためにこのメカニズムを使用することはできません。
Clojureのプロトコル
Clojureは、extend関数とClojure抽象化の組み合わせを使用することで、式の問題のこの側面に対して異なるアプローチを取ります。プロトコルは、概念的にはJavaインタフェースに似ています: 実装のないメソッド・シグネチャのコレクションです。Clojureは本質的にオブジェクト指向ではなく、関数を好みますが、クラスと対話し、メソッドを関数にマップすることができます。
リスト6: Clojureの拡張プロトコル
(defprotocol UnitConversions
(asF [this])
(asM [this]))
(extend Number
UnitConversions
{:asF (fn [this] (* this 3.2808))
:asM #(* % 0.30480)})
Clojure REPLで新しい拡張機能を使用して変換を検証できます:
user=> (println "1 foot is " (asM 1) " meters")
1 foot is 0.3048 meters
リスト6では、変換関数の2つの実装が、無名関数宣言の2つの構文バリエーションを示しています。各関数は引数を1つだけ取ります。単一引数関数は非常に一般的なので、Clojureは、% が引数のプレースホルダーであるAsM関数で示されているように、それらの作成のための構文上の糖を提供します。
プロトコルは、既存のクラスにメソッドを追加するための簡単なソリューションを作成します。Clojureには、拡張のセットをまとめることを可能にするいくつかの便利なマクロも含まれています。例えば、Compojure Webフレームワークは、プロトコルを使用して型を拡張し、型自身がレンダリングする方法を「知っている」ようにします。リスト7は、CompojureのRenderableのコードのスニペットです:
リスト7.合意による多くの型の拡張
(defprotocol Renderable
(render [this request]
"Render the object into a form suitable for the given request map."))
(extend-protocol Renderable
nil
(render [_ _] nil)
String
(render [body _]
(-> (response body)
(content-type "text/html; charset=utf-8")))
APersistentMap
(render [resp-map _]
(merge (with-meta (response "") (meta resp-map))
resp-map))
IFn
(render [func request]
(render (func request)
; . . .
リスト7では、Clojureのextend-protocolマクロは型と実装のペアを受け入れます。Clojureでは、気にしない引数の代わりにアンダースコアを使用できます。リスト7では、この定義の可視部分は、nil、String、APersistentMap、およびIFnのレンダリングディレクティブを提供します。レンダリングする必要があるすべての型について、セマンティクスと拡張を一緒に定義できます。
結語
今回は、Javaの次世代言語が既存クラスのクリーンな拡張にどのように対処しているかを解剖しながら、表現問題を紹介します。各言語は、同じような結果を得るために異なるテクニックを使っています。
しかし、式の問題は型の拡張よりも深いのです。次回は、他のプロトコルの関数や機能、ミックスインを使用する拡張について引き続き説明します。