Scalable Angular 2 Application Architecture

Scalable Angular 2 Application Architecture

Modern web applications have constantly growing requirements and their complexity grows exponentially. Some of the biggest challenges in front of us are state management, testability, flexibility and on top of that we need to develop the application at reasonable level of simplicity in order to ease the recruitment process.

In this presentation I’ll introduce a scalable Angular 2 application architecture, which answers the following requirements:
- Testability.
- Predictable state management.
- Scalable communication layer.
- Modular and robust design.

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

April 16, 2016
Tweet

Transcript

  1. None
  2. Scalable Application rchitecture github.com/mgechev twitter.com/mgechev blog.mgechev.com

  3. None
  4. None
  5. STANG2 50% off

  6. Agenda

  7. – Martin Fowler “…decisions that are hard to change…” Architecture

  8. Story Time

  9. Dynamic Requirements

  10. None
  11. Scalable communication layer

  12. Communication layer • RESTful API • WebSocket application service •

    WebRTC data-channel
  13. Various package formats

  14. Package formats • RESTful API • JSON commands • WebSocket

    application service • JSON-RPC • WebRTC data-channel • BERT-RPC
  15. Multiple state mutation sources

  16. Scalable team

  17. None
  18. Lazy-loading

  19. Dynamic Requirements Scalable Communication Layer Various package formats Multiple state

    mutation sources Scalable team Lazy-loading
  20. Dynamic Requirements Scalable Communication Layer Various package formats Multiple state

    mutation sources Scalable team Lazy-loading
  21. abstraction |əbˈstrakʃ(ə)n| noun [ mass noun ] … 4 the

    process of considering something independently of its associations or attributes: the question cannot be considered in abstraction from the historical context in which it was raised.
  22. WebRTC Gateway WebSocket Gateway

  23. Gateway WebRTC Gateway WebSocket Gateway

  24. Dynamic Requirements Scalable Communication Layer Various package formats Multiple state

    mutation sources Scalable team Lazy-loading
  25. redux

  26. Dynamic Requirements Scalable Communication Layer Various package formats Multiple state

    mutation sources Scalable team Lazy-loading
  27. Modular

  28. . ʦʒʒ src ʮʒʒ multi-player ʔ ʮʒʒ commands ʔ ʮʒʒ

    components ʔ ʦʒʒ gateways ʮʒʒ single-player ʔ ʦʒʒ components ʮʒʒ home ʔ ʦʒʒ components ʦʒʒ shared
  29. . ʦʒʒ src ʮʒʒ multi-player ʔ ʮʒʒ commands ʔ ʮʒʒ

    components ʔ ʦʒʒ gateways ʮʒʒ single-player ʔ ʦʒʒ components ʮʒʒ home ʔ ʦʒʒ components ʦʒʒ shared /home
  30. . ʦʒʒ src ʮʒʒ multi-player ʔ ʮʒʒ commands ʔ ʮʒʒ

    components ʔ ʦʒʒ gateways ʮʒʒ single-player ʔ ʦʒʒ components ʮʒʒ home ʔ ʦʒʒ components ʦʒʒ shared /single-player
  31. . ʦʒʒ src ʮʒʒ multi-player ʔ ʮʒʒ commands ʔ ʮʒʒ

    components ʔ ʦʒʒ gateways ʮʒʒ single-player ʔ ʦʒʒ components ʮʒʒ home ʔ ʦʒʒ components ʦʒʒ shared /multi-player
  32. Sample Tech Stack • Angular 2 • RxJS • ngrx

    • TypeScript • ImmutableJS
  33. RxJS in 2 slides

  34. [1, 2, 3] .map(n => n * 2) .filter(n =>

    n > 2); higher-order-functions.ts
  35. let obs = Rx.Observable.create(observer => { let counter = 0;

    setInterval(() => observer.next(counter++), 1000); }); obs .map(n => n * 2) .filter(n => n > 2) .subscribe(n => console.log(n)); rx.ts
  36. let obs = Rx.Observable.create(observer => { let counter = 0;

    setInterval(() => observer.next(counter++), 1000); }); obs .map(n => n * 2) .filter(n => n > 2) .subscribe(n => console.log(n)); rx.ts
  37. Sample application

  38. None
  39. None
  40. High-level Architecture

  41. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  42. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  43. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  44. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  45. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  46. UI components

  47. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  48. export class GameComponent implements AfterViewInit { @Input() text: string; @Output()

    change: EventEmitter<string> … constructor(private _model: GameModel …) {} changeHandler(data: string) { this._model.onProgress(data); } get invalid() { return this._model.game$ .scan((accum: boolean, current: any) => { return (current && current.get(‘invalid’) || accum; }, false); } } game.component.ts
  49. export class GameComponent implements AfterViewInit { @Input() text: string; @Output()

    change: EventEmitter<string> … constructor(private _model: GameModel …) {} changeHandler(data: string) { this._model.onProgress(data); } get invalid() { return this._model.game$ .scan((accum: boolean, current: any) => { return (current && current.get(‘invalid’) || accum; }, false); } } game.component.ts
  50. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  51. game.model.ts @Injectable() export class GameModel extends Model { game$: Observable<Game>;

    constructor(protected _store: Store<any>, @Inject(AsyncService) _services) { super(_services || []); this.game$ = this._store.select('game'); } ... completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this._store.dispatch(action); this.performAsyncAction(action) .subscribe(() => console.log('Done!')); } }
  52. game.model.ts @Injectable() export class GameModel extends Model { game$: Observable<Game>;

    constructor(protected _store: Store<any>, @Inject(AsyncService) _services) { super(_services || []); this.game$ = this._store.select('game'); } ... completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this._store.dispatch(action); this.performAsyncAction(action) .subscribe(() => console.log('Done!')); } }
  53. @Injectable() export class GameModel extends Model { game$: Observable<Game>; constructor(protected

    _store: Store<any>, @Inject(AsyncService) _services) { super(_services || []); this.game$ = this._store.select('game'); } ... completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this._store.dispatch(action); this.performAsyncAction(action) .subscribe(() => console.log('Done!')); } } game.model.ts
  54. @Injectable() export class GameModel extends Model { game$: Observable<Game>; constructor(protected

    _store: Store<any>, @Inject(AsyncService) _services) { super(_services || []); this.game$ = this._store.select('game'); } ... completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this._store.dispatch(action); this.performAsyncAction(action) .subscribe(() => console.log('Done!')); } } game.model.ts
  55. @Injectable() export class GameModel extends Model { game$: Observable<Game>; constructor(protected

    _store: Store<any>, @Inject(AsyncService) _services) { super(_services || []); this.game$ = this._store.select('game'); } ... completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this._store.dispatch(action); this.performAsyncAction(action) .subscribe(() => console.log('Done!')); } } game.model.ts
  56. @Injectable() export class GameModel extends Model { game$: Observable<Game>; constructor(protected

    _store: Store<any>, @Inject(AsyncService) _services) { super(_services || []); this.game$ = this._store.select('game'); } ... completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this._store.dispatch(action); this.performAsyncAction(action) .subscribe(() => console.log('Done!')); } } game.model.ts
  57. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  58. Component Model Store Dispatcher startGame() dispatch(action) applyReducers(action, store) next(state)

  59. Component Model Store Dispatcher startGame() dispatch(action) applyReducers(action, store) next(state)

  60. game.reducer.ts export const gameReducer = (state: any = initialState.get(‘game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case INVALID_GAME: state = state.set('invalid', true); break; case GAME_PROGRESS: state = state.set(‘currentText', action.payload.text); break; } return state; };
  61. game.reducer.ts export const gameReducer = (state: any = initialState.get(‘game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case INVALID_GAME: state = state.set('invalid', true); break; case GAME_PROGRESS: state = state.set(‘currentText', action.payload.text); break; } return state; };
  62. game.reducer.ts export const gameReducer = (state: any = initialState.get(‘game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case INVALID_GAME: state = state.set('invalid', true); break; case GAME_PROGRESS: state = state.set(‘currentText', action.payload.text); break; } return state; };
  63. game.reducer.ts export const gameReducer = (state: any = initialState.get(‘game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case INVALID_GAME: state = state.set('invalid', true); break; case GAME_PROGRESS: state = state.set(‘currentText', action.payload.text); break; } return state; };
  64. Component Model Store Dispatcher startGame() dispatch(action) applyReducers(action, store) next(state)

  65. game.component.ts … get invalid() { return this._model.game$ .scan((accum: boolean, current:

    any) => { return current.get('invalid') || accum; }, false); } …
  66. game.component.ts … get invalid() { return this._model.game$ .scan((accum: boolean, current:

    any) => { return current.get('invalid') || accum; }, false); } …
  67. game.component.ts … get invalid() { return this._model.game$ .scan((accum: boolean, current:

    any) => { return current.get('invalid') || accum; }, false); } …
  68. game.component.html … <div [hide]="!(invalid | async)"> <h1>The game is invalid...</h1>

    </div> …
  69. game.component.html … <div [hide]="!(invalid | async)"> <h1>The game is invalid...</h1>

    </div> …
  70. Async Services

  71. UI components Façade (provides simplified interface to the components) State

    management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  72. Remote Service App

  73. Remote Service App

  74. export abstract class AsyncService { abstract process(data: Action): Observable<any>; }

    base.async-service.ts
  75. export class GameP2PService extends AsyncService { constructor(private _rtcGateway: WebRTCGateway, private

    _store: Store) { _rtcGateway.dataStream .map((data: any) => JSON.parse(data.toString())) .subscribe((command: any) => { switch (command.method) { case PROGRESS: _store.dispatch(P2PActions.progress(command.payload.text)); break; } }); } process(action: Action) { const commandBuilder = buildP2PCommand(action); if (!commandBuilder) { console.warn('This command is not supported'); return Observable.create((obs: Observer<any>) => obs.complete()); } else return commandBuilder(baseCommand).invoke(); } } game-p2p.async-service.ts
  76. export class GameP2PService extends AsyncService { constructor(private _rtcGateway: WebRTCGateway, private

    _store: Store) { _rtcGateway.dataStream .map((data: any) => JSON.parse(data.toString())) .subscribe((command: any) => { switch (command.method) { case PROGRESS: _store.dispatch(P2PActions.progress(command.payload.text)); break; } }); } process(action: Action) { const commandBuilder = buildP2PCommand(action); if (!commandBuilder) { console.warn('This command is not supported'); return Observable.create((obs: Observer<any>) => obs.complete()); } else return commandBuilder(baseCommand).invoke(); } } game-p2p.async-service.ts
  77. export class GameP2PService extends AsyncService { constructor(private _rtcGateway: WebRTCGateway, private

    _store: Store) { _rtcGateway.dataStream .map((data: any) => JSON.parse(data.toString())) .subscribe((command: any) => { switch (command.method) { case PROGRESS: _store.dispatch(P2PActions.progress(command.payload.text)); break; } }); } process(action: Action) { const commandBuilder = buildP2PCommand(action); if (!commandBuilder) { console.warn('This command is not supported'); return Observable.create((obs: Observer<any>) => obs.complete()); } else return commandBuilder(baseCommand).invoke(); } } game-p2p.async-service.ts
  78. But what if…

  79. Model S1 S2

  80. Model S1 S2 A

  81. Model S1 S2 A

  82. Model S1 S2 A

  83. Model S1 S2 A

  84. Model S1 S2 A

  85. Model S1 S2 A

  86. Immutability

  87. let user = new Map(); user = user.set('name', 'Joe'); //

    { name: 'Joe' } console.log(user.toJS()); immutable.js
  88. No static typing

  89. Immutability or Static Typing

  90. Why not Both?

  91. export interface IUser { id?: number; gender?: number; email?: string;

    } const userRecord = Immutable.Record({ id: 0, gender: 0, email: null }); export class User extends userRecord implements IUser { id: number; gender: number; email: string; constructor(config: IUser) { super(config); } } immutable-records.ts
  92. export interface IUser { id?: number; gender?: number; email?: string;

    } const userRecord = Immutable.Record({ id: 0, gender: 0, email: null }); export class User extends userRecord implements IUser { id: number; gender: number; email: string; constructor(config: IUser) { super(config); } } immutable-records.ts
  93. export interface IUser { id?: number; gender?: number; email?: string;

    } const userRecord = Immutable.Record({ id: 0, gender: 0, email: null }); export class User extends userRecord implements IUser { id: number; gender: number; email: string; constructor(config: IUser) { super(config); } } immutable-records.ts
  94. export interface IUser { id?: number; gender?: number; email?: string;

    } const userRecord = Immutable.Record({ id: 0, gender: 0, email: null }); export class User extends userRecord implements IUser { id: number; gender: number; email: string; constructor(config: IUser) { super(config); } } immutable-records.ts
  95. None
  96. Recap

  97. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator signup(data) signup(data) RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name
  98. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator signup(data) RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data)
  99. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data)
  100. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data)
  101. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data)
  102. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data)
  103. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data)
  104. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data)
  105. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data)
  106. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator signup(data) RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data)
  107. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Not coupled to any message format • Model can use different services based on context • Easy management of async events
  108. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Not coupled to any message format • Model can use different services based on context • Easy management of async events
  109. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Not coupled to any message format • Model can use different services based on context • Easy management of async events
  110. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Not coupled to any message format • Model can use different services based on context • Easy management of async events
  111. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Not coupled to any message format • Model can use different services based on context • Easy management of async events
  112. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Not coupled to any message format • Model can use different services based on context • Easy management of async events
  113. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Not coupled to any message format • Model can use different services based on context • Easy management of async events
  114. Resources • redux • ngrx • Scalable Single-Page Application Architecture

    • Demo
  115. Thank you! github.com/mgechev twitter.com/mgechev blog.mgechev.com