Help us understand the problem. What is going on with this article?

Angular2におけるSVMモデル

More than 3 years have passed since last update.

はじめに

Angular2の仕様がようやく固まってきました。2016/06/20現在、Router以外の機能はほぼほぼfixしたと言っていいでしょう。JavaScriptフレームワークといえばReact 1 が日本でも海外でも人気を博していますが、個人的にはAngular2のほうが気持よく実装することができています。2

Angular2を用いてすでにいくつかアプリケーションを開発してきましたが、その中で、個人的に試行錯誤の上採用した"デザインパターン"みたいなものをメモがてらに書いていきます。

StoreManagerService
ViewManagerService
ModelManagerService

という3つのマネージャークラスを定義することでアプリケーション全体を構築しているので、SVMモデルと勝手に名付けています。3

Angular2の概要

Angular2の特徴のひとつとして、 Dependency Injection (DI) があげられます。詳細は 公式サイトに載っていますが、簡単に言うと、すでに(Providerで)初期化されているインスタンスを、他のクラスのインスタンス変数にそのまま格納して用いることができる仕組みのことを言います。

例えばログインしているユーザの内容をアプリケーション内で共有したい場合、

user.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class UserService {
  public name: string;
}

という UserService を定義し、これを

app.component.ts
import { Component } from '@angular/core';
import { UserService } from './user.service';

@Component({
  providers: [UserService]
})
export class AppComponent {
}

とアプリケーションのRootとなるComponentである AppComponentproviders に含めておけば、他のどのComponentからでも同じインスタンスが共有できます。(この providers にいれることでインスタンスができる、という理解で実用上問題ありません。)

そして、ログイン画面用のComponentでログイン時に呼ばれるメソッドにユーザ名を格納する処理を書いておけば、

login.component.ts
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で参照することができます。

index.component.ts
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が増えると、

  1. import 文でコードの先頭が埋め尽くされてしまう。
  2. DI する変数名が大量になる。

という問題が発生します。 (1) に関しては、 index.ts に Service をまとめることによって解決できるかもしれませんが、それでも import { FooService, BarService, ... } from './services' と Service を列挙しなくてはならないことには変わりありません。これは非常に面倒です。

提案手法

ManagerServiceクラス

上記の問題を解決するためのアプローチとして考えられるのが、 ManagerService という、全ての Service をまとめるクラスを作ることです。このマネージャクラスを定義するアプローチ自体は、他の言語やフレームワークでも実践されているものになります。 Angular2 に適用する場合、下記のようなコードになります。

manager.service.ts
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
  ) {
  }
}
app.component
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.serviceapp.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 のように定義していきます。

こうすることで、

store-manager.service
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 を下記のように定義しておけば、

store-service.ts
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];
  }
}

例えば

user-store.service.ts
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 で定義した内容は、例外処理をしていないなど、簡便のためにいくつか必要であるべき機能を実装していないので注意してください。また、 EntityUserなど、アプリケーション固有の(オブジェクト)データとし、どのEntityも必ずidをもっているものとします。

V (ViewService)

View は、基本的にはComponentから 参照(Read)および書き換え(Write)が行われることを想定しています。上述した user.name = 'yusugomori' がこれに該当します。SVMモデルを取り入れる場合、 View.user.name = 'yusugomori' などとなります。モーダルやflashの処理も、SVMを取り入れることで混乱することなく簡単に実装できるようになるかと思います。

M (ModelService)

Model は、APIの通信やStoreの制御、またその他のビジネスロジック等の処理を行います。 UserModelService を例にすると、下記のようになります。

user-model.service.ts
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も開発を進めています。開発を手伝ってくださる方を探しています!この辺りからご連絡お待ちしております。)


  1. Reactは厳密にはフレームワークではなくライブラリ。 

  2. 個人的な感触としては、出自がウェブデザインからの場合はAngular2、エンジニアリングからの場合はReactのほうが導入における相性はよいのではないかと考えています。 

  3. なので、 機械学習におけるSVM (Support Vector Machine) とは全く関係はありません。個人的なバックグラウンドとして機械学習や深層学習をずっと研究していたので、この命名がしっくりきました。深層学習の実装についてはGitHubのリポジトリブログでまとめています。 

  4. ただし、Detailでのみ必要なServiceはここで管理すべきではないケースもあり、その場合はそのServiceを必要とするComponentでのみ providers: [] によりDIをすべきである。 

  5. このApiService のように、Componentからは参照されないServiceに関しては、Managerで管理する必要はない。 

  6. Style Guild には private変数に _ prefixをつけるのは避けるべき、と明記されていますが、個人的にはコードのどこを見てもひと目で publicprivateか分かるため、 _ をつけるようにしています。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away