列挙型とポリモーフィズム
プラネタリウムを作ろう
ここでは例として、プラネタリウムを作ることを想定してみましょう。
ご覧のように、夜空にはいろいろな星が描かれています。月、木星、土星、彗星、赤い星、青い星、暗い星、星雲、銀河、…などが、それぞれ特徴的な姿で画面に表示されていますね。
では、このようなプラネタリウムの画面を描くには、どのようなプログラムを書けば良いでしょうか?
C言語なら列挙型を使ってこう書く
C言語では、いろいろなタイプの星を、列挙型(enum型)で表すことが多いです。たとえば、こんな列挙型を定義します。
typedef enum { StarType_Moon, /* 月 */ StarType_Jupiter, /* 木星 */ StarType_Saturn, /* 土星 */ StarType_Comet, /* 彗星 */ StarType_RedStar, /* 赤い星 */ StarType_BlueStar, /* 青い星 */ StarType_Faint, /* 暗い星 */ StarType_Nebula, /* 星雲 */ StarType_Galaxy /* 銀河 */ } StarType_En;
画面に星を描くコードは、列挙型を使って、次のような感じになるでしょう。
StarType_En eStarType = ○○; switch (eStarType) { case StarType_Moon: /* 月 */ DrawMoon(x, y); break; case StarType_Jupiter: /* 木星 */ DrawJupiter(x, y); break; case StarType_Saturn: /* 土星 */ DrawSaturn(x, y); break; case StarType_Comet: /* 彗星 */ DrawComet(x, y); break; : }
このように、たくさんのif文がネストしていたり、たくさんのcase文が並んだswitch文を書いたソースコードが、C言語では良く見られます。
しかし、このような書き方は、うまいやり方とは言えません。
Java言語ならポリモーフィズムを使ってこう書く
オブジェクト指向言語では、いろいろなタイプがあって、それぞれが違った動作をする場合は、ポリモーフィズムを使って表現します。ポリモーフィズムとは、多様性または多態性とも言われ、オブジェクト指向の大きな特徴の1つです。
オブジェクト指向言語であるJava言語では、前述のプラネタリウムの画面に星を描くコードは、次のように、とてもシンプルになります。
Star star = ○○; star.Draw(x, y);
ここで使っているStarクラスは、次のような抽象的なクラス(インターフェース)です。
public interface Star { public void Draw ( int x, int y ); }
Starクラスは、画面に星を描く、という機能を持っています。Drawというメソッドに位置を渡せば、その位置に星を描いてくれることになっています。但し、どのような絵を描くかは、Starクラスには実装されていません。
このStarクラスには、下記のようにたくさんのサブクラス(実装クラス)があり、それぞれ独自に、星を描く方法が実装されています。
// 月 public class Moon implements Star { public void Draw ( int x, int y ) { 月の絵を描く。 } } // 木星 public class Jupiter implements Star { public void Draw ( int x, int y ) { 木星の絵を描く。 } } // 土星 public class Saturn implements Star { public void Draw ( int x, int y ) { 土星の絵を描く。 } } :
ポリモーフィズムを使ったクラス図
C++言語ならどう書く?
C++言語は、C言語を拡張したオブジェクト指向言語です。そのため、C言語と同じく列挙型も使えますし、前述したポリモーフィズムの考え方も使えます。
列挙型とswitch文を使ったプログラムは、かんたんに書いて、動かすことができます。一方、ポリモーフィズムを使う方法は、プログラムを書き始める前にしっかり設計を考える必要があり、敷居が高いです。そのため、オブジェクト指向言語であるとはいえ、C++言語でプログラムを書くと、列挙型とswitch文で済ませてしまうことが多くあります。
C++言語で書いたプログラムで、列挙型やswitch文が多用されているとしたら、それは、オブジェクト指向的にうまく設計できていない証拠、と言えるかもしれません。
ポリモーフィズムとパワータイプ
ポリモーフィズムの欠点
ただ、ポリモーフィズムを使う方法には、クラスの数が爆発的に増える、という欠点があります。
列挙型を使ったプログラムをポリモーフィズムを使って書き換えると、enum文で定義されている列挙子の数だけ、サブクラスができることになります。何十個もの列挙子の1つ1つをクラスにしたのでは、クラスの管理が大変なことになります。
パワータイプという概念
ポリモーフィズムを使う方法では、いろいろなタイプの星を、クラスで表現しました。この他に、いろいろなタイプの星を、インスタンスで表現する方法もあります。これが、パワータイプ(型概念)という考え方です。
パワータイプ(型概念)はアナリシス・パターンの1つです。詳しくは、下記の文献が参考になります。
ゼロから始めるオブジェクト指向
第11回 概念モデルとしてのクラス図【その6】
矢崎博英
JavaWorld 2002 April p132
アナリシス・パターンとは、現実世界を分析してモデル化する際に適用されるデザイン・パターンです。その1つであるパワータイプも、本来は概念モデルを作る際のパターンですが、その考え方は、プログラムを設計する際にも参考にすることができます。
パワータイプでは、いろいろなタイプの星を、インスタンスで表現します。何十個もの列挙子が並んだenum文をパワータイプを使って書き換えても、クラスは1つしか増えません。enum文で定義されている列挙子の数だけ、サブクラスではなく、インスタンスができることになります。
パワータイプを使うならこう書く
前述のプラネタリウムの画面を描く例では、星のタイプごとに、Drawメソッドの中で絵を描いていました。しかし、プログラムの中で絵を描くのではなく、予め星のタイプごとに絵を描いておき、画像ファイルに保存しておく、というアイディアも考えられます。星のタイプごとに、適切な画像ファイルを選んで表示します。
星のタイプ | 画像ファイル | 画像ファイル名 |
---|---|---|
月 | Moon.gif | |
木星 | Jupiter.gif | |
土星 | Saturn.gif | |
彗星 | Comet.gif | |
赤い星 | RedStar.gif | |
青い星 | BlueStar.gif | |
暗い星 | Faint.gif | |
星雲 | Nebula.gif | |
銀河 | Galaxy.gif |
このケースでは、それぞれの星のタイプが、1つの画像ファイルとの関連を持つことになります。それぞれが異なった画像ファイルのオブジェクトと関連を持つことにより、星のタイプごとに違った絵が描かれる訳です。
このように、タイプの違いが、関連を持つオブジェクトの違いとして表される場合は、サブクラスによるポリモーフィズムよりも、パワータイプを使う方が適しています。
このケースでは、パワータイプを使って、星のタイプをインスタンスによって、次のように表現できます。
パワータイプを使ったクラス図
パワータイプを使ったオブジェクト図
パワータイプを使うと、前述のプラネタリウムの画面に星を描くコードは、次のようになります。
まず、星のタイプを表すインスタンスを生成し、画像ファイルのオブジェクトと関連づけます。
StarType star_type_moon = new StarType("Moon.gif"); StarType star_type_jupiter = new StarType("Jupiter.gif"); StarType star_type_saturn = new StarType("Saturn.gif"); StarType star_type_comet = new StarType("Comet.gif"); :
個々の星のインスタンスは、それぞれ、StarTypeインスタンスへの参照を1つ持つようにしておきます。
画面に星を描くコードは、次のようになります。
Star star = ○○; StarType star_type = star.getType(); Image image = star_type.getImage(); Draw(x, y, image);
オブジェクト指向言語における列挙型の意義
オブジェクト指向言語であっても、モデルによっては、サブクラスによるポリモーフィズムではなく、パワータイプを使った方が良いことが分かりました。
では、列挙型についてはどうでしょうか? オブジェクト指向言語であっても、列挙型を使った方が良いケースはあるのでしょうか?
オブジェクト指向言語における列挙型とは、パワータイプの概念を実装する手段である、と考えられると思います。設計フェーズで作ったモデルで、パワータイプの概念を使っている場合、それをソースコードで実現する方法として列挙型を使うことは、悪くありません。
特に、J2SE 5.0で導入された列挙型は、パワータイプを実現する有効な手段と言えます。
Java言語の列挙型
Typesafe Enumパターン
J2SE 1.4以前のJava言語では、列挙型を使うことはできませんでした。
しかし、オブジェクト指向言語で列挙型を実現するテクニックとして、Typesafe Enumパターンというデザインパターンが知られていました。
Typesafe Enumパターンとは、enum文を1つのクラスで表現する手法です。列挙子はそのクラスのインスタンスとして生成します。コンストラクタをprivateとすることにより、クラスの中に書かれている列挙子の他には、インスタンスを生成することはできません。
C言語の列挙型(enum型)は、実体はただの整数であり、int型との区別は曖昧でした。enum文で定義されていない値でも、enum型の変数に代入してしまうことも可能でした。Typesafe Enumパターンでは、そういった問題は起こりません。安全に使える列挙型として、C++言語でも利用されています。
ところで、列挙子をインスタンスとして表現する、というのは、パワータイプの考え方と同じです。実際、前述した、星のタイプをパワータイプを使って表現した例は、次のように、Typesafe Enumパターンを使って書くこともできます。
public class StarType { public final static StarType star_type_moon = new StarType("Moon.gif"); public final static StarType star_type_jupiter = new StarType("Jupiter.gif"); public final static StarType star_type_saturn = new StarType("Saturn.gif"); public final static StarType star_type_comet = new StarType("Comet.gif"); : private String image_filename; private StarType ( String image_filename ) { this.image_filename = image_filename; } }
J2SE 5.0で導入された列挙型
J2SE 5.0では、機能拡張の目玉の1つとして、Java言語にも列挙型が導入されました。
J2SE 5.0で導入された列挙型は、デザインパターンの名称と同じく、Typesafe Enumと呼ばれます。ソースコードの書き方は、次のように、一見するとC言語の列挙型(enum型)と同じに見えます。
public enum StarType { Moon, /* 月 */ Jupiter, /* 木星 */ Saturn, /* 土星 */ Comet, /* 彗星 */ RedStar, /* 赤い星 */ BlueStar, /* 青い星 */ Faint, /* 暗い星 */ Nebula, /* 星雲 */ Galaxy /* 銀河 */ }
J2SE 5.0の列挙型は、同じ名称のデザインパターンであるTypesafe Enumパターンと同様、型の安全性が保証されます。これが、C言語の列挙型(enum型)と異なる点です。
一方、C言語の列挙型(enum型)と同様に、switch文でも使うことができます。これは、Typesafe Enumパターンとは異なる点です。
また、C言語の列挙型(enum型)とも、Typesafe Enumパターンとも、どちらとも大きく異なる特徴として、ポリモーフィズムに対応している、という点が挙げられます。
列挙型によるポリモーフィズム
冒頭のプラネタリウムの画面を描く例では、たくさんのサブクラスを作って、サブクラスごとにDrawメソッドの実装を変えることで、星のタイプごとに違った絵を描くポリモーフィズムを実現していました。
しかし、J2SE 5.0で導入された列挙型を使うと、このようなケースでも、サブクラスを増やさずにポリモーフィズムを実現することができます。具体的には、次のようになります。
public abstract enum StarType { public abstract void Draw ( int x, int y ); Moon { /* 月 */ public void Draw ( int x, int y ) { 月の絵を描く。 } }, Jupiter { /* 木星 */ public void Draw ( int x, int y ) { 木星の絵を描く。 } }, Saturn { /* 土星 */ public void Draw ( int x, int y ) { 土星の絵を描く。 } }, : }
画面に星を描くコードは、次のようになります。
Star star = ○○; StarType star_type = star.getType(); star_type.Draw(x, y);
Java言語の列挙型の真価
前述したように、オブジェクト指向言語における列挙型の意義は、パワータイプの概念を実装する手段である、と考えています。
従来は、タイプごとに異なる動作を実装するためには、状況によらず、サブクラスによるポリモーフィズムを使って実現するしか、方法がありませんでした。J2SE 5.0で導入された列挙型は、ポリモーフィズムに対応することによって、パワータイプの考え方を導入して作った概念モデルをソースコードで実装する際の、選択の幅を広げることになったと思います。
しかし、J2SE 5.0で導入された列挙型には、ともすれば、多様な処理を1ヶ所にまとめて記述してしまうことになりやすい、という危険性もあります。
いろいろなタイプによる動作の違いを表すには、サブクラスによるポリモーフィズムを使った方が良い場合も多々あります。そのようなケースで、J2SE 5.0で導入された列挙型を使ってしまうと、C言語でenum型とswitch文を多用した場合と、結果的に同じ弊害が出てしまうことでしょう。
C言語に慣れ親しんだ人は、Java言語に列挙型が導入されたのと良いことに、C言語と同じ感覚で列挙型を使うようになるかもしれません。ですが、列挙型を、ただの便利なコーディング手法の1つだと思わずに、その真価を理解して使うことが重要だと思います。
おまけ
意外とまともなC++言語のenum型
C言語のenum型は、単に整数に皮を被せただけのもので、int型との区別は曖昧でした。
実際、次のような意味不明なコードですら、コンパイルが通ってしまいます。
typedef enum { APPLE, ORANGE, BANANA } FRUIT; int main ( ) { FRUIT e; int m, n; e = 10; m = ORANGE; n = e + m; printf("%d\n", n); return n; }
このいい加減なenum型は、C言語を拡張したC++言語でも利用できます。
ところが、C++言語でのenum型の扱いは、C言語とは異なり、ずいぶんまともになっています。
服部健太氏から、次のような指摘を頂きました。
最低限、型チェックはしてくれるみたいですね。
typedef enum { FOO, BAR, BAZ } hoge_t; typedef enum { APPLE, ORANGE, BANANA } fruit_t; int main(void) { fruit_t fruit; hoge_t hoge; int n; hoge = FOO; // OK fruit = APPLE; // OK hoge = fruit; // type error hoge = APPLE; // type error fruit = BAZ; // type error fruit = 5; // type error n = fruit; // OK return n; }g++では、上記のように型エラーを検出してくれたんですが、gccだと、すんなり通ってしまいました。
C言語ではコンパイルが通ってしまいますが、同じコードをC++言語として書くと、コンパイラによって適切にエラーと判断されました。
また、enum型を例外として投げた場合も、int型とは明確に区別されました。例えば、下記のサンプルコードを実行すると、正しく「fruit!」と出力されました。
typedef enum { APPLE, ORANGE, BANANA } FRUIT; typedef enum { TOMATO, CUCUMBER, ONION } VEGETABLE; main ( ) { int a = 0; try { if (a == 0) { throw APPLE; } else { throw TOMATO; } } catch (int e) { puts("int..."); } catch (VEGETABLE e) { puts("vegetable..."); } catch (FRUIT e) { puts("fruit!"); } }