はじめに
Angular2の仕様がようやく固まってきました。2016/06/20現在、Router以外の機能はほぼほぼfixしたと言っていいでしょう。JavaScriptフレームワークといえばReact 1 が日本でも海外でも人気を博していますが、個人的にはAngular2のほうが気持よく実装することができています。2
Angular2を用いてすでにいくつかアプリケーションを開発してきましたが、その中で、個人的に試行錯誤の上採用した"デザインパターン"みたいなものをメモがてらに書いていきます。
StoreManagerService
ViewManagerService
ModelManagerService
という3つのマネージャークラスを定義することでアプリケーション全体を構築しているので、SVMモデルと勝手に名付けています。3
Angular2の概要
Angular2の特徴のひとつとして、 Dependency Injection (DI) があげられます。詳細は 公式サイトに載っていますが、簡単に言うと、すでに(Providerで)初期化されているインスタンスを、他のクラスのインスタンス変数にそのまま格納して用いることができる仕組みのことを言います。
例えばログインしているユーザの内容をアプリケーション内で共有したい場合、
import { Injectable } from '@angular/core';
@Injectable()
export class UserService {
public name: string;
}
という UserService
を定義し、これを
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
providers: [UserService]
})
export class AppComponent {
}
とアプリケーションのRootとなるComponentである AppComponent
の providers
に含めておけば、他のどのComponentからでも同じインスタンスが共有できます。(この providers にいれることでインスタンスができる、という理解で実用上問題ありません。)
そして、ログイン画面用のComponentでログイン時に呼ばれるメソッドにユーザ名を格納する処理を書いておけば、
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
})
export class LoginComponent {
constructor(
public user: UserService
){
}
// called when a user clicks/touches login button
login(event: MouseEvent): void {
event.preventDefault();
let self = this;
self.user.name = 'yusugomori'; // should be fetched via API
}
}
(本来は、login
ではAPI通信をして、その結果をuser.name
に格納すべきですが簡単のためここでは省略しています。)
UserService
を DI することで、このユーザ名を他の画面 i.e. Componentで参照することができます。
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { Logger } from './logger';
@Component({
})
export class IndexComponent implements OnInit {
public name: string;
constructor(
public user: UserService
){
}
ngOnInit() {
let self = this;
self.name = self.user.name;
Logger.log(self.name); // => 'yusugomori'
}
}
上記では name
という別変数に値を代入していますが、もちろん、user.name
をそのまま html に用いても問題ありません(むしろ、そのほうが他のメソッドで user.name
の値が変更されたときにViewでも変更が行われるので望ましいでしょう。 name
はプリミティブなので、これをhtmlに用いると、また別に name
を更新する処理を書かなくてはならなくなってしまいます)。
実装における問題
さて、DI はとても便利な機能であることには間違いないのですが、アプリケーションが大規模になるに従って、少々煩わしい部分が出てきます。特に、下記の2つがあげられるでしょう。
import { FooService } from './services/foo.service';
import { BarService } from './services/bar.service';
...
export class SomeComponent {
constructor(
public foo: FooService,
public bar: BarService,
public ...
) {
}
}
と、Componentで必要なServiceが増えると、
-
import
文でコードの先頭が埋め尽くされてしまう。 - DI する変数名が大量になる。
という問題が発生します。 (1) に関しては、 index.ts
に Service をまとめることによって解決できるかもしれませんが、それでも import { FooService, BarService, ... } from './services'
と Service を列挙しなくてはならないことには変わりありません。これは非常に面倒です。
提案手法
ManagerServiceクラス
上記の問題を解決するためのアプローチとして考えられるのが、 ManagerService
という、全ての Service をまとめるクラスを作ることです。このマネージャクラスを定義するアプローチ自体は、他の言語やフレームワークでも実践されているものになります。 Angular2 に適用する場合、下記のようなコードになります。
import { Injectable } from '@angular/core';
import { FooService } from './foo.service';
import { BarService } from './bar.service';
...
@Injectable()
export class ManagerService {
constructor(
public foo: FooService,
public bar: BarService
) {
}
}
import { FooService } from './services/foo.service';
import { BarService } from './services/bar.service';
...
import { ManagerService } from './services/manager.service';
@Component({
providers: [FooService, BarService, ..., ManagerService]
})
export class AppComponent{
}
このように、 manager.service
と app.component
のみ列挙が必要になりますが、こうすることで、他のComponentではだいぶすっきり書くことができるようになります。
import { ManagerService } from './services/manager.service';
@Component({})
export class SomeComponent {
constructor(
public manager: ManagerService
) {
}
aFunction(): void {
let self = this;
self.manager.foo.whatever();
self.manager.bar.whatever();
}
}
これにより、たとえば全Componentで必要となるようなServiceが追加されたとしても、 Component に新たに import
を書くことなく、追加された Service にアクセスできるようになります。
SVMモデル
さて、小中規模のアプリケーションならば ManagerService
のみで十分でしょう。一方で、アプリケーションが大規模になるに従って、各Serviceが肥大化するという問題が発生します(MVCでいうところのいわゆる Fat Model)。そこで、Serviceの果たすべき機能を分割し、それぞれに ManagerService をつくることで、アプリケーションの保守性を高めることを考えます。具体的には、下記の3つに分割することを考えます。
-
StoreManagerService
: オンメモリ上で Object(Entity)を管理するマネージャクラス。データを管理するためだけに存在すべきであって、通信などは行ってはならない。 -
ViewManagerService
: Component間にまたがるView要素を管理するマネージャクラス。例えばモーダルといった GlobalView であったり、は全てここからアクセスする。 -
ModelManagerService
: S, V 以外のモデルに関する処理を管理するマネージャクラス。APIの通信や、Storeの制御などを行う。
このように3つのマネージャクラスを定義するに従い、各Serviceも3つに分割します。
例えば、UserStoreService
, UserViewService
, UserModelService
のように定義していきます。
こうすることで、
import { Injectable } from '@angular/core';
import { UserStoreService } from './user-store.service';
...
@Injectable()
export class StoreManagerService {
constructor(
public user: UserStoreService,
...
) {
}
}
などとすれば、
import { StoreManagerService } from './services/store-manager.service';
import { ModelManagerService } from './services/model-manager.service';
import { ViewManagerService } from './services/view-manager.service';
export class SomeComponent {
constructor(
public Store: StoreManagerService,
public View: ViewManagerService,
public Model: ModelManagerService
) {
}
}
というDIにより、あらゆる Service にアクセスできるようになります。 4
では、下記より、各論に入っていきます。
S (StoreService)
Store は、他の View, Model を参照することは決してなく、また Component から Store内のデータは参照するのみ(Read)であるべきで、書き換え(Write)をしてはいけません。 データの書き換えはすべて Model のメソッド経由で行うべきとします(このModelのメソッドはComponentからも呼び出される)。
ベースとなる StoreService
を下記のように定義しておけば、
import { Entity } from '../entity';
import { Logger } from '../logger';
export abstract class StoreService<T extends Entity> {
protected _map: {[id: string]: T} = {};
set map(object: any) {
Logger.error('Direct assertion to map is not allowed.');
}
getObject(id: string): T {
let self = this;
return self._map[id];
}
addObject(object: T): void {
let self = this;
self._map[object.id] = object;
}
deleteObject(object: T): void {
let self = this;
self._map[object.id] = null; // or delete self._map[object.id];
}
}
例えば
import { Injectable } from '@angular/core';
import { StoreService } from './store-service';
import { User } from './user';
@Injectable()
export class UserStoreService extends StoreService<User> {
}
などとすることで、 User
データはすべて Store.user.getObject(id)
などからアクセスできることになります。また、後述しますが、 addObject
, deleteObject
はすべて M で行うべきメソッドです。
ただし、上記の StoreService
で定義した内容は、例外処理をしていないなど、簡便のためにいくつか必要であるべき機能を実装していないので注意してください。また、 Entity
はUser
など、アプリケーション固有の(オブジェクト)データとし、どのEntity
も必ずid
をもっているものとします。
V (ViewService)
View は、基本的にはComponentから 参照(Read)および書き換え(Write)が行われることを想定しています。上述した user.name = 'yusugomori'
がこれに該当します。SVMモデルを取り入れる場合、 View.user.name = 'yusugomori'
などとなります。モーダルやflashの処理も、SVMを取り入れることで混乱することなく簡単に実装できるようになるかと思います。
M (ModelService)
Model は、APIの通信やStoreの制御、またその他のビジネスロジック等の処理を行います。 UserModelService
を例にすると、下記のようになります。
import { Injectable } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { ModelService } from './model-service';
import { ApiService } from './api-service';
import { StoreManagerService } from './store-manager.service';
import { User } from './user';
@Injectable()
export class UserModelService extends ModelService<User> {
constructor(
private _api: ApiService,
public Store: StoreManagerService
) {
super();
}
fetch(id: string, success: Function, error: Function): Subscription | void {
let self = this;
if (self.Store.user.getObject(id)) {
success(self.Store.user.getObject(id)); // return stored data
return;
} else {
let callback = (json: JSON) => {
self.Store.user.addObject(new User(json)); // add new data to Store
success(self.Store.user.getObject(id));
}
return self._api.request(`/users/${id}`, 'GET', callback, error);
}
}
delete(id: string, success: Function, error: Function): Subscription | void {
let self = this;
let callback = () => {
self.Store.user.deleteObject(id);
success();
}
return self._api.request(`/users/${id}`, 'DELETE', callback, error);
}
}
ここで、ApiService
はサーバーサイドとのAPI通信をとり行なってくれるラッパーServiceとします(この中身は割愛します)。56
このModel内でStoreを制御することにより、どのComponentからも同じオブジェクトを参照することが保証されるので、Modelのどこかで(Store内の)関連データの値を上書きした時にも、あらゆるView(Component)にその変更が反映されることが保証されます。
ここでは、オブジェクト全体の追加・削除処理しか考慮していない簡単な例しか記述していませんが、例えばここに update
といった処理も追加していくことで、Storeデータの一部を非同期に上書きしても、Viewにその変更を通知することができるようになります。
まとめ
Angular2を用いた大規模アプリケーションで発生し得る実装上の煩わしさをなくし、DIの利点を活かすため、SVMモデルというものを考えました。基本的には、有名なデザインパターンに加え、オンメモリのデータをどう扱っていくかという、JSならではの問題を考慮しているに過ぎません。他にもっとよい実装方法があるかもしれませんが、少なくとも現状では、本手法が個人的には最も実装しやすいパターンとなっています。
Angular2はTypeScriptベースなため、大規模になっても保守がしやすいフレームワークです。ようやく仕様も固まってきたので、ぜひこれから新しいアプリケーションを構築する際に採用してみるといいかもしれません。
その他
(Webだけでなく、iOSやAndroidも開発を進めています。開発を手伝ってくださる方を探しています!この辺りからご連絡お待ちしております。)
-
Reactは厳密にはフレームワークではなくライブラリ。 ↩
-
個人的な感触としては、出自がウェブデザインからの場合はAngular2、エンジニアリングからの場合はReactのほうが導入における相性はよいのではないかと考えています。 ↩
-
なので、 機械学習におけるSVM (Support Vector Machine) とは全く関係はありません。個人的なバックグラウンドとして機械学習や深層学習をずっと研究していたので、この命名がしっくりきました。深層学習の実装についてはGitHubのリポジトリやブログでまとめています。 ↩
-
ただし、Detailでのみ必要なServiceはここで管理すべきではないケースもあり、その場合はそのServiceを必要とするComponentでのみ
providers: []
によりDIをすべきである。 ↩ -
この
ApiService
のように、Componentからは参照されないServiceに関しては、Managerで管理する必要はない。 ↩ -
Style Guild には private変数に
_
prefixをつけるのは避けるべき、と明記されていますが、個人的にはコードのどこを見てもひと目でpublic
かprivate
か分かるため、_
をつけるようにしています。 ↩