目次
この回では、AngulerJSを少し本格的に使用して会員管理システムを構築する。
ここで用いるAngulerJSの機能および関連項目は以下のようなものである。
多くのデータベースアプリケーションが取り入れているデザインパターンの一つ。
それぞれの単位が責任を分担することで、見通しの良いすっきりとした構造のアプリケーションを構築することができる。
会員管理システムのMVCの構成は以下のようになる。
依存性注入とは(Wikipediaによると)マーティン・ファウラーによる造語で、あるコンポーネントAの中で別なコンポーネントBを使用する際、コンポーネントBをコンポーネントAの中で内部的に生成するのではなく、外部のインターフェイスなどを使って間接的に導入(これを注入という)することにより、コンポーネント間の依存関係を薄くし、単体テストを実施しやすくする技術である(とのこと)。
と言われても、何を言っているのかよくわからないかもしれないが、私の解釈によると次のようなことである。
例を挙げて説明する。
会員リストを作成するクラスMemberListのmakeList()メソッドの中で、会員の住所から郵便番号を得るZipCodeManagerというクラスを使っているとしよう。
つまり、
var MemberList=function(){
this.members=[];
this.prototype.makeList=function(){
var zcm=new ZipCodeManager(); // ←ココ
for(var i=0;i<this.members.length;i++)){
int z=zcm.getZipCode(this.members[i]); // 会員this.members[i]の郵便番号を得るはず・・・
// ...
}
}
}
問題となるのは「←ココ」と示した部分である。ここでは、newで新しいオブジェクトを生成して使っている。
ここでもし、ZipCodeManagerクラスを作っている人がなかなか開発が進まず、MemberListクラスをテストするときに間に合わなかったとしよう。このような場合は、MemberList#makeList()のテストを行うことができない。なぜなら、ZipCodeManagerのインスタンスを生成できないからである。
しかし、プログラムコードが以下のようになっていたとしよう。
var MemberList=function(){
this.members=[];
this.prototype.makeList=function(ZipCodeManager zcm){
for(var i=0;i<this.members.length;i++)){
int z=zcm.getZipCode(this.members[i]); // 会員this.members[i]の郵便番号を得るはず・・・
// ...
}
}
}
このコードでは、makeList()を呼び出すとき引数としてZipCodeManagerのインスタンスを渡している。
この場合でもやはりZipCodeManagerがなければ関数を実行することはできないわけであるが、外部からインスタンスを渡してくるということは、内部で生成するよりは依存関係が薄くなっている。
この場合、ZipCodeManagerの代わりをする別なオブジェクト(このようなオブジェクトをモックという)を引数として渡してあげれば、makeList()関数のテストを実施することが可能となる。
このように、外部からオブジェクトを入れてあげるような形を、オブジェクトの注入(Inject)と呼ぶ。またそれにより、依存性を低減することを依存性注入(DI)と呼ぶ(ということらしい)。
個人的には、ZipCodeManagerのモックのクラスを準備してリンクすれば最初のコードでもmakeList()関数のテストは可能になると思うのだが、依存性としてはどちらが低いかという後者の方が低くなっているのだとう。
DIの考え方は、コンポーネント間の依存性をできるだけ低く抑えることで、テストの実施を容易にしてソフトウェアの信頼性と保守性を向上させようということを狙っている。
AngularJSにおけるDIとは、あるモジュールの中のコントローラやサービスなどの中で、別なコントローラやサービスを使用するための手法を言う。
一般的なクラスの設計をする際、あるクラスAから別なクラスBのメソッドを呼び出す場合、クラスAはクラスBを知っている必要がある。この場合、クラスAの中にクラスBのインスタンスを保持しておくことで、これを実現できる。
しかし、この方法は2つのクラスの依存性を高めていることになり、上記の依存性の低減という観点から見ればあまり良い方法ではない。
そこでAngularJSでは、各コントローラやサービス内の関数の引数で別なコントローラやサービスの名称を列挙することで、そのコントローラやサービスを利用可能としている。
具体的には次のようなことである。
var app=angluar.module("MyApp",[]); // アプリケーションモジュールの生成
app.factory("MyService",function(){ // appにサービスを定義
// ... 何かのサービスの処理
});
app.controller("MyController",function($scope,MyService){ // appにコントローラを定義
// ... MyServiceを使う処理
});
MyControllerの中でMyServiceを使用するのに、特に何か宣言などをする必要はない。ただ単に、コントローラの引数の中にMyServiceと書けばよい。名前が一致するサービスを勝手にAngularJSが見つけて使ってくれるのである。
こんな仕様はどうなの??、と私も最初は思ったのだが、使ってみると意外と便利である。
そもそもAngularJSでは、変数の双方向バインディングとかワッチとか、裏で何やってるかよくわからないけど使うと便利、というものが多くある。このDIの仕様もその一つだと思って使えば、まぁ納得もできるだろう。
賛否両論があるとは思うが、少なくともAngularJSでは、このような方法でDIを実現している。
名前空間はできるだけ分けた方が良い。これは常識。
しかし、JavaScriptには次の2つの名前空間しか存在しない。
グローバル名前空間とは、どこでも使える名前空間であり、これはすなわち名前空間を使わないのと同意である。
ということは、JavaScriptの名前空間は関数名前空間しかない、ということになる。
つまり、JavaScriptで変数名を分けて使用したい場合、それを関数の中に書くしかないのである。
しかし、クラスの宣言や初期化時の処理など、特に関数を作らないで行う処理はどうしてもグローバル名前空間を使用することになる。
そこで登場するのが「即時関数」である。それは次のようにして宣言する。
(function(){
// ... ここに宣言や定義を書く。
}());
JavaScriptでは関数もまた1つの変数である。
上記の構文はややわかりにくいが、次のように分解するとわかりやすくなる。
var F=function(){ }; // 関数を定義して変数Fに入れる
F(); // 関数Fを呼び出す
この2つを同時にするのが即時関数である。
つまり、関数を定義すると同時に呼び出すのである。
即時関数に引数を渡すこともできる。
var x=3; // グローバル名前空間の変数x
var y=(function(i){ // 即時関数を実行し結果をグローバル名前空間の変数yに入れる
var a=i*2;
return a;
}(x));
この場合、yには6が入る。
AngularJSに限らず、HTML5で作られるアプリはSPA(Single Page Application)が多い。
SPAでは、画面の切り替え時にページ遷移を行わず、<div>タグの表示・非表示を切り替えることでユーザに見せる画面を切り替えることが多い。
しかし、この方法ではユーザにとっては画面が切り替わっても、ブラウザは同一ページを表示していることになるから、もしユーザがブラウザの戻るボタンを押すと、元の画面に戻るのではなく、このページの前に表示していたページに戻ることになる。
AngularJSではこれを回避するためngRouteというモジュールを提供している。
ngRouteでは、ページの一部に別なページを表示することで画面の切り替えを行うが、その際にブラウザのヒストリー情報を適切に書き換えて、ユーザが戻るボタンを押してもきちんと前の画面に戻ることができるように制御してくれる。
ngRouteモジュールはangular.jsとは別のソースに分かれており、利用時にはanguler-route.jsを読み込む必要がある。
AngulerJSでMVCパターンを実現する際、M(モデル)を担当するのがサービスである。
サービスは、その内部にデータを持ち、必要に応じてそのデータにアクセスするための機能を提供する。
AngulerJSのサービスを生成する方法はいくつか準備されているが、ここではfactory()メソッドを使う方法を紹介する。
app.factory("MyService",function(){
var ms={}; // サービスの実体
// ...サービスの処理
return ms;
});
このように書くことで、MyServiceという名前のサービスがアプリケーションモジュールappに追加される。
以降はDIにより、MyServiceという名前でこのサービスにアクセスすることが可能となる。