Scalable Angular 2 Application Architecture

115

Transcript of Scalable Angular 2 Application Architecture

Scalable Application rchitecture

github.com/mgechev twitter.com/mgechev blog.mgechev.com

STANG2

50% off

Agenda

– Martin Fowler

“…decisions that are hard to change…”

Architecture

Story Time

Dynamic Requirements

Scalable communication layer

Communication layer

• RESTful API • WebSocket application service • WebRTC data-channel

Various package formats

Package formats

• RESTful API • JSON commands

• WebSocket application service • JSON-RPC

• WebRTC data-channel • BERT-RPC

Multiple state mutation sources

Scalable team

Lazy-loading

Dynamic RequirementsScalable Communication Layer

Various package formatsMultiple state mutation sources

Scalable teamLazy-loading

Dynamic RequirementsScalable Communication Layer

Various package formatsMultiple state mutation sources

Scalable teamLazy-loading

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.

WebRTC Gateway

WebSocket Gateway

Gateway

WebRTC Gateway

WebSocket Gateway

Dynamic RequirementsScalable Communication Layer

Various package formatsMultiple state mutation sources

Scalable teamLazy-loading

redux

Dynamic RequirementsScalable Communication Layer

Various package formatsMultiple state mutation sources

Scalable teamLazy-loading

Modular

. src

multi-player commands components gateways single-player components home components shared

. src

multi-player commands components gateways single-player components home components shared

/home

. src

multi-player commands components gateways single-player components home components shared

/single-player

. src

multi-player commands components gateways single-player components home components shared

/multi-player

Sample Tech Stack

• Angular 2 • RxJS • ngrx

• TypeScript • ImmutableJS

RxJS in 2 slides

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

higher-order-functions.ts

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

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

Sample application

High-level Architecture

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

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

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

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

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

UI components

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

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

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

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

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!')); } }

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!')); } }

@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

@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

@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

@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

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

Component

Model

Store

Dispatcher

startGame()dispatch(action)

applyReducers(action, store)

next(state)

Component

Model

Store

Dispatcher

startGame()dispatch(action)

applyReducers(action, store)

next(state)

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; };

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; };

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; };

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; };

Component

Model

Store

Dispatcher

startGame()dispatch(action)

applyReducers(action, store)

next(state)

game.component.ts

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

game.component.ts

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

game.component.ts

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

game.component.html

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

game.component.html

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

Async Services

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

Remote Service App

Remote Service App

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

base.async-service.ts

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

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

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

But what if…

Model

S1 S2

Model

S1 S2

A

Model

S1 S2

A

Model

S1 S2A

Model

S1 S2A

Model

S1 S2

A

Model

S1 S2A

Immutability

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

// { name: 'Joe' } console.log(user.toJS());

immutable.js

No static typing

Immutability or Static Typing

Why not

Both?

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

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

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

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

Recap

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

signup(data)

Async services

Business facade

Business logicCommunication logic

Immutable app state Component tree

root cmp

sign-up form

userUserModel

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$

StreamDependencyAction (manipulation/method call)

User registration

user.email = email user.name = name

signup(data)

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

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

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

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

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

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

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

Thank you!github.com/mgechev twitter.com/mgechev blog.mgechev.com