JavaScript でプログラミングをするときに、オブジェクト指向の手法を使うのであれば、折角なのでデザインパターンを利用したいと思います。しかし、デザインパターンの本は、Java を対象に書かれているものが多く、単純には JavaScript へ応用できません。
僕はデザインパターンを覚えたてですし、JavaScript も修行中の身なので、分らないことだらけです。そこで、このブログを使って、みなさんと議論したいと思います。気軽にコメントを書いて下さい。
Java と JavaScript
デザインパターンを考える上で障壁となる Java と JavaScript の違いをまとめてみましょう。
機能 | Java | JavaScript |
変数の型 | ある | ない |
オブジェクトの型決め | 宣言的 | ダック・タイピング |
オブジェクトの性質 | 静的 | 動的 |
抽象クラス | ある | ない |
関数を引数として渡せるか | 渡せない | 渡せる |
オブジェクトのリテラル | ない | ある |
また、次のことも重要かもしれません。
- Java でクラスを拡張するときは、継承やインターフェイスを使う
- JavaScript でクラスを拡張するときは、継承も使えるが、Mixin も使える
抽象クラス
Java のデザインパターンには、よく抽象クラスが出てきます。たとえば、こんな感じです。
public abstract class Shape { public abstract void draw(); } public class Triangle extends Shape { public void draw() { } } public class Rectangle extends Shape { public void draw() { } }
クライアントは、型 Shape を使うことで、その実体が Triangle なのか Rectangle なのか気にしなくてよくなります。このように、継承の大切な役割の一つは、似通ったクラスをまとめて汎用的なクラスを作ることです。これにより、Java では具体的なクラスを抽象的なクラスにカプセル化します。
JavaScript には、抽象クラスがありません。どうしたらいいでしょうか?
そもそも、Java に抽象クラスがあるのは、変数に型があるためです。Triangle も Rectangle も格納できる変数を用意するためには、それらを抽象化する型が必要なのです。
JavaScript の変数には型はありませんから、抽象クラスは不要です。以下のように具象クラスだけ書けばいいでしょう。(prototype.js を使うことを前提としています。)
var Triangle = Class.create({ draw: function() { } }); var Rectangle = Class.create({ draw: function() { } });
ダック・タイピング
名著「プログラミング Ruby」の著者 Dave Thomas さんは、次に示す英語の格言を使って Ruby の型付けを説明しました。
If it walks like a duck and quacks like a duck, it must be a duck.
ダック・タイピングとは、「あるオブジェクトが、ある型に必要とされるインターフェイスを実装しているなら、そのオブジェクトはその型に属す」という型付けです。
JavaScript の型付けもダック・タイピングです。上記の例で言えば、「Shape のように draw するなら、それは Shape 」なのです。
実は、Java の interface は静的なダック・タイピングだと言えます。どのインターフェイスを実装するのか、あらかじめ宣言されているところが違うだけなのです。
オブジェクト指向では、オブジェクトの実装ではなく、インターフェイスに対してプログラミングすることが重要です。あるオブジェクトにメッセージを送って仕事をしてもらうのです。これによって、(内部のデータ構造などの)実装をカプセル化できます。
「やれるところは自分でやる」のが構造化プログラミングだとするなら、「任せられるところは任せる」のがオブジェクト指向プログラミングでしょう。この考えは、デザインパターンでも重要です。
蛇足ですが、JavaScript ではインスタンス関数とインスタンス変数の境界が曖昧です。インスタンス変数がインスタンス関数のように振る舞うことがあります。代入すると、動作するのです。たとえば、DOM のオブジェクトは、インスタンス変数(プロパティ)に代入することで、位置や色などを変化させることができます。
set/get を実装しているブラウザでは、インスタンス関数っぽいインスタンス変数を作成できます。ですから、JavaScript では、インスタンス関数を直接操作しても、実装に対してプログラミングしているのではなく、インターフェイスに対してプログラミングしていると言えることもあります。
継承と Mixin
継承の重要な役割の一つに、実装の共通化があります。次の例を見て下さい。
public abstract class OutputDevice { public final void output() { open(); write(); close(); } public abstract void open(); public abstract void write(); public abstract void close(); } public class Screen extends OutputDevice { public void open() { } public void write() { } public void close() { } } public class Printer extends OutputDevice { public void open() { } public void write() { } public void close() { } }
OutputDevice の output が、Screen と Printer の共通の実装となっています。Printer オブジェクトに output を頼むと、親のクラスである OutputDevice の output が呼ばれます。その中では、Printer の open/write/close が呼ばれます。
output のような実装を共通化するパターンは Template Method と呼ばれています。僕が感激したデザインパターンの一つです。
JavaScript では、Java と同様に継承を使って、実装を共通化できます。一方で、Mixin を使う方法でも実現できます。Mixin とは、オブジェクトを拡張するためのオブジェクトです。
Mixin という考え方は、Flavors で導入されました。名前は、マサチューセッツにあるアイスクリーム屋の商品に由来するようです。アイスクリームの味に変化を付けるトッピングが、Mixin という名前だったそうです。
上記のコードを Mixin を使い JavaScript で書き直すと以下のようになります。
var Mixin = { output: function() { this.open(); this.write(); this.close(); } }; var Screen = Class.create(Mixin, { open: function() { }, write: function() { }, close: function() { } }); var Printer = Class.create(Mixin, { open: function() { }, write: function() { }, close: function() { } });
注意:Class.create の Mixin の部分がクラスであれば、継承になります。
実装の共通化のために、継承を使うべきか、Mixin を使うべきか、まだ僕は結論を出せていません。
さて次は?
次は、Singleton パターンを考えたいと思っています。