Angular2製アプリの設計についてのメモを共有する。
MVC, FluxをAngular2のComponent, DI, ChangeDetectionを利用した実装に落とし込んでいく。さらに、Clean ArchitectureなどのLayered Architectureへの発展を検討する。自分の主にAngular2への理解不足により、間違った情報やおかしな解説が含まれているかも知れない。必要に応じて追記、修正していく。前編と後編に分けている。後編は、量によってはこの前編に追記する形をとるかもしれない。
MVC, Flux
FluxはMVCに基づき、データフローを単方向にしたパターンである(というか、ほぼObserverパターンによるMVCの設計に名前を与えたものと解釈している。なお、MVCという概念は本来、特定の設計を指すものではない)。Fluxの解説は世に溢れているおかげで単方向データフローの説明を省略できて便利なので、ここでもFluxの語彙を一部使用している。ただ、Storeは(自分の解釈では)Viewに自身の変更をPushするModelであるため、Angular2では変更のViewへの反映はChangeDetectionに任せること、およびClean ArchitecutreやDDDに発展させていく時のためにModelと呼ぶほうが便利であることから、ここではStoreでなく単にModel(層)と呼称する。
今回の例では、Angular2のComponentがViewとControllerを兼任し、ComponentがActionDispatcherを通じてActionをdispatchしてModelを操作し、ChangeDetectionによりModelの状態変化をComponentに反映させている。データフローはComponent -> ActionDispatcher -> Model -> Componentと、(原則として)単方向となる。
サンプルコード: https://github.com/ntaoo/angular2_dart_todo/tree/master/mvc
ごく単純なTodoアプリ。Dartで書いているが、アーキテクチャの話なので言語に依存しない。
以下、それぞれ説明を加えていく。
Component
Ng2のComponentにViewおよびControllerを兼務させる。Componentはその性質によりSmart ComponentとDumb Componentに分けることができる。それらは木構造となり、少数のSmart Componentとそれを親componentとする多数のDumb Componentにより構成される。ツリーのルートをRoot Componentと呼ぶ。
Smart component
Stateful Componentとも呼ばれるようだ。Model objectへの参照を持ち、Modelの状態をPropertyにbindし、ChangeDetectionによってViewに反映させる。
また、ユーザーによるClick操作などのUIイベントを解釈してをModelを操作する。
今回の例では、ComponentにModelを直接操作させず、後述するIntentにUIイベントを解釈して適切なActionを生成させ、ActionDispatcherにdispatchさせている。
TodoListComponentがRoot Componentであり、Smart Componentである。子ComponentにDumb ComponentであるTodoItemComponentを持つ。また、ここではRoot ComponentでDIの設定(providers)も行っている。
@Component(
selector: 'todo-list',
templateUrl: './todo_list.html',
styleUrls: const [
'./todo_list.css'
],
providers: const [
const Provider(TodoList, useClass: TodoList),
const Provider(TodoListStorage, useClass: TodoListStorage),
const Provider(OnAction,
useClass: OnAction, deps: const [TodoList, TodoListStorage]),
const Provider(ActionDispatcher,
useClass: ActionDispatcher, deps: const [OnAction]),
const Provider(Intent, useClass: Intent, deps: const [ActionDispatcher])
],
directives: const [
TodoItemComponent,
CORE_DIRECTIVES,
FORM_DIRECTIVES
])
class TodoListComponent {
TodoList todoList;
String newItem = 'test';
Intent _intent;
TodoListComponent(this.todoList, this._intent) {
_intent.launchApp();
}
void addTodo() {
_intent.addTodo(newItem);
this.newItem = '';
}
void removeTodo(UserRemovedTodo event) {
_intent.removeTodo(event.id);
}
void updateText(UserUpdatedText event) {
if (event.text.isEmpty) {
_intent.removeTodo(event.id);
} else {
_intent.updateText(event.id, event.text);
}
}
void updateCompletion(UserUpdatedCompletion event) {
_intent.updateCompletion(event.id, event.isCompleted);
}
}
Intent
ユーザーの操作により発生するイベントストリームを解釈して適切なActionを発行したり、Actionの発行間隔等を調整する等行う。MVIパターンから名前を借用した。Controllerの一部である。Controllerと言ってしまっても良いかもしれない。
Dart版ではコアライブラリのEventStream、TS版ではRxJSのObservableを操作する。
残念ながら今回の例は単純なためEventStreamを操作しているわけでもなく、Componentと処理が重複して単に冗長なだけになってしまっている。
@Injectable()
class Intent {
final ActionDispatcher _dispatcher;
Intent(this._dispatcher);
void addTodo(String text) {
_dispatcher.dispatch(new UserAddedTodo(text));
}
void launchApp() {
_dispatcher.dispatch(new UserLaunchedApp());
}
void removeTodo(String id) {
_dispatcher.dispatch(new UserRemovedTodo(id));
}
void updateCompletion(String id, bool isCompleted) {
_dispatcher.dispatch(new UserUpdatedCompletion(id, isCompleted));
}
void updateText(String id, String text) {
_dispatcher.dispatch(new UserUpdatedText(id, text));
}
}
複数のSmart Component間で重複する操作がある場合、Intentに括りだすことでDRYにできる。
FluxではAction Creatorに相当するのかもしれないが、Action CreatorはRemote API Callを行うことを許容する。Intentはユーザー操作の解釈をする場でありRemote API Callはしたくないため、混乱を避けるためActionCreatorという用語を使っていない。
Remote API Callは別途RemoteApi等の別の名前をつけてそこで行う。
Intent(Controller)、およびRemoteApiはClean architectureで言うInterface Adapter層である。
Dumb Component
Stateless ComponentまたはPure Componentとも呼ばれるようだ。
https://github.com/mgechev/angular2-style-guide#change-detection
状態を@Inputを通じて親Componentのみに依存することで、ChangeDetectionStrategy.OnPushを適用できる。これによりView更新のパフォーマンスが向上する。
ChangeDetection Strategyの詳細については以下の資料が理解の助けになる。
http://pascalprecht.github.io/slides/angular-2-change-detection-explained/#/
日本語訳。
http://qiita.com/laco0416/items/523d96ddbfe55c4e6949
またUIイベントからのも、直接Actionを生成してdispatchさせずに、一旦@Output()
からEventを親に投げ、親のSmart Componentを中継してActionを発行させることで、Modelへの副作用が発生する処理ををSmartComponentに集約させる。
@Component(
selector: 'todo-item',
templateUrl: './todo_item.html',
styleUrls: const ['./todo_item.css'],
changeDetection: ChangeDetectionStrategy.OnPush)
class TodoItemComponent {
bool editMode = false;
@Input()
TodoItem item;
@Output()
EventEmitter<UserRemovedTodo> userRemovedTodo = new EventEmitter<UserRemovedTodo>();
@Output()
EventEmitter<UserUpdatedText> userUpdatedText = new EventEmitter<UserUpdatedText>();
@Output()
EventEmitter<UserUpdatedCompletion> userUpdatedCompletion =
new EventEmitter<UserUpdatedCompletion>();
// 以下略
}
Smart Componentが肥大化してきたら一部をDumb Componentに括りだすことを考えていく。
一部パフォーマンスの問題や実装の単純化のため、componentにviewに関するstate(今回の例ではbool editMode = false;
など)を持たせても良いが、メンテナンス性を考慮するとこのような例外は最小限に保つようにするべきである。
Layered Architectureへの発展について
アプリが複雑化していく際には、Clean ArchitectureなどのLayered Architectureに発展させ、ComponentはViewのみ担当し、Controllerを分離したり、直接Modelへの参照を持たせずPresenterを間に挟むなどして責務を分離していく。しかし多くのアプリはそこまでの責務の分離が必要になるほど複雑にはならないはずで、Layered Architectureはコード量を増大させるので、小規模の開発では生産性が落ちる。そのため、やみくもにそれを適用するべきではない。
後編へ
後編は、同じサンプルコードを題材に、DI、Action, ActionDispatcher、Model、ChangeDetectionについてそれぞれ見ていく。量によってはこの前編に追記する形をとるかもしれない。