The Little Book on CoffeeScript

クラス

JavaScriptのクラスは純粋主義者にとってニンニクがドラキュラに対して与えるものと同様な影響を持っていました。とはいえ、正直に言ってあなたがそのような傾倒をお持ちでしたらCoffeeScriptの本を呼んだりはしないでしょう。しかし、クラスは他の言語と同じようにJavaScriptでもとても便利であると判明しました。そしてCoffeeScriptは素晴しい抽象化を提供します。

その裏では、CoffeeScriptはJavaScriptのネイティブなprototypeをクラスを作成するのに用いています。少しの構文糖を静的なプロパティの継承とコンテキストの永続化のために用いています。開発者としてのあなたに見えるのはclassキーワードだけです。

class Animal

この上の例ではAnimalがクラス名であり、かつ結果としてインスタンスの作成に用いる変数です。裏側ではCoffeeScriptはコンストラクタ関数を用いており、クラスのインスタンスをnew演算子を用いて作成することが可能です。

animal = new Animal

コンストラクタ(インスタンス化において実行される関数)を定義することは簡単です。constructorという名前の関数を使用してください。これはRubyのinitializeやPythonの__init__と同様です。

class Animal
  constructor: (name) ->
    @name = name

実はCoffeeScriptはインスタンスのプロパティ設定の一般的なパターンのために速記法を用意しています。引数の直前に@を置くことで、CoffeeScriptはコンストラクタの中では自動的に引数をインスタンスのプロパティに設定します。実際にはこの速記法はクラスの外の通常の関数でも使用可能です。下の例は先程の例と等価です。先程はインスタンスのプロパティを手で設定しています。

class Animal
  constructor: (@name) ->

期待どおりに、インスタンス化において渡される任意数の引数はコンストラクタ関数に渡されます。

animal = new Animal("Parrot")
alert "Animal is a #{animal.name}"

インスタンスプロパティ

クラスにインスタンスプロパティを追加することはとても簡単です。オブジェクトにプロパティを追加する文法そのものです。ただプロパティがクラスの本体で正しくインデントされているかは注意してください。

class Animal
  price: 5

  sell: (customer) ->

animal = new Animal
animal.sell(new Customer)

JavaScriptにおけるコンテキストの変更は嫌なものです。以前の文法の章でCoffeeScriptがファットアロー=>を用いることでthisの値を特有のコンテキストにロックすることを説明しました。関数がどのようなコンテキストの下に呼ばれようとも、関数が作成されたコンテキストの下で実行されます。つまりファットアローをインスタンスメソッドに用いることで、それが正しいコンテキストで実行されることを確認することができます。thisは常に現在のインスタンスになります。

class Animal
  price: 5

  sell: =>
    alert "#{@price}シリングになります!"

animal = new Animal
$("#sell").click(animal.sell)

上の例でデモしたとおり、これはイベントコールバックにおいてとても便利です。通常はsell()関数は#sell要素の下で実行されます。しかしsell()に対しファットアローを用いることで正しいコンテキストが管理されていることを確認できます。this.price5に等しくなります。

静的なプロパティ

クラスの(つまり静的な)プロパティはどうやって定義するのでしょうか?実はクラス定義の中ではthisはクラスオブジェクトを指しています。つまりクラスプロパティはthisの上に直接設定することになります。

class Animal
  this.find = (name) ->      

Animal.find("Parrot")

実際には、あなたも覚えておいででしょうが、CoffeeScriptにはthis@で記述可能で、静的プロパティをより簡潔に記述可能です。

class Animal
  @find: (name) ->

Animal.find("Parrot")

継承とsuper

何らかの形の継承が無ければ正しいクラスの実装にはならないでしょう。CoffeeScriptは失望させません。extendsキーワードを用いて他のクラスを継承することが可能です。下の例ではParrotAnimalを拡張し、全てのインスタンスプロパティ、alive()等を継承しています。

class Animal
  constructor: (@name) ->

  alive: ->
    false

class Parrot extends Animal
  constructor: ->
    super("Parrot")

  dead: ->
    not @alive()

上の例でお気付きになられるでしょうが、ここではsuper()キーワードを使いました。裏側ではクラスの親のprototypeの関数呼出に翻訳され、現在のコンテキストにて実行されます。この例ではParrot.__super__.constructor.call(this, "Parrot");になります。実際にこれはRubyやPythonでsuperを実行したのと同じ影響を持ち、オーバーライドされた関数を実行します。

constructorをオーバーライドしない限り、デフォルトでCoffeeScriptは親のコンストラクタをインスタンスが作成された時に実行します。

CoffeeScriptはprotype形式の継承を用いて、自動的にクラスの全てのインスタンスプロパティを継承します。これはクラスが動的であることを示します。子が作成された後に親クラスにインスタンスプロパティを追加すればそのプロパティは全ての継承した子に伝播します。

class Animal
  constructor: (@name) ->

class Parrot extends Animal

Animal::rip = true

parrot = new Parrot("Macaw")
alert("This parrot is no more") if parrot.rip

しかし静的なプロパティはサブクラスにコピーされるのであり、インスタンスプロパティのようにプロトタイプを用いて継承されるのではないことは重要です。これはJavaScriptのプロトタイプアーキテクチャの実装の詳細によります。そして次善の策を探すのは難しい問題です。

ミックスイン

ミックスインMixinsはCoffeeScriptの言語仕様としてサポートされたものではありません。しかし簡単に実装可能です。例として、2つの関数を考えます。extend()include()はあるクラスにクラスとインスタンスのプロパティを別々に追加します。

extend = (obj, mixin) ->
  obj[name] = method for name, method of mixin        
  obj

include = (klass, mixin) ->
  extend klass.prototype, mixin

# Usage
include Parrot,
  isDeceased: true

(new Parrot).isDeceased

ミックスインは継承が適切でない場合にモジュール間で共通なロジックを共有するのにとても良いパターンです。ミックスインの利点は継承が1つのクラスからしか継承できないのに対し、複数を取り込めることです。

クラスの拡張

ミックスインはとても格好良いです。しかしあまりオブジェクト指向ではありません。代わりとしてCoffeeScriptのクラスにミックスインを統合しましょう。Moduleと呼ばれるクラスを定義してミックスインサポートのために継承できるようにしましょう。Moduleは2つの静的関数を持ちます。@extend()@include()で静的プロパティとインスタンスプロパティを個別に拡張するのに利用可能です。

moduleKeywords = ['extended', 'included']

class Module
  @extend: (obj) ->
    for key, value of obj when key not in moduleKeywords
      @[key] = value

    obj.extended?.apply(@)
    this

  @include: (obj) ->
    for key, value of obj when key not in moduleKeywords
      # Assign properties to the prototype
      @::[key] = value

    obj.included?.apply(@)
    this

moduleKeywords変数まわりのちょっとしたダンスはミックスインがクラスを拡張した時にコールバックをサポートするためです。我々のModuleクラスがどう動くか見てみましょう。

classProperties = 
  find: (id) ->
  create: (attrs) ->

instanceProperties =
  save: -> 

class User extends Module
  @extend classProperties
  @include instanceProperties

# Usage:
user = User.find(1)

user = new User
user.save()

ご覧のとおり、我々は静的プロパティとしてfind()create()Userクラスに追加しました。またインスタンスプロパティもsave()を追加しました。 モジュールが拡張されたときにはコールバックを得ますので静的、及びインスタンスプロパティを適用するプロセスはショートカット可能です。

ORM = 
  find: (id) ->
  create: (attrs) ->
  extended: ->
    @include
      save: -> 

class User extends Module
  @extend ORM

とてもシンプルでエレガントでしょう!