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

項目34 拡張可能なenumをインタフェースで模倣する

項目30の説明で、現在のJavaのenumはEffective Java 第1版で紹介されたタイプセーフenumパターンを言語でサポートしたものだと書きました。Javaのenumは、そのタイプセーフenumパターンよりもあらゆる場面で優れていると筆者は述べています。ただしJavaのenumは拡張できないという例外があるけど、ということだそうです。

それにenumが拡張できないようになっているのは、enumに拡張性を持たせることが間違った考えだからね、と続きます。そのenumが拡張可能だった場合の問題点ですが、その問題について考えるのは、私には難しいと感じました。

まず「拡張された型の要素が基底型の要素であり、基底型の要素が拡張された型の要素でないことは混乱する」とあります。

もしenumが拡張できるとすると、上記のようなコードが書けます。本来、enumは実質的なfinalクラスで拡張できませんが、その制約がなくなっていて拡張可能となっているとします。

このとき、例えばWeekend.SATURDAYがWeekday列挙の要素であり、Weekday.MONDAYがWeekend列挙の要素でないことが混乱するということですが・・・どういう意味で混乱するんでしょうか。単純な理由なのか複雑な理由なのか・・・。

次に「ベース型とその拡張された型のすべての要素を列挙する手っ取り早い方法はない」とあります。enumのvaluesメソッドが使えない、あるいは使えても面倒な手順が必要になるという感じでしょうか。

最後の「拡張性は、設計と実装の多くの局面を複雑にする」という部分は、最初は本書の項目16の話かなと思いましたが、特に参照も振っていないので、そのあたりも含めた一般論なのでしょうか。

こういった感じで話の導入部分でつまづきまして必要以上に時間が・・・しかも結論出ず。とりあえず現状、enumは拡張できない、ということだけ覚えて先を読みたいと思います。

拡張可能な列挙型を使わざるを得ない場合が少なくとも1つあり、それはオペコードとして知られているオペレーションコードである

オペレーションコードは何らかのマシンに対する操作を表す要素を持つ列挙として項目30で取り上げられていますが、時にはAPIのユーザにユーザ固有の操作を提供できるようにすることが望ましく、それは実質的に、APIが提供している操作の集合を拡張させることになるということです。

enumは拡張できませんが、enumが任意のインタフェースを実装できるという事実を利用すると、それに近い動きが実現できます。

リファクタリングにおけるインタフェースの抽出というヤツですね。つまり、項目30のOperation列挙からインタフェースを抽出することから始めます。まずは項目30のOperation列挙です。

ここからインタフェースとして抽出するのはapplyメソッドです。本書のとおりにすると、抽出したインタフェース名はOperation、元のOperation列挙はBasicOperation列挙としています。

ここからがテクニック。enumを拡張するのではなく、抽出したインタフェースを実装する、拡張したい機能を要素とした別のenumを作るのです。

ExtendedOperation列挙はBasicOpearation列挙から、要素を変えただけの列挙です。元のenumからインタフェースを抽出して基本のenumを作り、新しい機能を持ったenumを基本のenumから模倣しながら作成しています。

ここまででお分かりと思いますが、このパターンの欠点は、基本のenumと新しいenumとの間で実装を継承できないことです。どうしてもコードが重複するかと思いますが、その場合はヘルパークラスかstaticなヘルパーメソッドなどを作って実装を隠蔽(本書でいうところのカプセル化)を行います。

次は、このインタフェースをユーザがどう使うかということを見てみます。とりあえず各列挙の中身を表示します。

それはそうですね、それぞれ別のenumなので、valuesメソッドを使うとそれぞれの要素の配列だけを返します。次に、それぞれの要素がOperationインタフェースを使うことで交換可能になるかどうか確かめます。

これでインタフェースを使って基本のenumと新しいenumが交換可能であると言えます。しかし、これではOperationインタフェースを実装したenumでない具象クラスとも交換可能になります。

仮にこのとき、Operationインタフェースを実装したenumだけを利用できるようにしたいとなった場合(EnumSetやEnumMapを使いたい場合など)、本書のように境界型トークンを利用してOperationインタフェースを実装したenumだけを許可したクラスリテラルを渡すメソッドを作成すると型の保証が確実です。そのやり方は以下となります。

「<T extends Enum<T> & Operation>」の&は、論理積です。EnumかつOperationのサブタイプであることを表します。

これがenumでない具象クラスとも交換可能として柔軟性を持たせたいとした場合には、境界型トークンではなく、境界ワイルドカード型を使った方法も利用できます。

インタフェースを使ってenumを疑似的に拡張すれば、上記のtestメソッドのように、基本のenumを使ったAPIを提供することで、クライアントはインタフェースを実装した新しいenumを作成し、そのAPIをそのまま利用することができるようになります。

最初の一段落で5日ぐらい悩んでしまいましたが、どうにかenumは終わり、次はアノテーションです。

広告
  • LINEで送る