Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

以前、書いた二つの記事、

では、CakePHPの出力するJSONに合わせて、Backbone.jsにカスタムのparseメソッドを実装していました。ただ、世の中のライブラリを見ると、Backbone.jsが期待している形式の方が一般的のようです。ですので、CakePHP側のJSONフォーマットを変更したバージョンを作成してみました。ソースコードはgithubの「backbone_client_nocustomparse」,「cakephp_service_nocustomparse」となります。行っていることはCakePHPのコントローラでモデルから返されたarrayをちょっと変更しているだけです。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

以前、記事「CakePHP + Backbone.jsで作るJavaScript Web Application」を書きましたが、このときは、CakePHPとBackbone.jsをどう連携させるかに焦点を当てていたので、Backbone.jsの重要な機能であるViewを全く使っていませんでした。ということで、今回はViewを使った場合のCakePHPとBackbone.jsの連携について説明します。

なお、ソースコードはgithubのこちらのリポジトリのbackbone_client2となります。

1. Backbone.jsにおけるView

Backbone.jsのViewは、その名前とは異なり、実際にはViewを表示するというより、ModelとViewをつなぐ部品としての役割が主となります。なので、実際の表示、つまりDOMの操作にどの方法を使うかは開発者に任されています。今回のアプリケーションでは前回と同じくjQueryを使うこととします。なので、Backbone.jsが提供しているViewクラス(Backbone.View)には、あまり多くの機能はなく、どちらかと言えばViewを作るための土台となるクラスを提供しているというイメージです。

今回のアプリケーションで使うモデルは、以前の記事と変わらず、一つの顧客モデルを表すCustomerと、複数の顧客モデルを含むCustomersコレクションを定義します。そして、二つのモデルに対応するViewクラスであるCustomerViewとCustomersViewを新たに定義します。(わかりづらいですが単数形と複数形の違い)

アプリケーションの構成としては、一つのCustomersコレクションのインスタンスと、それに対応するCustomersViewのインスタンスがあり、その二つがバインドされます。そして、一覧表示、追加、削除、編集といった処理で、一つの顧客を表すCustomerモデルとCustomerViewが生成、更新、削除されていくこととなります。このため、CustomersViewとCustomerViewとでは、同じBackbone.Viewを継承しているViewクラスですが、結構実装の内容が異なる点に注意してください。

なお、先ほども触れたようにBackbone.jsのViewは、土台程度の部分しか提供されていないので、実装の仕方には、いろいろな選択肢があります。今回は、Backbone.jsのサイトにあるTodo List Applicationに近い形式を選択しています。

2. 大まかな実装の流れ

Viewの実装の流れは、大きく以下となります。

  1. tagNameプロパティもしくはelで、Viewと対となるHTML要素を生成もしくは指定する。
  2. eventsプロパティで、UIイベントとViewをバインドする。
  3. initializeメソッドで、ModelのイベントとViewをバインドする。
  4. renderメソッドで$elを編集する処理を記述する。
  5. その他のイベントに対するコールバックメソッドを実装する。

3. CustomerViewクラスの定義

Customerモデルと対になるCustomerViewクラスは以下のような内容となります。

var CustomerView = Backbone.View.extend({
  tagName : 'tr',
  events : {
    "click .edit-button" : "onEditButton",
    "click .delete-button" : "onDeleteButton"
  },
  initialize : function() {
    this.model.on('change', this.render, this);
    this.model.on('destroy', this.remove, this);
  },
  render : function() {
    this.$el.html('<td>' + this.model.get('id') + '</td><td>'
        + this.model.get('name') + '</td><td>'
        + this.model.get('address')
        + '</td><td>'
        + '<input class="edit-button" type="button" value="Edit"/>'
        + '<input class="delete-button" type="button" value="Delete"/>'
        + '</td>');
    return this;
  },
  onEditButton : function() {
    $('#dialog-name').val(this.model.get('name'));
    $('#dialog-address').val(this.model.get('address'));
    $('#dialog').bind('dialogclose', {model: this.model},
      function(event) {
        event.data.model.set('name', $('#dialog-name').val());
        event.data.model.set('address', $('#dialog-address').val());
        event.data.model.save();
        $('#dialog').unbind('dialogclose');
      }
    );
    $('#dialog').dialog('open');
  },
  onDeleteButton : function() {
    this.model.destroy();
  }
});

Backbone.jsのViewは、モデルを表すビューのDOMツリーの起点(ルート)となる要素と対になります。たとえば、table要素の一つの行が一つのモデルに対応する場合は、tr要素とViewが対になります。そしてViewクラスがインスタンス化される際に、対になるtr要素がBackbone.View内部で自動的に生成されます。このため、ビューに対してどうやって要素を生成させるかの情報を、ビューを定義する際に与えてあげる必要があります。そのための指定が以下のtagNameの指定です。つまり、ここではtr要素が生成されます。tagName以外にも、オプションとしてclassNameやidを指定することもできます。

  tagName : 'tr'

そして、生成された要素はelというプロパティに設定されます。さらに$elでjQueryオブジェクトとしてアクセスできます。

次にeventsオプションで、HTML要素のイベントとViewをバインドします。

events : {
  "click .edit-button" : "onEditButton",
  "click .delete-button" : "onDeleteButton"
},

指定の仕方は「”イベント セレクタ” : “コールバック”」となります。ですので、最初の指定ではedit-buttonクラスを持つ要素のclickイベントにonEditButtonコールバックをバインドします。

イベントのバインドにはjQueryのdelegateを使っています。具体的には「this.$el.delegate(セレクタ, イベント, コールバック)」でバインドしています。ですので、$elを起点としたDOMツリー内からセレクタで選択される要素にバインドされます。つまり、$el以下にない要素とはバインドできません。

initializeメソッドは、Viewがインスタンス化される際に自動的に呼び出されます。ここでは、モデルのイベントとコールバックを結び付けています。

initialize : function() {
  this.model.on('change', this.render, this);
  this.model.on('destroy', this.remove, this);
},

Backbone.jsのモデルには、イベントをバインドするためのon(もしくはbind)というメソッドが定義されています。そして、イベントの種類として以下が定義されています。

イベント名 契機 対象
add コレクションにモデルが追加された場合 モデル、コレクション
remove コレクションからモデルが削除された場合 モデル、コレクション
reset コレクションの内容がすべて入れ代った際 コレクション
change モデルの属性が変更された際 モデル、オプション
change:[attribute] 特定の属性が変更された際 モデル、値、オプション
destroy モデルが破棄された際 モデル、コレクション
sync モデルのサーバへの同期に成功した際 モデル、コレクション
error モデルのバリデーションが失敗した際か、サーバへの保存に失敗した際 モデル、コレクション
route:[name] ルーターのルートに一致した際 ルーター
all この特別なイベントは、すべてのイベントで発生し、第一引数にイベント名を設定する

一つ目の「this.model.on(‘change’, this.render, this)」の場合、モデル「this.model」で「change」イベント(つまり、モデルの属性の変更)が発生した場合、ビューの「render」メソッドが呼び出されます。なお、第三引数の「this」は、コールバックメソッド側でthisとして扱うオブジェクトを指定します。ここでは、thisなのでビュー自身を指定しています。

二つ目の「this.model.on(‘destroy’, this.remove, this)」の場合、「destroy」イベント(つまり、モデルの破棄)が発生した場合、ビューの「remove」メソッドが呼び出されます。なお、このremoveメソッドは、Backbone.Viewにあらかじめ定義されており、「this.$el.remove()」が実行され、このビューと対になるHTML要素の削除が行われます。

なお、「this.model」の値は、Viewを生成する際にオプションとしてmodelを指定することで設定されます。今回はCustomersViewのonAddEventで指定しています。

次のrenderがビューの中心となる部分です。

render : function() {
  this.$el.html('<td>' + this.model.get('id') + '</td><td>'
      + this.model.get('name') + '</td><td>' + this.model.get('address')
      + '</td><td>'
      + '<input class="edit-button" type="button" value="Edit"/>'
      + '<input class="delete-button" type="button" value="Delete"/>'
      + '</td>');
  return this;
},

renderメソッド内では、$elをモデルのデータを基に編集します。$el自身はtagNameから生成されているので、行うのは$elの属性の変更や、子要素の追加などとなります。

最後の「return this」は、他のビューがCustomerViewのrenderの結果が必要となる際にアクセスしやすいように、このような記述の仕方をしています(ちなみに、Backbone.Viewのrenderメソッドの内容は「return this」のみ)。実際に、この後、CustomersViewのrenderで使っています。

Backbone.jsで用意しているビューへの変更はここまでです。この後にあるonEditButtonとonDeleteButtonは、編集ボタンと削除ボタンを押した際に起動されるコールバックメソッドとなります。それぞれ、モデルの属性の更新とモデルの削除を実装しています。

4. CustomersViewクラスの定義

コレクションCustomersに対応するビューCustomersViewクラスの内容は以下となります。

var CustomersView = Backbone.View.extend({
  el : $('#customer-app'),
  events : {
    "click #add-button" : "onAddButton"
  },
  initialize : function() {
    customers.on('add', this.onAddEvent, this);
    customers.on('reset', this.onResetEvent, this);
    customers.on('all', this.render, this);
    customers.fetch();
  },
  onAddEvent : function(customer) {
    var customerView = new CustomerView({
      model : customer,
      id : customer.get('id')
    });
    this.$('#list-tbody').append(customerView.render().el);
  },
  onResetEvent : function() {
    this.$('#list-tbody').html('');
    customers.each(this.onAddEvent);
  },
  onAddButton : function(e) {
    $('#dialog-name').val('');
    $('#dialog-address').val('');

    $('#dialog').bind('dialogclose', function(event) {
      customers.create({
        name : $('#dialog-name').val(),
        address : $('#dialog-address').val()
      });
    });
    $('#dialog').dialog('open');
  }
});

すでに説明したCustomerViewと同じくBackbone.Viewを継承していますが、いきなり最初から異なります。

  el : $('#customer-app'),

CustomerViewの場合は、tagNameを指定して、生成されるHTML要素の種類を指定していました。というのも、CustomerViewの場合、顧客の件数により動的にビューが生成されるためです。対して、CustomersViewはtable要素などをすべて含むdiv要素と対となります。そして、そのdiv要素はHTML上にある静的なもので、生成、削除が行われることはありません。Backbone.jsのViewは、elが存在しないとインスタンス化の際にtagNameからelを生成しますが、もしすでにelが存在する場合は、生成しないようになっています。ですので、$(‘#customer-app’)で、対となるdiv要素をelに指定しています。

次のinitializeでは、CustomerViewと同じくイベントとコールバックメソッドのバインドを行っています。そして最後に「customers.fetch()」でサーバから現在DBに登録されている顧客データの一覧を取得しています。なお、customersはCustomersコレクションのインスタンスです。

initialize : function() {
  customers.on('add', this.onAddEvent, this);
  customers.on('reset', this.onResetEvent, this);
  customers.on('all', this.render, this);
  customers.fetch();
},

コレクションのfetch()が実行されると、resetイベントが発生するので、自動的にバインドしていたthis.onResetEventメソッドが呼び出されます。そして、this.onResetEventメソッドではすべての顧客データをtable要素に追加しています。

一件追加処理は、まず、Addボタンが押された後に、コレクションのcreateメソッドで一件のCustomerを追加しています。すると、コレクションでaddイベントが発生するのでonAddEventメソッドが呼び出されます。

onAddEvent : function(customer) {
  var customerView = new CustomerView({
    model : customer,
    id : customer.get('id')
  });
  this.$('#list-tbody').append(customerView.render().el);
},

ここでは、CustomerViewを生成していますが、その際に対となるCustomerモデルを指定しています。同時に、idを指定することで、CustomerViewの対となるtr要素のid属性を指定しています。

実際のtable要素への行の追加は、CustomerViewのrenderメソッドを呼び出し、その結果を追加することで行っています。

なお、今回はrenderメソッドを特に定義していませんが、もし、モデルの情報が変わったら変更しなければならないUIがあれば定義します。ちなみに、Backbone.jsのサイトにあるTodo List Applicationでは、データが0件の場合に、一覧自体をまったく表示しないようにしています。

5. 最後に

ソースコードはgithubのこちらのリポジトリのbackbone_client2となりますので、細かい点はこちらを参照してください。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

正確に言うと「ブロック」はObjective-CではなくAppleがC言語に標準として追加することを提案している機能です。ブロックとは何かというと、最近の言語だとだいたい含まれている「クロージャ」のことです。(だったら、「クロージャ」という名前にすればよいのだと思うのですが、なぜか「ブロック」という非常に一般的な言葉を選んでいます。さらに、C言語ではすでに{}で囲まれたところをブロックと呼んでいると思うのですが…)
参考にしたのは、以下のAppleの資料です。

ブロックの基本

C言語では昔から関数ポインタがあり、関数のポインタを別の関数の引数に渡したりすることができました。ただ、そのためには、関数を定義し、それを関数ポインタとして渡すという二段階の手順が必要でした。対して、RubyやC#のようにラムダ式やクロージャをインラインで定義できると、ちょっとした数行の処理を関数に渡したりすることができ便利です。

ブロックは、ブロックをコード内にインラインで定義する機能を持っています。そして、その定義したブロックを関数に渡すことができます。実際の例を以下に示します。

int calc(int a, int b, int(^f)(int, int)) {
  return f(a, b);
}

int main(int argc, const char * argv[])
{
  @autoreleasepool {
    int result1 = calc(1, 2, ^(int a, int b) {
      return a + b;
    });
  }
}

構文が今までみたことないようなものなので、少しわかりずらいですが、calc関数の呼び出しの第三引数である以下がブロックです。先頭の^がブロックであることを示しています。

^(int a, int b) {
  return a + b;
}

足し算をするブロックを定義し、それをすぐにcalc関数に渡しています。今までのC言語の関数ポインタでは、add_funcといった名前の関数を定義し、その関数ポインタをcalc関数に渡すという処理が必要でしたが、ブロックでは定義と引数として渡す処理が一緒にできます。

多くの場合は、インラインでブロックを書くことになりますが、関数ポインタのように、ブロックの参照(ブロック参照)を取得し、それを関数の引数に渡すようにすることもできます。

int calc(int a, int b, int(^f)(int, int)) {
  return f(a, b);
}

int main(int argc, const char * argv[])
{
  @autoreleasepool {
    int(^add_func)(int, int) = ^(int a, int b) {
      return a + b;
    };
    int result1 = calc(1, 2, add_func);
  }
}

main関数内では、最初にadd_funcというブロック参照を定義しています。(非常にわかりづらいですが、これでブロックを格納する変数を定義しています)

int(^add_func)(int, int);

これで、int型の2つの引数を受け取り、intを戻すブロック参照を定義したこととなります。そして、単純にブロックをこのブロック参照に代入し、そのブロック参照を別の関数の引数に渡すことができます。

注意として、ブロックの定義”int(^f)(int,int)”では、最初のintで戻り値の型を明示していますが、ブロックでは”^(int, int)”のように戻り値の型は指定しません。コンパイラがブロック定義より正しい戻り値の型を推測します

ブロックを用いて、同じ関数に違う処理をさせる例を示します。

int foreach(int data1[], int data2[], int n, int(^f)(int, int)) {
    int result = 0;
    for(int i = 0; i < n; ++i) {
        result += f(data1[i], data2[i]);
    }
    return result;
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int data1[] = {1, 2, 3, 4, 5};
        int data2[] = {2, 4, 6, 8, 10};
        // add
        int result1 = foreach(data1, data2, 5, ^(int a, int b){
            return a + b;
        });
        // sub
        int result2 = foreach(data1, data2, 5, ^(int a, int b){
            return a - b;
        });
    }
}

ブロックと変数

ブロックでは、他の言語のクロージャやラムダ式と同様に、ブロックが定義された場所で可視な変数を使うことができます(これを変数のキャプチャという)。

int foreach(int data[], int n, int(^f)(int)) {
    int result = 0;
    for(int i = 0; i < n; ++i) {
        result += f(data[i]);
    }
    return result;
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int data[] = {1, 2, 3, 4, 5};
        int value = 2;
        int result = foreach2(data, 5, ^(int a){
            return a * value;
        });
    }
}

ここで、main関数内で定義されたvalueをブロック内でキャプチャしています。ただし、value変数は読み取り専用としてしかアクセスできません。書き込みたい場合は、__blockという修飾子を付ける必要があります。

void foreach(int data[], int n, void(^f)(int)) {
    for(int i = 0; i < n; ++i) {
        f(data[i]);
    }
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int data[] = {1, 2, 3, 4, 5};
        int value = 2;
        __block int result = 0;
        foreach(data, 5, ^(int a){
            result += a * value;
        });
    }
}

この例では、resultを__blockを付けて宣言しています。

ブロックとメモリ管理

インラインでブロックを定義し、その場で呼び出しているだけであれば、あまりメモリ管理については気にする必要がありません。ですが、ブロックが活躍するのはコールバックや非同期処理です。これらの場合、ブロックの呼び出しは、定義した所のずっと後という可能性があります。するとキャプチャした変数が、もう解放されて、なくなっている可能性が出てきます。

ブロックに引数として渡された変数やグローバル変数は、当然ながら、ずっとブロック内から参照可能です。また、キャプチャされた変数も問題なく参照できます。

ブロック内で参照されているObjective-Cのオブジェクトは、自動的に参照カウンタ(retain counter)が+1されます。ですので、解放されることはありません。

C++のオブジェクトの場合、__block修飾子付きでスタックに作成された場合は、コピーコンストラクタでコピーが作成され、それにアクセスできます。__blockが無い場合は、constコピーコンストラクタでコピーが作成されます。

上記の変数についてはなんら問題ないことがわかります。注意するのは、スタック上のデータをポインタで参照している場合や、ヒープ上に作成したC/C++のデータをポインタで参照している場合です。これらの場合、ブロックが実行される際にメモリ上のデータが消えていないことは、開発者が保証しなければなりません。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

前回の記事「CakePHP+Backbone.jsで作成したJavaScript Web Application」の内容を、比較のためにSpine.jsでも試してみました。
CakePHP側は何も変更していないので、Spine.js側だけを説明します。それから、Spine.jsはCofeeScriptにも対応していますが、ここではJavaScriptで作成しています。ソースコードは前回のソースに追加する形でGitHubで公開しています。

1.Spine.jsでのModelの定義

Spine.jsはControllerやViewも持っていますが、今回はModelだけを使っています。

まず、nameとaddressを属性として持つCustomerモデルを以下のようにして作成します。

var Customer = Spine.Model.sub();
Customer.configure('Customer', 'name', 'address');

CoffeeScriptですと「class Customer extends Spine.Model」ですので、これでModelクラスを継承して新しいモデルクラスを作成しているということになります。ですが、Spine.jsのややこしいのは、このCustomerクラスには、複数のCustomerが含まれるコレクションとしての機能も持っているということです。

Backbone.jsでは、モデルとコレクションのクラスを定義し、そのインスタンスを生成するという、オブジェクト指向らしい手順を踏んでいました。ですが、Spine.jsでは、Customerクラスがコレクションで、Customerクラスのインスタンスが個々のCustomerを表しています。困ったのは、この後、このCustomerをクラスと呼ぶべきか、それともコレクションと呼ぶべきかです。迷ったのですが、ひとまずはクラスと呼ぶこととします。ですが、Customerクラスは、コレクションであり複数のCustomerインスタンスを持っていることは気にとめておいてください。

次に、Ajaxによる永続化の設定をします。

Customer.extend(Spine.Model.Ajax);
Customer.extend({
    url : '/cakephp_service/customers'
});

Ajaxによる永続化を利用することと、Ajaxリクエストを送信するURLを指定します。
次に、Backbone.jsでも同じことをしましたが、CakePHPからのJSONをパースできるように、特別なパース処理を定義します。

Customer.extend({
    fromJSON : function(objects) {
        var value, _i, _len, _results;
        if (!objects) {
            return;
        }
        if (typeof objects === 'string') {
            objects = JSON.parse(objects);
        }
        if (Spine.isArray(objects)) {
            _results = [];
            for (_i = 0, _len = objects.length; _i < _len; _i++) {
                if (objects[_i].Customer != undefined) {
                    value = objects[_i].Customer;
                } else {
                    value = objects[_i];
                }
                _results.push(new this(value));
            }
            return _results;
        } else {
            if (objects.Customer != undefined) {
                return new this(objects.Customer);
            }
            return new this(objects);
        }
    }
});

ちょっと長いですが、元々のソースであるSpine.Model.fromJSON関数から、「.Customer」で取得する処理を数行追加しただけです。

2. イベント

当然ながらSpine.jsもAjaxの永続化は非同期で行われますが、Backbone.jsのようにfetch()にコールバック関数を指定できない仕様のようです。代わりに、イベントを使う必要があります。(ちなみに、Backbone.jsにも同じような機能はありますが、前回の記事では使っていません)

Spine.jsのイベントとしては、以下のようなものがあります。

イベント名 発生するタイミング
save 保存(生成(create)もしくは更新(update))が発生した場合
update 更新が発生した場合
create 生成が発生した場合
destroy 削除が発生した場合
change 生成(create)/更新(update)/削除(destroy)のどれかが発生した場合
refresh すべてのデータが無効化され置き換わった場合
error バリデーションに失敗した場合

ひとまず、表示を更新できるようにするためrefreshイベントにCustomerの一覧を表示するように、コールバック関数を設定します。

Customer.bind("refresh", function() {
    var tr = $('#list-table>tbody>tr');
    if (tr.length > 0) {
        tr.remove();
    }
    Customer.each(function(customer) {
        var tbody = $('#list-table>tbody');
        tbody.append('<tr><td>' + customer.id + '</td><td>' + customer.name
                + '</td><td>' + customer.address + '</td></tr>');
    });
});

3. アクションの定義

3.1 Customerの追加(addアクション)

以下が、Spine.jsで新しいCustomerを一件追加する処理です。

$(function() {
    $('#add-button').click(function(e) {
        Customer.create({
            name : $('#add-name').val(),
            address : $('#add-address').val()
        });
    });
});

Backbone.jsと異なるのは、Customerクラス(というかコレクション)のcreateを呼び出すだけで、Customerインスタンスの生成、Cusomterクラスへの追加、そして、Ajaxによる永続化まで行われているということです。Backbone.jsではsave()を呼び出すまで永続化が行われませんが、Spine.jsではcreateを呼び出すだけで直ぐに行われます。当然ながら、新しいCustomerを追加するには常にcreateを呼び出すしかありません。なので、インスタンスは生成するが、永続化はしないということができません。

3.2 Customerの一覧の取得(indexアクション)

Customerの一覧の取得は以下の通りとなります。すでにrefreshイベントに画面の更新処理を入れているので、結果を受け取った後にrefreshイベントが発生し、画面が更新されます。

$(function() {
    $('#list-button').click(function(e) {
        Customer.fetch();
    });
});

3.3 指定したIDのCustomerの取得(viewアクション)

指定したIDのCustomerの取得は、find(id)を使い、以下の通りとなります。ここで初めてCustomerのインスタンスが出てきます。

$(function() {
    $('#update-read_button').click(function(e) {
        var customer = Customer.find($('#update-read_id').val());
        $('#update-name').val(customer.name);
        $('#update-address').val(customer.address);
    });
});

3.4 Customerの属性の更新(editアクション)

属性を変更し、save()を呼び出せば、更新が行われます。

$(function() {
    $('#update-update_button').click(function(e) {
        var customer = Customer.find($('#update-read_id').val());
        customer.name = $('#update-name').val();
        customer.address = $('#update-address').val();
        customer.save();
    });
});

3.5 Customerの削除(deleteアクション)

Customerクラスのdestroy(id)を呼び出せば、指定したIDのCustomerが削除されます。また、Customerインスタンスにdestroy()があるのでそちらも使えます。

$(function() {
    $('#delete-button').click(function(e) {
        Customer.destroy($('#delete-id').val());
    });
});

4. まとめ

おそらく、Spine.jsの思想としては、JavaScript上のモデルは常にサーバと同期され、同時にViewとも同期されるべきと考えているようです。そのためか、モデルのcreateを呼び出すだけで、すぐにAjaxリクエストが送られるようになっています。fetchにコールバックが指定できず、イベントでしか更新できないのも、ViewとModelが常に同期されているようにするためと考えられます。さらに、モデルクラスがコレクションになっているのも、モデルクラスが一つ存在し、それがクライアント側でのモデルのリポジトリのように動作し、サーバおよびViewと同期するようにさせたいためではないかと思われます。

対して、Backbone.jsは、サーバとの同期タイミングは開発者が制御できますし、fetchメソッドにコールバック関数を指定できViewが更新できるタイミングを制御できます。Spine.jsの考えが常にうまくいけば、こちらの方が楽そうですが、うまくいかなった場合の懸念があります。

Spine.jsで納得できないのが、モデルクラスがコレクションにもなっているという設計です。なんともわかりずらいとしか言いようがありません

ということで、私は、現時点ではBackbone.jsがおすすめです。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

CakePHPでWebサービスを公開して、Backbone.jsでアクセスするという、今話題のJavaScript Web Applicationを試してみたのですが、いろいろと苦労したのでまとめてみました。
ソースコードはGitHubに置いてあります。

更新: 本記事ではCakePHPとBackbone.jsを連携させる部分に焦点を当てているために、Backbone.jsのViewを使っていません。View使った場合については「CakePHP + Backbone.jsで作るJavaScript Web Application ~ Viewの利用 ~」を参照してください。

更新2: 本記事ではCakePHPに合わせるようにBackbone.js側のモデルにカスタムparseメソッドを実装しています。逆にCakePHP側の出力するJSONフォーマットを変更したものが「CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~」にあります。

1.Webアプリの構成

Webアプリは、CakePHP側とBackbone.js側とを別のアプリ、つまり別のフォルダに構成することにします。CakePHP側は”cakephp_service”、Backbone.js側は”backbone_client”という名前のフォルダに置くこととします。作成するアプリの内容としては、DB上の顧客(Customer)情報にアクセスできるWebサービスをCakePHPで提供し、そのWebサービスにBackbone.jsでアクセスすることとします。以下が簡単なアプリケーションの構成図です。

2.環境

今回は以下の環境を使いました。

3.MySQLのセットアップ

XAMPPからphpMyAdminを起動して、以下のデータベースとユーザを作成します。

  • データベース: test_service
  • ユーザ/パスワード: test/test

test_serviceデータベースに、以下のようにcustomersテーブルを作成します。

CREATE TABLE customers (
  id CHAR(36) NOT NULL,
  name VARCHAR(64) NOT NULL,
  address VARCHAR(255) NOT NULL,
  created datetime NOT NULL,
  modified datetime NOT NULL,
  PRIMARY KEY (id)
);

このテーブルはCakePHPの規約に沿って、作成しています。

4.CakePHPの基本的なセットアップ

XAMPPのhtdocs内に”cakephp_service”と”backbone_client”フォルダを作成します。

cd xampphtdocs
mkdir cakephp_service
mkdir backbone_client

CakePHP 2.1のzipファイルを展開し、その中身をすべてcakephp_serviceにコピーします。XAMPPのApacheが起動していなければ起動します。ブラウザで”http://localhost/cakephp_service/“にアクセスすることでCakePHPの動作が確認できます。
ここからは、CakePHPの基本的な設定を行います。詳しくはCookbookのInstallを見てください。

  1. app/Config/core.phpのSecurity.saltの値を変更する。
  2. app/Config/core.phpのSecurity.cipherSeedの値を変更する。
  3. app/Config/database.php.defaultをdatabase.phpという名前でコピーする。
  4. database.phpを編集する。

database.phpの内容は以下の通りにしました。

class DATABASE_CONFIG {
  public $default = array(
    'datasource' => 'Database/Mysql',
    'persistent' => false,
    'host' => 'localhost',
    'login' => 'test',
    'password' => 'test',
    'database' => 'test_service',
    'prefix' => '',
    'encoding' => 'utf8',
  );

public $test = array(
    'datasource' => 'Database/Mysql',
    'persistent' => false,
    'host' => 'localhost',
    'login' => 'test',
    'password' => 'test',
    'database' => 'test_service',
    'prefix' => '',
    'encoding' => 'utf8',
  );
}

ブラウザで”http://localhost/cakephp_service/“にアクセスして、エラーや警告が出ていないことを確認してください。

5.CakePHPのWebサービスの設定

CakePHPをWebサービスに対応させるためには、3つのことが必要となります。

  1. CakePHPでJson形式のレスポンスを返せるようにする – JsonViewの設定をします
  2. CakePHPでJson形式のリクエストを受け取れるようにする – CakePHPがJsonをパースするので、コントローラでそのデータにアクセスするようにします
  3. CakePHPのアクションとWebサービスのHTTPメソッドをマッピングする – routes.phpを編集します

最後のHTTPメソッドですが、Backbone.jsやSpine.jsは、サーバとの通信に以下のようなHTTPメソッドとURLの組み合わせでアクセスしてきます。そして、CakePHPでは、設定を変更することで、それぞれを以下のようにControllerのアクションにマッピングする機能を持っています。

機能 HTTPメソッド URL コントローラのアクション
一覧の取得 GET /customers CustomersController::index()
IDを持つ値の取得 GET /customers/id CustomersController::view(id)
追加 POST /customers CustomersController::add()
編集 POSTもしくはPUT /customers/id CustomersController::edit(id)
削除 DELETE /customers/id CustomersController::delete(id)

特筆すべきは、JsonViewを有効にすることで、CakePHPのModelとControllerを作成するだけで、つまり、Viewを作成することなく、Jsonのレスポンスを返せるようになります。
Webサービスに関する設定は、CakePHP Cookbookに書かれている以下の情報を参考にしています。

5.1 AppControllerの編集

“app/Controller/AppController.php”をテキストエディタで開き、以下のように編集します。

App::uses('Controller', 'Controller');

class AppController extends Controller {
  public $viewClass = 'Json';
  public $components = array('RequestHandler');
}

“$viewClass = ‘Json'”により、JsonViewをViewクラスとして使うことを指定しています。”$components = array(‘RequestHandler’)”により、RequestHandlerが有効となり、コンテンツタイプにより自動的にViewクラスが変更されるようになります。

5.2 routes.phpの編集

“app/Config/routes.php”をテキストエディタで開き、以下のように編集します。

Router::mapResources('customers');
Router::parseExtensions('json');

Router::connect('/',
  array('controller' => 'pages', 'action' => 'display', 'home'));
...

“Router::parseExtensions(‘json’)”により、Accept Headerが”application/json”の場合にJsonViewに切り替わるようになります。
そして、”Router::mapResources(‘customers’)”により、CustomersControllerに対するリクエストはWebサービス特有のHTTPメソッドにマッピングするようになります。注意として、今後Webサービスとして公開するコントローラが増えた場合、routes.phpに”Router::mapResources(コントローラ名)”を追加していく必要があります。

6.モデルの作成

モデルは、通常のCakePHPのモデルと変わりません。今回のアプリケーションではCustomer.phpを”app/Model”に以下の内容で作成します。

<?php
class Customer extends AppModel {
    public $name = 'Customer';
}
?>

7.コントローラの作成

まず、”app/Controller”にCustomersController.phpを以下の内容で作成します。

<?php
class CustomersController extends AppController {
    public $name = 'Customers';
}
?>

ここに、アクションを一つずつ追加していきます。その前に、Backbone.js側の準備をします。

8. Backbone.js側の準備

8.1 環境

backbone_clientフォルダ内にlibフォルダを作成し、その中に以下のファイルをコピーします。

  1. backbone.js
  2. jquery-1.7.2.js
  3. json2.js
  4. underscore.js

アプリケーションはindex.htmlとapplication.jsだけとなります。index.htmlには以下の内容を記述します。

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Backbone.js Client</title>
    <script src="lib/jquery-1.7.2.js" type="text/javascript"></script>
    <script src="lib/underscore.js" type="text/javascript"></script>
    <script src="lib/json2.js" type="text/javascript"></script>
    <script src="lib/backbone.js" type="text/javascript"></script>
    <script src="application.js" type="text/javascript"></script>
  </head>
  <body>
  </body>
</html>

8.2 モデルの定義

Backbone.jsにはいろいろな機能が含まれていますが、ここで使うのはモデルの定義と、モデルに含まれるデータのAjaxによるサーバへの永続化です。定義するモデルはCustomerモデルと、そのコレクションであるCustomersコレクションです。
まず、Customerモデルを定義しますので、application.jsの先頭に以下を記述します。なお、モデルが持つデータをBackbone.jsでは属性(attribute)と読んでいますので、その用語をここでも使います。ちなみに、CakePHPのモデルではプロパティという用語を使いますが、JavaScriptでのプロパティはJavaScriptオブジェクトの持つプロパティとなり、モデルの持つデータとは異なる意味になるので注意が必要です。

var Customer = Backbone.Model.extend({
  defaults : {
    "name" : "",
    "address" : ""
  },
  urlRoot : "/cakephp_service/customers",
  parse : function(response) {
    if (response.Customer != undefined) {
      return response.Customer;
    }
    return response;
  }
});

Backbone.jsではBackbone.Model.extend({})を使って、モデルクラスを定義します。引数でオプションを指定できますが、ここではdefaults、urlRoot、parseという3つのオプションを指定しています。

“defaults”は、その名の通り、Customerモデルが持つ属性のデフォルト値を指定しています。”defaults”は無くても問題はないのですが、nameとaddressが存在しないという状態を作りたくなかったので設定しました。注意として、idという属性は”defaults”に指定してはなりません。Backbone.jsでは、idという属性が存在しているかどうかで、そのインスタンスがサーバに永続されているかを判断するために使います。つまり、属性idが割り当てられていないインスタンスは、新しいインスタンスと判断され、サーバに永続される際にaddアクションが呼び出され、DBへのINSERTが発生します。

“urlRoot”は、このモデルがサーバに永続される際に、サーバへAjaxリクエストを送る際のURLルートを指定します。今回のアプリケーションでは”/cakephp_service”の”/customers”にアクセスするように指定します。

“parse”は、サーバが返したJSONを、どのようにパースしてモデルのインスタンスを生成するかを定義する関数を記述します。デフォルトでは、受け取ったJSONがそのままモデルのインスタンスとなることを仮定しています(つまり”return response;”)。ですので、CakePHP側が生成するJSONのフォーマットと合うように設計する必要があります。加えて、この後で説明するコレクションのパースの場合でも正しく動作する必要があります。ここでは、responseにCustomerというプロパティが定義されていたらその値をモデルのインスタンスとし、それ以外の場合は、そのままresponseをモデルのインスタンスとして返すようにしています。

8.3 コレクションの定義

Backbone.jsでは、検索結果のように複数件のモデルが含まれる結果が返る場合は、それをコレクションに格納することになっています。ですので、複数のCustomerモデルが含まれるコレクションとしてCustomersコレクションを次に定義します。

var Customers = Backbone.Collection.extend({
  model : Customer,
  url : "/cakephp_service/customers"
});

Backbone.Collection.extend({})で、コレクションクラスを定義します。ここでは、オプションとしてmodelとurlを指定しています。modelはこのコレクションが格納するモデルを指定します。urlは、コレクションをサーバから取得する際のURLを指定します。ちなみに、コレクションでもparseを指定し、JSONのパースの仕方を変えることができます。また、コレクションのparseは、内部でモデルのparseを呼び出して、最終的なモデルのインスタンスを取得しています。

9 アクションの定義

9.1 Customerの追加(addアクション)

Customerを新しく追加するため、nameとaddressを入力し、Addボタンを押すことができるWebページを作成します。

HTMLとしては以下のようになります。

<table id="add-table">
  <tbody>
    <tr>
      <th>name</th>
      <td><input id="add-name" size="32"/></td>
    </tr>
    <tr>
      <th>address</th>
      <td><input id="add-address" size="32"/></td>
    </tr>
  </tbody>
</table>
<input id="add-button" type="button" value="Add"/>

Addボタンが押された場合に、Customerモデルをサーバに永続化する処理は以下となります。

$(function() {
  $("#add-button").click(function(e) {
    var customer = new Customer({
      name : $('#add-name').val(),
      address : $('#add-address').val()
      });
    customer.save();
  });
});

“new Cutomer({})”とすることで、新しいCustomerのインスタンスが生成できます。この際、設定したい属性を引数に指定できます。ここでは、nameとaddress属性を、inputタグの値を取得して設定しています。
生成したcustomerインスタンスを永続化するにはsave()を呼び出します。これで、非同期でサーバへの永続化が実行されます。
nameに”test”、addressに”tokyo”と指定した場合に、Backbone.jsから送られるHTTPリクエストの内容は以下の通りとなります。

POST /cakephp_service/customers HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) Gecko/20100101 Firefox/11.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Type: application/json; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://localhost/backbone_client/
Content-Length: 33
Pragma: no-cache
Cache-Control: no-cache

{"name":"test","address":"tokyo"}

さて、CakePHPのCustomersControllerに、このリクエストを受けて、データベースにCustomer格納するするアクションaddを追加します。

class CustomersController extends AppController {
  public $name = 'Customers';
  function add() {
    $customer['Customer']['name'] = $this->params['data']['name'];
    $customer['Customer']['address']  = $this->params['data']['address'];
    $this->Customer->create();
    $this->Customer->save($customer);
    $customer['Customer']['id'] = $this->Customer->id;
    $this->set('customer', $customer);
    $this->set('_serialize', 'customer');
  }
}

まず、Backbone.jsはデータをJSON形式でPOSTしています。通常のFORMからのPOSTならば$this->params[‘form’]でパラメータを取得できますが、JSONの場合$this->[‘data’]を使う必要があります。CakePHPのモデルを用いたデータベースの永続化は、今まで通りです。最後に、登録された内容をJSON形式でクライアントに返しています。JsonViewは”_serialize”という名前で配列(array)を設定すると、それをJSONとして出力します。
実際にaddアクションのHTTPレスポンスは以下のような内容となります。

HTTP/1.1 200 OK
Server: Apache/2.2.21 (Win32) mod_ssl/2.2.21 OpenSSL/1.0.0e PHP/5.3.8 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.8
Content-Length: 46
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json

{"Customer":{"name":"test","address":"tokyo"}}

9.2 Customerの一覧の取得(indexアクション)

次に、Customerの一覧を取得するindexアクションを追加します。CustomersControllerに以下のようにindexアクションを追加します。

class CustomersController extends AppController {
  public $name = 'Customers';
    function index() {
      $customers = $this->Customer->find('all');
      $this->set('customers', $customers);
      $this->set('_serialize', 'customers');
  }
}

こちらは、非常にシンプルです。find(‘all’)でCustomerの一覧を取得し、それを’_serialize’に設定しています。
Backbone.js側は以下のようになります。

$(function() {
  $('#list-button').click(function(e){
    var customers = new Customers;
    customers.fetch({
      success : function(customers, response) {
        var tr = $('#list-table>tbody>tr');
        if(tr.length > 0) {
          tr.remove();
        }
        var tbody = $('#list-table>tbody');
        var i;
        for(i = 0; i < customers.length; ++i) {
          var customer = customers.at(i);
          tbody.append('<tr><td>' + customer.get('id') +
              '</td><td>' + customer.get('name') +
              '</td><td>' + customer.get('address') +
              '</td></tr>');
        }
      },
      error : function(customers, response) {
        alert('error: ' + response);
      }
    });
  });
});

table要素に結果を追加しているところまで示しているのでちょっと長いですが、その部分を削ると、実質Backbone.jsで行っているのは以下の処理だけです。

$(function() {
  $('#list-button').click(function(e){
    var customers = new Customers;
    customers.fetch({
      success : function(customers, response) {
        // 取得に成功した場合の処理
      },
      error : function(customers, response) {
        // エラーが発生した場合の処理
      }
    });
  });
});

まず、”new Customers”でCustomersコレクションのインスタンスを生成します。
次に、fetch()関数を呼び出して、サーバに一覧取得のリクエストを送ります。処理は非同期で行われるので、コールバック関数としてsuccessとerrorを指定します。successの第一引数に取得できたコレクションのインスタンスが渡されるので、そこからモデルのインスタンスを取得し、表示処理を行います。

9.3 指定したIDのCustomerの取得(viewアクション)

既存のCustomerを取得するviewアクションの場合、Backbone.jsは”/cakephp_service/customers/(id)”というパスでサーバにアクセスしてきます。そして、CakePHPは最後のidをviewアクションの引数に自動的に渡してくれます。ですので、viewアクション内では渡されたidでデータベースを検索し、取得されたCustomerをJSON形式で返すようにします。

class CustomersController extends AppController {
  public $name = 'Customers';
  function view($id) {
    $customer = $this->Customer->find('first', array(
        'conditions' => array('Customer.id' => $id)
    ));
    $this->set('customer', $customer);
    $this->set('_serialize', 'customer');
  }
}

Backbone.jsでは、idだけを指定したCustomerのインスタンスを作成して、fetch()を呼び出します。

$(function() {
  $('#update-read_button').click(function(e){
    var customer = new Customer({
      id : $('#update-read_id').val()
    });
    customer.fetch({
      success : function(customer, response) {
        $('#update-name').val(customer.get('name'));
        $('#update-address').val(customer.get('address'));
      },
      error : function(customers, response) {
        alert('error: ' + response);
      }
    });
  });
});

実際に送られたHTTPリクエストです。

GET /cakephp_service/customers/4f6f3d11-27d8-4a52-b68d-1b7c4e257404 HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) Gecko/20100101 Firefox/11.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
X-Requested-With: XMLHttpRequest
Referer: http://localhost/backbone_client/

HTTPレスポンスです。

HTTP/1.1 200 OK
Server: Apache/2.2.21 (Win32) mod_ssl/2.2.21 OpenSSL/1.0.0e PHP/5.3.8 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.8
Content-Length: 155
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json

{"Customer":{"id":"4f6f3d11-27d8-4a52-b68d-1b7c4e257404","name":"test","address":"tokyo","created":"2012-01-01 00:00:00","modified":"2012-01-01 00:00:00"}}

なぜCustomerモデルのparseメソッドをわざわざ定義したのかの理由がこのレスポンスにあります。レスポンスを見るとCustomerの中に属性が含まれています。このため、パースされたJSONは、”response.Customer”という形式になります。Backbone.jsは、デフォルトではresponse = Customerを期待しているので、以下のようにparseの処理を変更する必要があったのです。

parse : function(response) {
  if (response.Customer != undefined) {
    return response.Customer;
  }
  return response;
}

最後に

更新と削除の説明は省略しますので、直接、ソースコードを見てください。

開発をする際の注意として、CakePHPとBackbone.jsは同じドメインに置かなければなりません。JavaScriptはクロスドメインのリクエストが基本的に送れないので、当然といえば当然なのですが、つい、普通のWebサービスのつもりで手元でBackbone.jsのWebアプリを起動すると、CakePHPのサービスにアクセスできなくなります。(この場合OPTIONSというHTTPメソッドでリクエストが送られます)

まだまだ情報がない分野だけに、ここまでたどり着くのは結構苦労させられました。でも、作成してみた後にソースを眺めてみると、すっきりしていて、JavaScriptを中心としたアプリを作るならば、Backbone.jsやSpine.jsなどを使うことをおすすめします。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

Objective-Cの機能のネーミングは、わかりにくい気がします。今回は名前からは想像できない機能を持つ「カテゴリ」です。

カテゴリは既存のクラスを、継承することのなく機能拡張してしまう機能です。なんと恐ろしい。

@interface MyClass : NSObject
@end

@implementation MyClass
@end

@interface MyClass (MyCategory)
-(void)print;
@end

@implementation MyClass (MyCategory)
-(void)print;
{
  NSLog(@"Hello World!");
}
@end

これで、MyClassを拡張して、新たにprintメソッドを追加しています。@interfaceと@implementationを以下のように書くところがポイントです。

@interface 拡張したいクラスの名前 (カテゴリ名)
...
@implementation 拡張したいクラスの名前 (カテゴリ名)
...

同じファイルに基のクラス定義とカテゴリの定義を書いてしまうと、すごさがわかりずらいけど、このクラスの拡張はラインタイム時に行われるので、この定義を参照しているところだけでなく、すべてのコードに反映されます。
そして、これはNSObjectやNSStringに対してもできてしまう!!
機能としてはC#のpartialクラスが近いだろうけど、C#は基のクラスもpartialにしなければならないから、最初から拡張されると定義されたクラスにしかできない。でもObjective-Cのカテゴリは関係なく、どのクラスに対してもできてしまいます。

利用シーンとしては、C#のpartialと同様に、ツールによる生成部分とソースコードを分けるために使うというのがあります。後は、それこそ、継承せずに既存のクラスを拡張してしまうという使い方ですが、これをしてしまうとコードの可読性はおそらく悪くなるのでおすすめはしません。

おそらく多くの人にとって有用なのはGoogleのコーディング規約にもあるように、クラスが内部で利用するメソッドと、外部へ公開するメソッドに分割するためにカテゴリを使うという手法でしょう。
特にObjective-C 2.0からはカテゴリ名を無名にしたクラスエクステンションという機能により、これを実現できるようになりました。MyClassを新たにクラスエクステンションを用いて、公開メソッドとプライベートメソッドに分けて定義してみます。まずはヘッダーファイル(.h)です。
MyClass.h:

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
-(void)print;
@end

公開メソッドであるprintしか定義がありません。次に、実装ファイル(.m)です。
MyClass.m:

#import "MyClass.h"

@interface MyClass ()
-(void)innerMethod;
@end

@implementation MyClass
-(void)print
{
    [self innerMethod];
}
-(void)innerMethod
{
    NSLog(@"Hello World!");
}
@end

ここで注意するのが以下の部分です。

@interface MyClass ()
-(void)innerMethod;
@end

括弧内にカテゴリ名を記述しないことでクラスエクステンションとなります。そして内部メソッドであるinnerMethodを定義しています。@implementationには、公開メソッドであるprintと、プライベートメソッドであるinnerMethodの両方の実装を記述しています。これで、printのみが外部からアクセスでき、innerMethodにはアクセスできなくなります。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

selectorに続いて、関連の深いmessage forwardingについて。
あるオブジェクトから他のオブジェクトに処理の委譲をしたい場合、selectorを使うと以下のように書けます。

@interface DelegateClass : NSObject
-(void)hello;
@end

@implementation DelegateClass
-(void)hello
{
    NSLog(@"Hello");
}
@end

@interface MyClass : NSObject
@property(strong, nonatomic) DelegateClass *obj;
-(void)hello;
@end

@implementation MyClass
@synthesize obj;
-(id)init
{
    obj = [[DelegateClass alloc]init];
    return self;
}
-(void)hello
{
    if([obj respondsToSelector: @selector(hello)]) {
        [obj hello];
    }
}
@end

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        SEL aSelector = @selector(hello);
       
        MyClass *obj1 = [[MyClass alloc] init];
        [obj1 performSelector: aSelector];
    }
    return 0;
}

ここではMyClassのhelloメソッドの処理を、DelegateClassへ委譲しています。NSObjectのrespondsToSelectorで、そのオブジェクトがselectorに対応したメソッドを実装しているかを確認できます。
この方法だと、委譲するメソッドの数が増えるとMyClassにも同じようなことをいくつも書かなければいけないという問題があります。
ここで、message forwardingを使うと、2つのメソッド(forwardInvocationとmethodSignatureForSelector)ですべての処理を委譲できるようになります。

@interface DelegateClass : NSObject
-(void)hello;
@end

@implementation DelegateClass
-(void)hello
{
    NSLog(@"Hello");
}
@end

@interface MyClass2 : NSObject
@property(strong, nonatomic) DelegateClass *obj;
-(void)forwardInvocation:(NSInvocation *)anInvocation;
@end

@implementation MyClass2
@synthesize obj;
-(id)init
{
    obj = [[DelegateClass alloc]init];
    return self;
}
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
    if ([obj respondsToSelector: aSelector]) {
        return [obj methodSignatureForSelector: aSelector];
    }
    return [super methodSignatureForSelector: aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation;
{
    if([obj respondsToSelector: [anInvocation selector]]) {
        [anInvocation invokeWithTarget: obj];
    }
    else {
        [super forwardInvocation:anInvocation];
    }
}
@end

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        SEL aSelector = @selector(hello);
       
        MyClass2 *obj2 = [[MyClass2 alloc] init];
        [obj2 performSelector: aSelector];
    }
    return 0;
}

MyClass2にselectorでメッセージングした際、もしそのselectorに対応できるメソッドが無い場合、まずmethodSignatureForSelectorが呼び出されます。ここで、DelegateClassの対応するメソッド(今回はhelloメソッド)のメソッドシグネチャーを返します。次にforwardInvocationが呼び出されるので、実際にDelegateClassのオブジェクトのhelloメソッドを呼び出します。このやり方ですとDelegateClassに新しいメソッドが追加されてもMyClass2への変更はいらなくできます。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

Objective-Cのちょっと他には無い機能について、自分自身のメモのためにも書き留めておこうと思います。
まずは、selectorから。
C/C++の関数ポインタに近いですが、動的言語としての面も持ち合わせているObjective-Cらしい所もあり、Javaのリフレクション/イントロスペクションの方が近いかもしれません。

@interface MyClass : NSObject
-(void)hello;
@end

@implementation MyClass
-(void)hello
{
  NSLog(@"Hello");
}
@end

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        SEL helloSelector = @selector(hello);

        MyClass *aObj = [[MyClass alloc] init];
        [aObj performSelector: helloSelector];
    }
    return 0;
}

MyClassというクラスにhelloというメソッドがあります。これを、通常のメッセージングではなく、selector経由で呼び出しています。そのためには、まずメソッドhelloのselectorを取得します。

SEL helloSelector = @selector(hello);

“@selector(メソッド名)”でselectorを取得し、SELという特別なselectorを格納するための型の変数を宣言し、そこに代入します。
selectorを用いたメソッド呼び出しは、NSObjectのperformSelectorを使います。

[aObj performSelector: helloSelector];

Javaのリフレクション/インストロスペクションに近いというと、文字列でメソッドを呼び出したいですが、NSSelectorFromStringを用いると文字列でselectorを生成できます。

SEL helloStringSelector = NSSelectorFromString(@"hello");

MyClass *aObj = [[MyClass alloc] init];
[aObj performSelector: helloStringSelector];

このSEL型ですが、おもしろいのはどのクラス定義とも関連づけられていなく、純粋なメソッドの定義から作られているということです。どういうことかというと、以下のように異なるMyClassとMyClass2に対しても、同じメソッド定義ならば、同じselectorが使えます。

@interface MyClass : NSObject
-(void)hello;
@end

@implementation MyClass
-(void)hello
{
  NSLog(@"Hello");
}
@end

@interface MyClass2 : NSObject
-(void)hello;
@end

@implementation MyClass2
-(void)hello
{
  NSLog(@"Hello");
}
@end

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        SEL helloSelector = @selector(hello);

        MyClass *aObj = [[MyClass alloc] init];
        [aObj performSelector: helloSelector];
        MyClass2 *obj2 = [[MyClass2 alloc] init];
        [obj2 performSelector: helloSelector];
    }
    return 0;
}

Javaのリフレクションでは、java.lang.reflect.Methodは、クラス定義と紐づいているのでこういったことはできません。

そのためか、引数を持つメソッドの場合、引数を持つということも含めて指定してselectorを作成する必要があります。

@interface MyClass : NSObject
-(void)hi:(NSString*)name;
@end

@implementation MyClass
-(void)hi:(NSString*)name;
{
    NSLog(@"Hi, %@", name);
}
@end

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        MyClass *aObj = [[MyClass alloc] init];

        SEL hiSelector = @selector(hi:);
        [aObj performSelector: hiSelector withObject: @"John"];

        SEL hiStringSelector = NSSelectorFromString(@"hi:");
        [aObj performSelector: hiStringSelector withObject: @"John"];
    }
}

@selector(hi:)のようにコロン(:)を一つ含めていることで、引数を一つ持つメソッドであることを宣言しています。引数が二つになればコロンが二つになります。

@interface MyClass : NSObject
-(void)hi:(NSString*)name name2:(NSString*)name2;
@end

@implementation MyClass
-(void)hi:(NSString*)name name2:(NSString*)name2
{
    NSLog(@"Hi, %@ and %@", name, name2);
}
@end

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        MyClass *aObj = [[MyClass alloc] init];

        SEL hiTwoArgsSelector = @selector(hi:name2:);
        [obj2 performSelector: hiTwoArgsSelector withObject: @"John" withObject: @"Bill"];

        SEL hiTwoArgsStringSelector = NSSelectorFromString(@"hi:name2:");
        [obj2 performSelector: hiTwoArgsStringSelector withObject: @"John" withObject: @"Bill"];
    }
}

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

Windows 8 consumer previewがリリースされたということもあり、少しずつMetroスタイルアプリの開発をチェックしています。
Learn to build Metro style apps
DirectX(Direct2D/Write/3D)とMetroを組み合わせたサンプルも公開されています。
Create your first Metro style app using DirectX
DirectXのAPIを呼んでいるところは変わりがないので、さほど難しくはなさそうです。
気をつけなければいけないのは、Win32 APIが一部利用できない点。
Win32 and COM for Metro style apps
どういう基準で選んでいるのかわからないけど、ネットワーク系はWinsockもWinHTTPも利用できない模様。Cryptographyも使えません。でも、TSF、UIAなどはOKとなっています。
TouchMindやGame of Life 3Dは移植できそうだけど、winsshlibはWinsockとCryptographyをメインで使っているから、だいぶ書き変えないといけないな…。

Archive for the ‘Article’ Category:

CakePHP + Backbone.jsで作るJavaScript Web Application ~ JSONフォーマットの変更 ~

Sunday, April 15th, 2012

Channel9にて公開されているC++ and Beyond 2011の動画にて、Andrei Alexandrescu、Scott Meyers、Herb Sutterの三名が、C++ and Beyond 2011の参加者からの質問に回答している。
その中で4:34からの「On shared_ptr performance and correctness」が興味深い。
質問の主旨としては、shared_ptrを関数やメソッドへ渡す場合、const参照で渡すべきかどうかということらしい。つまり、

void func(std::shared_ptr<std::string> s)
{
  ...
}

int main(int argc, char* argv[])
{
  std::shared_ptr<std::string> s = ...;
  func(s);
}

とするか、const参照にして以下のように渡すか、

void func(const std::shared_ptr<std::string> &s)
{
  ...
}

int main(int argc, char* argv[])
{
  std::shared_ptr<std::string> s = ...;
  func(s);
}

どちらがよいかということである。違いが出てくるのは速度である。const参照で渡さない場合、C/C++は値渡し(pass by value)が基本なので、関数の引数として渡すだけでshared_ptrのコピーが行われ、参照カウンタが+1されてしまう。対して、const参照で渡した場合、当然ながら参照渡し(pass by reference)なので+1されない。
Channel9の動画の中では、const参照で渡しなさいとのことである。

実際に、両方のパターンをQueryPerformanceCounterを使って時間計測してみたが、const参照で渡すケースはさすがに動作が速く、QueryPerformanceCounterでも測れませんでした。
実際のアプリケーションでどれだけの性能差が出るかは微妙ですが、Channel9の動画の質問者の環境では10%未満の差とのことです。
性能が向上するのは確かなので、shared_ptrを渡す際はconst参照で渡すが正しいようです。