Skip to content

Latest commit

 

History

History
1101 lines (804 loc) · 63.1 KB

README-ja-jp.md

File metadata and controls

1101 lines (804 loc) · 63.1 KB

AngularJS in Patterns

このドキュメントはAngularJS in Patternsの日本語翻訳版です。

目次

要旨

新しいことを学ぶ際のもっともよい方法の1つとして、既に知っているものがそこでどのように使われているかを観察するという方法があります。 このドキュメントは読者にデザインやアーキテクチャのパターンに親しんでもらおうとして書かれているものではありませんので、オブジェクト指向やデザイン・パターン、アーキテクチャ・パターンについての基本的な理解をしておくことをおすすめします。 このドキュメントの目的は、AngularJSやAngularJSのシングル・ページ・アプリケーションにどれだけ様々なソフトウェア・デザインやアーキテクチャのパターンが採用されているかを述べることです。

はじめに

このドキュメントはAngularJSの概要を簡単に見ていくところから始まります。「AngularJSの概要」ではAngularJSの主なコンポーネントとして、ディレクティブ、フィルタ、コントローラ、サービス、スコープを見ていきます。2番目のセクションでは、フレームワークの内部で利用されている別のデザインとアーキテクチャのパターンを解説していきます。いくつかのコンポーネントで利用されているパターンがあった場合は、言及していきます。

最後のセクションでは、AngularJSで作られているシングル・ページ・アプリケーションでよく使われているいくつかのアーキテクチャ・パターンを解説します。

AngularJSの概要

AngularJSはCRUDなシングル・ページ・アプリケーション(SPA)開発の基盤を提供する目的で作られたGoogle製のJavascriptフレームワークです。

SPAとは一度ロードされたら以後ページの全要素を再読込する必要なく、ユーザの操作を受け付けるウェブ・アプリケーションです。これはデータ、テンプレート、スクリプト、スタイルなど全てのリソースを最初のリクエスト時に、または、それが必要になった時にロードすることを意味します。ほとんどすべてのCRUDなアプリケーションは共通の特性と要求を持っているので、AngularJSはそれらのアプリケーションが必要とするものをまとめてすぐに使える最高セットを提供しようとしています。AngularJSのいくつかの重要な特徴は下記のとおりです:

  • 双方向バインディング
  • 依存性の注入
  • 関心の分離
  • テストの容易性
  • 抽象化

関心の分離はそれぞれのAngularJSアプリケーションを別々のコンポーネント(下記)に分けることで達成されています。

  • パーシャル
  • コントローラ
  • ディレクティブ
  • サービス
  • フィルタ

これらのコンポーネントはそれぞれのモジュールの中でグループ化することができるので、高度な抽象化がやりやすくなりますし、複雑な処理も扱いやすくなっています。それぞれのコンポーネントはアプリケーションの必要なロジックを隠蔽します。

パーシャル

パーシャルはHTMLの文字列です。パーシャルはエレメントまたはアトリビュートの中にAngularJSのエクスプレッションを含むことがあります。AngularJSとその他のフレームワークの違いの1つは、AngularJSのテンプレートがHTMLに変換される前の中間的なフォーマット(例えば、mustache.jsやhandlebarsのようなもの)ではないということです。

SPAは最初に index.html ファイルを読み込みます。AngularJSの場合、このファイルに標準のHTMLアトリビュート、エレメント、コメントに加えカスタムのものも含みます。この段階で、アプリケーションの設定と準備をします。これに続くユーザのアクションは、例えば、フレームワークによって提供されるデータ・バインディングを通すなどして、アプリケーションの他のパーシャルの読み込みや、状態の変更のみで対応します。

パーシャルのサンプル

<html ng-app>
 <!-- BodyタグはngControllerディレクティブによって機能追加されます  -->
 <body ng-controller="MyController">
   <input ng-model="foo" value="bar">
   <!-- ng-clickディレクティブ付きのButtonタグと
          "{{ }}"マークアップで囲われた 'buttonText'
          エクスプレッションです -->
   <button ng-click="changeFoo()">{{buttonText}}</button>
   <script src="angular.js"></script>
 </body>
</html>

AngularJSのエクスプレッションでパーシャルはユーザとの対話の中でどのアクションを実行すべきかを定義します。上記の例では、 ng-click の値は、現在の scopechangeFoo メソッドが実行されることを表しています。

コントローラ

AngularJSのコントローラはユーザとウェブ・アプリケーションとの双方向のやりとり(マウスイベント、キーボードイベントなど)を扱うJavaScriptの関数です。 scope に、メソッドを追加することで実現します。コントローラに必要なコンポーネントはAngularJSの依存性の注入によって提供されます。コントローラはまた scope にデータを追加することで、パーシャルに model を提供する責務を負います。このデータを view model と考えることができます。

function MyController($scope) {
  $scope.buttonText = 'Click me to change foo!';
  $scope.foo = 42;

  $scope.changeFoo = function () {
    $scope.foo += 1;
    alert('Foo changed');
  };
}

例えば、もし上記のサンプルコントローラと、前述のパーシャルをつなげた場合、ユーザはアプリケーションといくつかの方法でコミュニケーションができるようになります。

  1. インプット・ボックスにタイプすることで、 foo の値を変更します。双方向バインディングによって、この変更はすぐに foo の値に反映されます。
  2. Click me to change foo! と表示されているボタンをクリックすることで foo の値を変更します。

全てのカスタム・エレメント、コメント、また、クラスは事前に定義されている場合、AngularJSの ディレクティブ として認識されます。

スコープ

AngularJSではスコープはパーシャルに露出したJavaScriptのオブジェクトです。スコープはプリミティブ、オブジェクト、メソッドなど異なるプロパティを含んでいます。スコープに追加された全てのメソッドはスコープと関連付けられたパーシャルの中でAngularJSのエクスプレッションによって評価され実行されます。また、スコープへの参照を持つコンポーネントから直接呼び出されます。適切な ディレクティブ を使うことでスコープに追加されたデータはビューにバインディングされ、パーシャルの中の変更がスコープのプロパティに反映されます。また、プロパティの変更がパーシャルに反映されます。

AngularJSアプリケーションのスコープのもう一つの重要な特性は、それがプロトタイプ・チェーンと結びついていることです( 明示的に 分離 されたものを除きます)。これにより、子のスコープは親のスコープのメソッドを実行することができます。この場合のメソッドは子のスコープの直接、または関節のプロトタイプのプロパティだからです。

スコープの継承は次の例で説明します:

<div ng-controller="BaseCtrl">
  <div id="child" ng-controller="ChildCtrl">
    <button id="parent-method" ng-click="foo()">Parent method</button>
    <button ng-click="bar()">Child method</button>
  </div>
</div>
function BaseCtrl($scope) {
  $scope.foo = function () {
    alert('Base foo');
  };
}

function ChildCtrl($scope) {
  $scope.bar = function () {
    alert('Child bar');
  };
}

div#childChildCtrl と結びついていますが、 ChildCtrl に注入されたスコープは親のスコープ( BaseCtrl に注入されたスコープ )からプロトタイプ継承をしているので、 foo メソッドは button#parent-method でアクセス可能になっています。

ディレクティブ

AngularJSでは全てのDOM操作がなされるべき場所です。目安としては、コントローラがDOM操作を含む場合、新しいディレクティブを作るか、すでにあるディレクティブが必要なDOM操作ができるようにするためのリファクタリングをするべきでしょう。 全てのディレクティブは名前と関連するロジックを持っています。最もシンプルなケースとしては、ティレクティブは名前と必要なすべてのロジックをカプセル化するための postLink 関数を持ちます。少し複雑なケースでは、下記のようなたくさんのプロパティを持ちます:

  • テンプレート
  • コンパイル関数
  • リンク関数
  • などなど...

ディレクティブの名前を利用することで、パーシャルの中で利用することができます。

例:

myModule.directive('alertButton', function () {
  return {
    template: '<button ng-transclude></button>',
    scope: {
      content: '@'
    },
    replace: true,
    restrict: 'E',
    transclude: true,
    link: function (scope, el) {
      el.click(function () {
        alert(scope.content);
      });
    }
  };
});
<alert-button content="42">Click me</alert-button>

上記の例では、 <alert-button></alert-button> タグはボタンエレメントに置換えられます。ユーザがボタンをクリックした時に、文字列の 42 がアラートとして表示されます。

このドキュメントの意図はAngularJSの完全なAPIの解説をすることを意図しているわけではないので、ディレクティブの説明はこの辺りでやめておきます。

フィルタ

AngularJSのフィルタはデータをフォーマットするために必要なロジックをカプセル化する責務を負っています。普通、フィルタはパーシャルの中で利用されますが、コントローラやディレクティブ、 サービス 、また依存性の注入を用いて他のフィルタの中で利用することも可能です。

与えられた文字列を全て大文字にするサンプルフィルタの定義です。

myModule.filter('uppercase', function () {
  return function (str) {
    return (str || '').toUpperCase();
  };
});

パーシャルの中ではUnixのパイプ記法でこのフィルタを使うことができます:

<div>{{ name | uppercase }}</div>

コントローラの中では次のように利用します:

function MyCtrl(uppercaseFilter) {
  $scope.name = uppercaseFilter('foo'); //FOO
}

サービス

上述のコンポーネントに属さない全てのロジックはサービスに格納されるべきです。普通、サービスは問題領域(ドメイン)固有のロジックや、永続化に関わるロジック、XHR、ウェブソケットなどをカプセル化します。アプリケーションの中のコントローラが "肥大化" した際には、何度も利用されるコードをサービスに移し替えるべきです。

myModule.service('Developer', function () {
  this.name = 'Foo';
  this.motherLanguage = 'JavaScript';
  this.live = function () {
    while (true) {
      this.code();
    }
  };
});

サービスは依存性の注入を扱えるどのコンポーネント(コントローラ、多のサービス、フィルター、ディレクティブ)にも注入できます。

function MyCtrl(developer) {
  var developer = new Developer();
  developer.live();
}

AngularJSのパターン

次の2つのセクションで、伝統的なデザインとアーキテクチャのパターンがAngularJSのコンポーネントの中でどのように構成されているのかを見ていきます。

最後の章ではAngularJSに限らずシングル・ページ・アプリケーションで頻繁に使われるアーキテクチャのパターンについて見ていきます。

サービス

シングルトン

シングルトン・パターンはクラスのインスンタンスを1つに制限するデザイン・パターンです。システムを通してアクションを調整するオブジェクトが1つで良い場合に役に立ちます。この考え方はしばしばシステムに対して、オブジェクトを1つにして効率的に稼働させることや、オブジェクトの数を一定の数以下にを制限することに一般化されます。

下記のUMLダイアグラムはシングルトンのデザイン・パターンを表しています。

Singleton

依存性がコンポーネントに必要とされる際に、AngularJSは次のアルゴリズムを使って依存性の解決を行っています:

  • 依存性の名前で語彙のクロージャの中に定義されているハッシュ・マップを検索します(プライベートにアクセスできるようになっています)。
  • 依存性がAngularJSの中に存在する場合は、それを必要としているコンポーネントにパラメタとして渡します。
  • 依存性が存在しない場合は:
    • AngularJSはプロバイダのファクトリ・メソッド( $get )を用いてその依存性をインスタンス化します。 依存性のインスタンス化は必要に応じて、同じアルゴリズムを用いて再帰的に行われます。このプロセスは循環依存を起こします。
    • AngularJSはそのインスタンスを上述のハッシュ・マップにキャッシュします。
    • AngularJSは必要としているコンポーネントにパラメタとしてそのインスタンスを渡します。

getService メソッドが実装されている部分のソースコードを見たほうが良いでしょう。

function getService(serviceName) {
  if (cache.hasOwnProperty(serviceName)) {
    if (cache[serviceName] === INSTANTIATING) {
      throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- '));
    }
    return cache[serviceName];
  } else {
    try {
      path.unshift(serviceName);
      cache[serviceName] = INSTANTIATING;
      return cache[serviceName] = factory(serviceName);
    } catch (err) {
      if (cache[serviceName] === INSTANTIATING) {
        delete cache[serviceName];
      }
      throw err;
    } finally {
      path.shift();
    }
  }
}

サービスは一度しかインスタンス化されないので、全てのサービスをシングルトンと考えることができます。キャッシュはシングルトンのマネージャと考えることができます。上記のUMLダイアグラムと少し違いがあります。コンストラクタ関数の中のシングルトンオブジェクトにスタティックでプライベートな参照を保つ代わりに、シングルトン・マネージャ(上記のコードの中の cache )の中に参照を保ちます。

このように、サービスは実際にはシングルトンですが、シングルトン・パターンを通して実装されているわkではありません。これは、一般的な実装に比べていくつかの利点があります。

  • テストをしやすくします。
  • シングルトンオブジェクトの生成をコントロールできます(私達のケースでは、IoCコンテナがシングルトンを遅延インスタンス化することでコントロールしています)。

このトピックに関する更に一歩踏み込んだ議論のために、Google Testing blogのMisko Heveryの 記事 を考慮にいれましょう。

ファクトリ・メソッド

ファクトリ・メソッド・パターンは生成のパターンです。生成のパターンは生成するクラス指定のないオブジェクトを生成する際に生じる問題をうまく扱うためにファクトリ・メソッドを利用します。コンストラクタではなく、インターフェイス(抽象クラス)で指定されているファクトリメソッド、実装クラス(具象クラス)に実装されているファクトリメソッド、また、継承される可能性もあるのですが、ベースクラスに実装されているファクトリメソッドを通してオブジェクトが生成される場合に利用されます。

Factory Method

次のスニペットを考えてみましょう:

myModule.config(function ($provide) {
  $provide.provider('foo', function () {
    var baz = 42;
    return {
      //ファクトリ・メソッド
      $get: function (bar) {
        var baz = bar.baz();
        return {
          baz: baz
        };
      }
    };
  });
});

新しい "プロバイダ" を定義するために、上記のコードで config コールバックを利用しています。プロバイダは $get メソッドを持っているオブジェクトです。JavaScriptでインターフェイスを持たず、ダックタイプされているので、このようにプロバイダのファクトリ・メソッドを名付ける慣例があります。

サービス、フィルタ、ディレクティブ、コントローラはそれぞれコンポーネントのインスタンスを生成する責務を負うプロバイダ( $get を持つオブジェクト)を持ちます。

AngularJSの実装をもう少し深く探っていくことができます:

//...

createInternalInjector(instanceCache, function(servicename) {
  var provider = providerInjector.get(servicename + providerSuffix);
  return instanceInjector.invoke(provider.$get, provider, undefined, servicename);
}, strictDi));

//...

function invoke(fn, self, locals, serviceName){
  if (typeof locals === 'string') {
    serviceName = locals;
    locals = null;
  }

  var args = [],
      $inject = annotate(fn, strictDi, serviceName),
      length, i,
      key;

  for(i = 0, length = $inject.length; i < length; i++) {
    key = $inject[i];
    if (typeof key !== 'string') {
      throw $injectorMinErr('itkn',
              'Incorrect injection token! Expected service name as string, got {0}', key);
    }
    args.push(
      locals && locals.hasOwnProperty(key)
      ? locals[key]
      : getService(key)
    );
  }
  if (!fn.$inject) {
    // これは配列でなければいけないことを意味しています
    fn = fn[length];
  }

  return fn.apply(self, args);
}

上記の例から、 $get メソッドが実際に利用されていることを知ることができます:

instanceInjector.invoke(provider.$get, provider, undefined, servicename)

上記のスニペットは instanceInjectorinvoke メソッドを最初の引数にサービスのファクトリ・メソッド( $get )を指定して呼んでいます。 invokeメソッドの中では、最初の引数にファクトリメソッドを指定して annotate が呼ばれています。アノテートはAngularJSの依存性の注入メカニズムを通して全ての依存性を解決します。全ての依存性の解決ができた時、ファクトリ・メソッドが呼ばれます: fn.apply(self, args)

上記のUMLダイアグラムの観点から考えると、プロバイダを "ConcreteCreator" と呼ぶことができます。そして、実際のコンポーネントは作られた "Product" となります。

ファクトリ・メソッドの間接性にファクトリ・メソッドを使ういくつかの利点があります。この方法で、フレームワークは新しいコンポーネントを生成する際の基本的な中身に注意を払うことができます:

  • コンポーネントがインスタンス化される最も適切なタイミング
  • コンポーネントに必要とされるすべての依存性の解決
  • コンポーネントが持つことを許されているインスタンスの数(サービスとフィルタは1つ。コントローラは複数)

デコレータ

デコレータ・パターン(アダプタ・パターンの別名でもあるラッパーとしても知られています。)は個別のオブジェクトに静的であっても動的であっても同じクラスの他のオブジェクトに影響をあたえることなく振る舞いを追加するデザイン・パターンです。

Decorator

AngularJSは既に存在するサービスの機能を追加したり、強化するための簡単な方法を提供しています。 $providedecorator メソッドを使うことによりカスタムのサービスやサード・パーティで使われているサービスに "ラッパー" を作ることができます:

myModule.controller('MainCtrl', function (foo) {
  foo.bar();
});

myModule.factory('foo', function () {
  return {
    bar: function () {
      console.log('I\'m bar');
    },
    baz: function () {
      console.log('I\'m baz');
    }
  };
});

myModule.config(function ($provide) {
  $provide.decorator('foo', function ($delegate) {
    var barBackup = $delegate.bar;
    $delegate.bar = function () {
      console.log('Decorated');
      barBackup.apply($delegate, arguments);
    };
    return $delegate;
  });
});

上記の例では foo という名の新しいサービスを定義しています。 config のコールバックは、最初の引数をデコレートしたいサービス名である foo として $provide.decorator を呼び出しています。2番目の引数は実際のデコレーションを実装しているファクトリ関数です。 $delegate はオリジナルサービス foo への参照を持っています。AngularJSの依存性の注入メカニズムを使うことにより、ローカルな依存性への参照はコンストラクタ関数の最初の引数として渡されます。 bar メソッドを上書きすることによってサービスをデコレートします。実際のデコレーションは単に bar でもう一つの console.log ステートメント - console.log('Decorated'); を実行するように拡張することです。その後、オリジナルの 'bar' メソッドを適切な文脈で利用します。

サード・パーティの機能を変更する必要がある場合特にこのパターンは役に立ちます。複数の似たようなデコレーションが必要となった時(複数のメソッドのパフォーマンス計測、認証、ログ出力など)、複製がたくさんでき、DRYの原則を破ってしまいます。そのような場合にはアスペクト指向プログラミング(AOP)を取り入れるとよいでしょう。AngularJSで利用できるAOPフレームワークとしては、分かる範囲では唯一、 github.com/mgechev/angular-aop があります。

ファサード

ファサードはクラス・ライブラリのような多くのコードにシンプルなインターフェイスを提供するオブジェクトです。ファサードは次のことができます:

  1. ソフトウェア・ライブラリを理解しやすく、使いやすくします。またテストをしやすくします。ファサードはよく使われるタスクを実行するための使いやすいメソッドを持つからです;
  1. 同じ理由から、ライブラリを読みやすくします。
  1. 外部のコードのライブラリの処理に対する依存性を減らします。なぜなら、ほとんどのコードはファサードを使うのでシステム開発の際の柔軟性を許容します。
  1. うまくデザインされていないAPI群を、(タスクが必要とする単位で)よくデザインされたAPIとしてラップします。

Facade

AngularJSにはいくつかのファサードがあります。高レベルのAPIを提供された機能に追加したいとき、実際にファサードを作ることになります。

例えば、 XMLHttpRequest のPOSTリクエストをどのように作るか見て行きましょう:

var http = new XMLHttpRequest(),
    url = '/example/new',
    params = encodeURIComponent(data);
http.open("POST", url, true);

http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.setRequestHeader("Content-length", params.length);
http.setRequestHeader("Connection", "close");

http.onreadystatechange = function () {
  if(http.readyState == 4 && http.status == 200) {
    alert(http.responseText);
  }
}
http.send(params);

しかし、このデータをポストしたいとき、AngularJSの $http サービスを使うことができます:

$http({
  method: 'POST',
  url: '/example/new',
  data: data
})
.then(function (response) {
  alert(response);
});

また、このように書いても同じです:

$http.post('/someUrl', data)
.then(function (response) {
  alert(response);
});

2番目の例は、与えられたURLでHTTP POSTリクエストを作る様に設定されたバージョンです。

更に高レベルの抽象化は $http サービスをもとに構築された $resource で行うことができます。このサービスに関しては、 アクティブ・レコードプロキシ のところでもう少し深く見ていきます。

プロキシ

プロキシの最も一般的な形は、何か別のものに対するインターフェイスとしての振る舞うクラスの働きです。プロキシは、通信接続、メモリ上の大きなオブジェクト、ファイル、複製するのが不可能だったりコストが掛かり過ぎるその他のリソースなどいろいろなもののインターフェイスになることができます。

Proxy

プロキシは3つの種類に分けることができます:

  • バーチャル・プロキシ
  • リモート・プロキシ
  • プロテクション・プロキシ

この副章では、AngularJSのバーじゃる・プロキシの実装を見ていきます。

下記のスニペットでは、 User という名の $resource インスタンスの get メソッドを呼んでいます:

var User = $resource('/users/:id'),
    user = User.get({ id: 42 });
console.log(user); //{}

console.log は空のオブジェクトを出力します。 AJAXリクエストは、 User.get が呼ばれた時点で非同期で処理されていて、 console.log が呼ばれた時点ではまだ実際のユーザが準備されていないのです。 User.get はGETリクエストをし、空のオブジェクトを返しますが、参照は持ち続けています。このオブジェクトをバーチャル・プロキシ(単に、プレースホルダーとも)考えることができます。クライアントがサーバからレスポンスを受け取ると実際のデータが格納されます。

これはAngularJSでどのように使われるのでしょうか? 次のスニペットを考えてみましょう:

function MainCtrl($scope, $resource) {
  var User = $resource('/users/:id'),
  $scope.user = User.get({ id: 42 });
}
<span ng-bind="user.name"></span>

上記のスニペットが事項された直後、 $scopeuser プロパティは空のオブジェクト( {} )になります。 user.name はundefinedとなり何もレンダリングされません。内部ではAngularJSはこの空のオブジェクトに参照を保っています。サーバがGETリクエストのレスポンスを返すと、AngularJSはサーバから受け取ったデータをオブジェクトに格納します。次の $digest ループでAngularJSは $scope.user の変更を検知し、ビューの更新に移ります。

アクティブ・レコード

アクティブ・レコードはデータと振る舞いを持つオブジェクトです。普通、アクティブ・レコード内のほとんどのデータは永続的です。アクティブ・レコード・オブジェクトの責務はデータの生成、更新、取得、削除をするためのデータベースとのやりとりを適切に行うことです。この責務を更に低レベルのオブジェクトに委譲することはありますが、アクティブ・レコード・オブジェクトのインスタンスや静的メソッドの呼び出しはデータベースとのやりとりをもたらします。

Active Record

AngularJSは $resource と呼ばれるサービスを定義しています。 AngularJS(1.2+)ではAngularJSのコアの外部モジュールとして配布されています。

AngularJSのドキュメントによると $resource は:

RESTFullサーバ・サイド・データ構造とやりとりするためのリソース・オブジェクトを生成するファクトリ。 返却されたリソース・オブジェクトは低レベルの$httpサービスを直接操作する必要なく、高レベルの振る舞いを提供するアクションを持っています。

$resource がどのように使われているかしめします:

var User = $resource('/users/:id'),
    user = new User({
      name: 'foo',
      age : 42
    });

user.$save();

$resource の呼び出しはモデルインスタンスのコンストラクタ関数を生成します。それぞれのモデルインスタンスはCRUDオペレーションに応じたメソッドを持つことになります。

このように、コンストラクタ関数と静的メソッドを使います:

User.get({ userid: userid });

上記のコードはすぐに空のオブジェクトを返し、それに対して参照を持ち続けます。レスポンスが成功して、パースされるとAngularJSは受け取ったデータをオブジェクトに格納します(参考: プロキシ )。

$resource についての詳細は、 The magic of $resourceAngularJS' documentation で確認できます。

Martin Fowlerがこのように宣言しているように:

アクティブ・レコード・オブジェクトの責務はデータの生成、更新、取得、削除をするためのデータベースとのやりとりを適切に行うことです。...

$resource はデータベースではなくRESTfulサービスとのやりとりをするので、アクティブ・レコード・パターンそのままの実装ではありません。そうは言っても、 "アクティブ・レコードのようなRESTFulコミュニケーション" と考えることができます。

傍受フィルタ

ウェブページのリクエストの際の共通の事前処理と事後処理タスクを実装するために構成可能なフィルタ・チェーンを作成する

Composite

HTTPリクエストの際に、事前処理、または、事後処理、またはその両方をしたい時があります。傍受フィルタを使うと、ログ出力、セキュリティまたリクエストのボディやヘッダによって影響を受ける関心事に対応するために、HTTPリクエストやレスポンスに事前・事後プロセスを追加することができます。基本的に傍受フィルタ・パターンはフィルタのチェーンを含みます。それぞれのフィルタは順番通りにデータを処理します。それぞれのフィルタのアウトプットは次のフィルタのインプットになります。

AngularJSでは $httpProvider で母樹フィルタのアイディアを利用しています。 $httpProviderinterceptors と呼ばれている配列プロパティを持っています。それぞれのオブジェクトは リクエスト , レスポンス , requestError , responseError と呼ばれるプロパティを必要に応じて持ちます。

requestError は一つ前のインターセプタがエラーを投げた時や、処理の拒否を行って終了した時に呼び出されるインターセプタです。 responseError は、一つ前の response インターセプタがエラーを投げた時に呼び出されます。

これは、インターセプタをオブジェクト・リテラルで利用する例です:

$httpProvider.interceptors.push(function($q, dependency1, dependency2) {
  return {
   'request': function(config) {
       // same as above
    },
    'response': function(response) {
       // same as above
    }
  };
});

ディレクティブ

コンポジット

コンポジット・パターンは分離をするためのデザイン・パターンです。コンポジット・パターンはオブジェクトのまとまりは1つのオブジェクトのインスタンスとして同じように扱われるべきとしています。コンポジットの意図は、複数のオブジェクトを部分と全体の階層構造を表す3つの構造に "構成する" ということです。

Composite

Gang of Fourによると、MVCは次の組み合わせであるに過ぎないということです:

  • ストラテジ
  • コンポジット
  • オブザーバ

これらはビューはコンポーネントのコンポジションであるということを表しています。AngularJSでは状況は似ています。ビューはディレクティブとディレクティブが適用されていることもあるDOM要素のコンポジションです。

次の例を見てみましょう:

<!doctype html>
<html>
  <head>
  </head>
  <body>
    <zippy title="Zippy">
      Zippy!
    </zippy>
  </body>
</html>
myModule.directive('zippy', function () {
  return {
    restrict: 'E',
    template: '<div><div class="header"></div><div class="content" ng-transclude></div></div>',
    link: function (scope, el) {
      el.find('.header').click(function () {
        el.find('.content').toggle();
      });
    }
  }
});

この例はUIコンポーネントとしてのシンプルなディレクティブを定義しています。定義されたコンポーネント("zippy")はヘッダとコンテントを持っています。ヘッダをクリックするとコンテントが見え隠れします。

最初の例から、全てのDOM要素の木構造は要素のコンポジションであると気づきます。ルート・コンポーネントは html 要素です。そしてそこに、 headbody などが続きます。

2番目のJavaScriptの例から、ディレクティブの template プロパティは ng-transclude ディレクティブが付加されたマークアップを見つけることができます。 zippy ディレクティブの中で別のディレクティブである ng-transclude を持つことを意味しています。つまり、ディレクティブのコンポジションです。理論上はコンポーネントは末節のノードまで無限にネストすることができます。

インタープリタ

コンピュータプログラミングではインタープリタ・パターンはある言語の文をどのように評価するかを決めるデザイン・パターンです。言語に特化したそれぞれのシンボル(オペレータであるかそうでないかは関係なく)に対する分類を持つというのが基本的な考え方です。文のシンタックス・ツリーはコンポジットパターンのインスタンスです。そして、それは分を評価(解釈)する際に使われます。

Interpreter

$parse サービスの背後では、AngularJSは独自のDSL(Domain Specific Language)記法のインタープリタを実装しています。DSLはシンプルに変更されたJavaScriptです。 JavaScript記法とAngularJS機能の主な違いとして、AngularJS記法は:

  • UNIX的なパイプ・シンタックスを含んでいること
  • エラーを投げないこと
  • コントロール・フロー文をもたないこと(オペレータは使えるが、例外、ループ、if文は持たない)
  • 所与のコンテクスト内で評価されること(現在の $scope のコンテクスト)

$parse サービスのの内部では2つの主なコンポーネントが定義されています:

//与えられた文字列をトークンに変換する責務を追う
var Lexer;
//トークンをパースして式を評価する責務を追う
var Parser;

式がトークン化されると、パフォーマンスのために内部にキャッシュされます。

AngularJS DSLではオペレータは下記のように定義されています:

var OPERATORS = {
  /* jshint bitwise : false */
  'null':function(){return null;},
  'true':function(){return true;},
  'false':function(){return false;},
  undefined:noop,
  '+':function(self, locals, a,b){
        //...
      },
  '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);},
  '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);},
  '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);},
  '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);},
  '=':noop,
  '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);},
  '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);},
  '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);},
  '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);},
  '<':function(self, locals, a,b){return a(self, locals)<b(self, locals);},
  '>':function(self, locals, a,b){return a(self, locals)>b(self, locals);},
  '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);},
  '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);},
  '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);},
  '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);},
  '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);},
  '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));},
  '!':function(self, locals, a){return !a(self, locals);}
};

それぞれのオペレータに関連付けられた関数を AbstractExpression のインターフェイス実装と考えることができます。

それぞれの Client は与えられたAngularJSの式を固有のコンテキスト - 固有のスコープで解釈します。

AngularJSのサンプルの式です:

// toUpperCase フィルタは式の結果に対して適用されます
// (foo) ? bar : baz
(foo) ? bar : baz | toUpperCase

テンプレート・ビュー

ページの中にマーカーを埋め込むことにより情報をHTMLにレンダーします。

Template View

動的なページのレンダリングはそんなに簡単なことではありません。たくさんの文字列の連結や操作やいらいらと結びついています。動的なページを構築するとても簡単な方法はマークアップとちょっとした式をページに書き込んでしまうことです。それはコンテキスト内で評価されテンプレートは最終的な形にコンパイルされます。今回そのフォーマットはHTML(DOM)になります。これはまさにテンプレート・エンジンそのものです - 与えられたDSLを適切なコンテキスト内で評価し、最終的な形に変換します。

テンプレートはバックエンド環境ではよく使われています。例えば、Smartyを使ってPHPコードをHTMLに埋め込んで動的なページを作ることができます。RubyではeRubyを使って静的なページにコードを埋め込むことができます。

JavaScriptにはmustache.jsやhandlebarsなどたくさんのテンプレートエンジンがあります。これらのエンジンの殆どは文字列としてテンプレートを操作します。テンプレートは別の場所に静的ファイルとして置いてAJAXで取得します。また、 script としてビューやJavaScriptの中に埋め込まれます。

例えばこのように:

<script type="template/mustache">
  <h2>Names</h2>
  {{#names}}
    <strong>{{name}}</strong>
  {{/names}}
</script>

テンプレートエンジンはコンテキストの中でコンパイルすることにより文字列をDOM要素に変換します。このように全てのマークアップに埋め込まれている全ての式は評価されそれらの値に変換されます。

例えば、上記のテンプレートを次のオブジェクト・コンテキスト { names: ['foo', 'bar', 'baz'] } の状態で評価するとこのような結果を得ることができます:

<h2>Names</h2>
  <strong>foo</strong>
  <strong>bar</strong>
  <strong>baz</strong>

AngularJSのテンプレートは本物のHTMLです。同等的なテンプレートエンジンがするような中間フォーマットではありません。

AngularJSコンパイラはDOMツリーを行き来し、既に知っているディレクティブ(要素、アトリビュート、クラス、コメント)を探すことです。AngularJSがこれらのディレクティブを見つけると、それと関連付けられたロジックを実行します。現在のスコープ・コンテキストの中で別の式を評価することもあります。

例えば:

<ul ng-repeat="name in names">
  <li>{{name}}</li>
</ul>

スコープのコンテキストの中は:

$scope.names = ['foo', 'bar', 'baz'];

これは、上記のものと同じ結果を出力します。ここでの主な違いはテンプレートが script にラップされていず、HTMLのままであるということです。

スコープ

オブザーバ

オブザーバはサブジェクトと呼ばれるオブジェクトが依存しているオブザーバのリストを管理し、変更があったらオブザーバのメソッドを呼び出すことで通知するデザイン・パターンです。主に分散したイベント・ハンドリング・システムで利用されます。

Observer

AngularJSアプリケーションのスコープ間では主に2つの基本的なやりとりの方式があります。一つ目は子スコープが親スコープのメソッドを呼び出すことです。子スコープは親スコープをプロトタイプ継承しているのでこれが可能になります(参考: スコープ )。これは子から親への1方向のコミュニケーションです。時に、親スコープから子スコープのメソッドを呼び出したり、子スコープにイベントの通知を送りたい時があります。AngularJSはこれを実現するための組み込みのオブザーバ・パターンを用意しています。オブザーバ・パターンが必要とされる別のケースとして、複数のスコープがあるイベントに関心があるものの、そのイベントは別スコープにあるため気づけないというものがあります。別々のスコープ間の分離が行われているため、別のスコーぷの変更に気づくことができません。

それぞれのAngularJSのスコープは $on$emit$broadcast と呼ばれるパブリック・メソッドを持っています。 $on メソッドは最初の引数として関心のある項目をとり、2つ目の引数としてコールバックをとります。このコールバックを Observer インターフェイスが実装されたオブザーバと考えることができます(JavaScriptでは関数は第一級オブジェクトなので、ただ、 notify メソッドを実装すればよいだけです):

function ExampleCtrl($scope) {
  $scope.$on('event-name', function handler() {
    //内容
  });
}

この方法で、現在のスコープは event-name のイベントを登録することができます。 event-name が親スコープや子スコープで実行された場合、 handler が呼ばれます。

$emit メソッドと $broadcast メソッドはそれぞれスコープチェーンの上方向と下方向にイベントを伝播します。 例えば:

function ExampleCtrl($scope) {
  $scope.$emit('event-name', { foo: 'bar' });
}

上記のスコープは、 event-name を上方向に伝播します。 event-name を登録している全ての親スコープは通知を受け、登録されているコールバックが実行されます。

$broadcast が呼ばれたときも同様です。違いは、イベントの伝播が下方向(すべての子スコープ)に行くということです。 それぞれのスコープが複数のコールバックを登録することができます(複数のオブザーバと関連することができます)。

JavaScriptコミュニティではこのパターンはパブリッシュ/サブスクライブとして知られています。

チェーン・オブ・レスポンシビリティ

チェーン・オブ・レスポンシビリティ・パターンはコマンド・オブジェクトと続く一連の処理オブジェクトからなるデザイン・パターンです。それぞれの処理オブジェクトは処理が可能なコマンド・オブジェクトを規定するロジックを持っています。残りの部分は次の処理オブジェクトに連鎖的に渡されます。新しい処理オブジェクトを連らの末尾に追加するメカニズムも存在しています。

Chain of Responsibilities

上述のようにAngularJSアプリケーションのスコープはスコープ・チェーンという階層構造を持っています。いくつかのスコープは "分離" しています。 "分離" とは親スコープからプロトタイプ継承していないということを意味しています。しかし、親スコープへは $parent プロパティでアクセスできます。

$emit$broadcast が呼ばれた時、スコープ・チェーンをイベント・バスとして、またはより正確に責任の連鎖考えることができます。イベントが起こると、それは(呼ばれたメソッドに応じて)下方向に、または、上方向に伝播します。続くスコープは:

  • イベントを処理し、次のスコープに渡す
  • イベントを処理し、そこで伝播を止める
  • イベントを処理せず、次のスコープに渡す
  • イベントを処理せず、そこで伝播を止める

下の例では ChildCtrl がイベントを発し、スコープ・チェーンの上方向に伝播させるところを確認できます。 親のスコープ( ParentCtrlMainCtrl )はコンソールにログを出します: "foo received" 。スコープがイベントの終着地点である場合は、イベント・オブジェクトの stopPropagation メソッドを呼び出し、コールバックに渡します。

myModule.controller('MainCtrl', function ($scope) {
  $scope.$on('foo', function () {
    console.log('foo received');
  });
});

myModule.controller('ParentCtrl', function ($scope) {
  $scope.$on('foo', function (e) {
    console.log('foo received');
  });
});

myModule.controller('ChildCtrl', function ($scope) {
  $scope.$emit('foo');
});

上記UMLダイアグラムの別々のハンドラーはそれぞれコントローラに注入された別々のスコープです。

コマンド

オブジェクト指向プログラミングでは、コマンド・パターンは後々メソッドの呼び出しをする際に必要となる情報をカプセル化した振る舞いのデザイン・パターンです。この情報はメソッド名、メソッドやメソッドパラメータとして利用される値を持つオブジェクトを含みます。

Command

コマンド・パターンのアプリケーションに進む前に、AngularJSではどのようにデータ・バインディングをしているか説明しましょう。

モデルとビューをバインドしたいとき、 ng-bind (1方向データ・バインディング)や ng-model (双方向データ・バインディング)を使います。例えば、 foo モデルの変更をビューに反映させたいとき、このように書くことができます:

<span ng-bind="foo"></span>

foo が変更される度に、spanのテキストは変わります。もう少し複雑なAngularJSの式を書くこともできます:

<span ng-bind="foo + ' ' + bar | uppercase"></span>

このケースでは、spanは大文字化した foobar の組み合わせとなります。裏では何が起こっているのでしょうか?

$scope$watch と呼ばれるメソッドを持っています。AngularJSコンパイラが ng-bind を見つけると、 foo + ' ' + bar | uppercase 式のwatcherを生成します。具体的には、 $scope.$watch("foo + ' ' + bar | uppercase", function () { /* body */ }); です。式の値が変わる度に、コールバックが呼ばれます。今回のケースではspanを更新します。

これは、 $watch の実装の最初の行です:

$watch: function(watchExp, listener, objectEquality) {
  var scope = this,
      get = compileToFn(watchExp, 'watch'),
      array = scope.$$watchers,
      watcher = {
        fn: listener,
        last: initWatchVal,
        get: get,
        exp: watchExp,
        eq: !!objectEquality
      };
//...

watcher オブジェクトをコマンドと考えることができます。コマンドの式は、 "$digest" ループの度に評価されます.AngularJSが式の変更を検知すると、 listner 関数を呼びます。 watcher コマンドは式の変更に必要な情報をカプセル化しています。そして、コマンドの実行を listner (実際のレシーバ)に委譲します。 $scope をコマンドの Client$digest ループをコマンドの Invoker と考えることができます。

コントローラ

ページ・コントローラ

ウェブサイトの特定のページやアクションのリクエストを扱うオブジェクト。 Martin Fowler

Page Controller

4 によると、ページ・コントローラは:

ページ・コントローラ・パターンはページのリクエストを受け、モデルに対して要求されたアクションを実行します。そして、リクエストされたページのための正しいビューを決定します。ビューと関連するコードはロジックと分離します。

ページ毎にたくさんの似たような振る舞いがある(フッタやヘッダのレンダリング、ユーザ・セッションの扱い)ので、ページコントローラは階層構造を持っています。AngularJSでは責任の制限されたコントローラを持っています。このコントローラは $route$state サービスがあるため、ユーザのリクエストを受け付けません。また、ページのレンダリングは ng-viewui-view ディレクティブの責任です。

ページ・コントローラと同じようにAngularJSのコントローラはユーザのインタラクションを扱いますし、モデルを提供して更新します。モデルはスコープに付けられた場合、ビューに露出します。ユーザのアクションによってビューから呼び出されるメソッドは既にスコープに付けられたものです。ページ・コントローラとAngularJSのコントローラのもう一つの類似点は、階層構造です。これはスコーぷの階層構造に対応しています。このやりかたで、共通のアクションはベース・コントローラに分離することができます。

AngularJSのコントローラはASP.NET WebFormsのコードにとても良く似ています。これらの責任はほぼ重なります。 いくつかのコントローラの階層構造の例です:

<!doctype html>
<html>
  <head>
  </head>
  <body ng-controller="MainCtrl">
    <div ng-controller="ChildCtrl">
      <span>{{user.name}}</span>
      <button ng-click="click()">Click</button>
    </div>
  </body>
</html>
function MainCtrl($scope, $location, User) {
  if (!User.isAuthenticated()) {
    $location.path('/unauthenticated');
  }
}

function ChildCtrl($scope, User) {
  $scope.click = function () {
    alert('You clicked me!');
  };
  $scope.user = User.get(0);
}

この例ではベース・コントローラを使ってロジックの再利用をするよくある例を示しています。それはそうとして、プロダクション環境で認証ロジックをコントローラで行うことはおすすめしません。別のルートにアクセスするロジックは抽象化した高レベルのところで決められるべきです。

ChildCtrl"Click" ラベルのあるボタンをクリックするアクションを扱い、また、モデルをスコープに取り付けビューに露出させる責任があります。

その他

モジュール・パターン

これは実際にはGang of FourやP of EAAのデザイン・パターンではありません。これはカプセル化とプライバシーを目的とした伝統的なJavaScriptのパターンです。

モジュール・パターンを利用することで、JavaScriptの関数スコープにおけるプライバシーを達成することができます。それぞれのモジュールは関数のローカルスコープの中に隠されたゼロかプリミティブな番号を持っています。この関数は与えられたモジュールのパブリックAPIを出力するオブジェクトを返します。

var Page = (function () {

  var title;

  function setTitle(t) {
    document.title = t;
    title = t;
  }

  function getTitle() {
    return title;
  }

  return {
    setTitle: setTitle,
    getTitle: getTitle
  };
}());

上記の例は2つのメソッド( setTitlegetTitle )を持ったオブジェクトを返すIIFE(Immediately-Invoked Function Expression)を持っています。返却されたオブジェクトは、 Page 変数に関連付けられています。

このケースでは Page オブジェクトのユーザは title に直接アクセスするすべを持っていません。 title はIIFEのローカルスコープの中に定義されているからです。

モジュール・パターンはAngularJSでサービスを定義する際にとても有益です。このパターンを使うことで、プライバシーをシミュレート(事実上達成)することができます:

app.factory('foo', function () {

  function privateMember() {
    //内容...
  }

  function publicMember() {
    //内容...
    privateMember();
    //内容
  }

  return {
    publicMember: publicMember
  };
});

foo を別のコンポーネントに注入すると、パブリック・メソッドだけにアクセスしてプライベートメソッドを呼ぶ必要がありません。この方法は再利用可能なライブラリを作成する際にとても強力なたすけになります。

Once we want to inject foo inside any other component we won't be able to use the private methods, but only the public ones. This solution is extremely powerful especially when one is building a reusable library.

データ・マッパ

データ・マッパは永続データ・ストア(リレーショナル・データベースがよく使われる)とイン・メモリ・データ・リプリゼンテーション(ドメイン・レイヤ)との双方向のやりとりをするためのデータ・アクセス・レイヤです。このパターンの目的はイン・メモリ・リプリゼンテーションと永続データ・ストアとマッパそれ自体をそれぞれ独立させることです。

Data Mapper

上記の図が示すように、データ・マッパは永続データ・ストアとイン・メモリ・データ・リプレゼンテーションの双方向通信をするために利用されています。普通、AngluarJSアプリケーションでは、サーバ・サイドの言語(Ruby, PHP, Java, JavaScriptなど)で書かれたAPIサーバとやりとりします。

普通、RESTful APIを持っている場合、 $resource がアクティブ・レコードのような形でその通信をサポートします。しかし、アプリケーションによってはサーバから返されるデータがフロントエンドで利用するには適切で無いフォーマットで返されることもあります。

例えば、ユーザが次の要素を持つと想定してみてください:

  • 名前
  • 住所
  • 友達リスト

そして、APIが次のメソッドを持つとします:

  • GET /user/:id - ユーザの名前と住所を返します
  • GET /friends/:id - ユーザの友達リストを返します

解決策としては2つの別々のサービスを作ることです。恐らくもう少し有効な解決策は、 User という1つのサービスがあった場合に、ユーザをリクエストした際に、ユーザの友達リストも一緒に読み込むことです。

app.factory('User', function ($q) {

  function User(name, address, friends) {
    this.name = name;
    this.address = address;
    this.friends = friends;
  }

  User.get = function (params) {
    var user = $http.get('/user/' + params.id),
        friends = $http.get('/friends/' + params.id);
    $q.all([user, friends])
    .then(function (user, friends) {
      return new User(user.name, user.address, friends);
    });
  };
  return User;
});

この方法でSPAの要求に応じてAPIを適用させた仮のデータ・マッパを作ることができます。

User サービスはこのように利用することができます:

function MainCtrl($scope, User) {
  User.get({ id: 1 })
  .then(function (data) {
    $scope.user = data;
  });
}

そして、パーシャルがこちらです:

<div>
  <div>
    Name: {{user.name}}
  </div>
  <div>
    Address: {{user.address}}
  </div>
  <div>
    Friends with ids:
    <ul>
      <li ng-repeat="friend in user.friends">{{friend}}</li>
    </ul>
  </div>
</div>

References

  1. Wikipedia. The source of all brief descriptions of the design patterns is wikipedia.
  2. AngularJS' documentation
  3. AngularJS' git repository
  4. Page Controller
  5. Patterns of Enterprise Application Architecture (P of EAA)
  6. Using Dependancy Injection to Avoid Singletons
  7. Why would one use the Publish/Subscribe pattern (in JS/jQuery)?