MVS: An angular MVC

Preview:

Citation preview

FRONT MVC@DRPICOX

THINGS WRONG THAT I DID TO FIT THINGS IN SLIDES

DISCLAIMER

• Directives should use controller and bindToController

• Directives should never load resources, this should be done by routes or under demand

• Use .factory to simplify and homogenize code (not .service neither .value)

• In a previous talk I said that model was plain JS data, sorry (plain data is the form, but does not defines a model)

• Use better variable names

MVC - MODEL THE GREAT FORGOTTEN

FRONT MVC

TYPICAL ANGULAR CODE

AS SEEN ON SOME DOCS (AND FROM SOME BEGINNERS)

TYPICAL ANGULAR CODE<APP-BOOKS-LIST>

app.component(‘appBooksList’, { template: ‘…’, controller($http) { $http.get(‘/api/v1/books’).then(response => { this.books = response.data; }); }, });

TYPICAL ANGULAR CODE<APP-SCI-FI -BOOKS-LIST>

app.component(‘appSciFiBooksList’, { template: ‘…’, controller($http) { $http.get(‘/api/v1/books’).then(response => { this.books = response.data.filter(isSciFi); }); }, });

REFACTORING - SERVICE

THERE IS REPLICATED CODE ¡LET’S DO A SERVICE!

REFACTORING - SERVICEbooksService

app.service(‘booksService’, ($http) => { this.getList = () => { return $http.get(‘/api/v1/books’).then(function(response) { return response.data; }); }; });

Acquires the responsibility for: • R1: Know the API context Url • R2: Create a remote connection • R3: Transform response into usable object

REFACTORING - SERVICE<APP-BOOKS-LIST> & <APP-SCI-FI -BOOKS-LIST>

app.component(‘appBooksList’, { template: ‘…’, controller(booksService) { booksService.getList().then(books => { this.books = books; }); }, });

app.component(‘appSciFiBooksList’, { template: ‘…’, controller(booksService) { booksService.getList().then(books => { this.books = books.filter(isSciFi); }); }, });

PERFORMANCE - CACHING

OPS! WE ARE LOADING DATA TWICE ¡LET’S DO A CACHE!

PERFORMANCE - CACHINGbooksService

Notice that: • Repairs all directives at once • Acquires one new responsibility (R4: manage cache)

app.service(‘booksService’, ($http) => { this.getList = () => { return $http.get(‘/api/v1/books’, {cache:true}).then(response => { return response.data; }); }; });

PERFORMANCE - CACHINGbooksService

Notice that: • Repairs all directives at once • Acquires one new responsibility (R4: manage cache)

app.service(‘booksService’, ($http) => { this.getList = () => { if (!this.list) { this.list = $http.get(‘/api/v1/books’).then(response => { return response.data; }); } return this.list; }; });

Alternate

NEW FEATURE - ONE BOOK

WE HAVE A VIEW THAT ONLY SHOWS ONE BOOK

NEW FEATURE - ONE BOOK<APP-BOOK-VIEW BOOK-ID=“ID”>

app.component(‘appBookView’, { template: ‘…’, bindings: { bookId: ‘<‘ }, controller: function(booksService) { booksService.get(this.bookId).then(book => { this.book = book; }); }, });

NEW FEATURE - ONE BOOKbooksService (step1)

Notice that: • What about repeating loading of books (R4: manage cache)

app.service(‘booksService’, ($http) => { this.loadAll = function() { return $http.get(‘/api/v1/books’, {cache:true}).then(response => { return response.data; }); }; this.load = (id) => { return $http.get(‘/api/v1/books/’+id).then(response => { return response.data; }); }; });

NEW FEATURE - ONE BOOKbooksService (step2)

• Uhmmmm… about R4… managing caches

app.service(‘booksService’, ($http) => { this.loadAll = function() { return $http.get(‘/api/v1/books’, {cache:true}).then(response => { return response.data; }); }; this.load = (id) => { return $http.get(‘/api/v1/books/’+id, {cache:true}).then(response => { return response.data; }); }; });

NEW FEATURE - ONE BOOK

WE ARE LOADING TWICE THE SAME BOOK!

It happens when: • Loading one book • Loading all books

NEW FEATURE - ONE BOOK

OK… MAY BE IT’S OK LOAD TWICE BUT NOT MORE TIMES

THE SAME BOOK

“We keep it simple…”

NEW FEATURE - UPDATE BOOK

WE HAVE A VIEW THAT MAKES AN UPDATE OF A BOOK AND SHOWS THE CHANGES INTRODUCED BY API

NEW FEATURE - UPDATE BOOK<APP-BOOK-UPDATE BOOK-ID=“ID”>

app.component(‘appBookUpdate’, { template: ‘…’, bindings: { bookId: ‘<‘, } constructor(booksService) { booksService.load(this).then(book => { this.book = book; }); this.update = () => { booksService.update(this.book).then(updatedBook => { this.book = updatedBook; }); }; }; });

NEW FEATURE - ONE BOOKbooksService (step2)

• Uhmmmm… about R4… managing caches

app.service(‘booksService’, ($http) => { this.getList = () => { return $http.get(‘/api/v1/books’, {cache:true}).then(response => { return response.data; }); }; this.get = (id) => { return $http.get(‘/api/v1/books/’+id, {cache:true}) .then(response => { return response.data; }); }; this.update = (book) => { return $http.put(‘/api/v1/books/’+book.id) .then(function(response) { return response.data; }); }; });

NEW FEATURE - UPDATE BOOK

SHIT!

NEW FEATURE - UPDATE BOOK

SHIT!

What about: • Cache? • What if we have a list side-by side, it is updated? • What if we have view and update side-by-side?

NEW FEATURE - UPDATE BOOK

SHIT!

How to solve it? • New services? • Callbacks? • RxJS? • More caches? • Events?

NEW FEATURE - UPDATE BOOK

WHAT IS THE REAL PROBLEM?

NEW FEATURE - UPDATE BOOK

WHAT IS THE REAL PROBLEM?

R4 (manage caches) is hidden another responsibility: • Single source of true

We need a model!

WHAT A MODEL IS NOT?

• A model is not styles in the web

• A model is not objects in the scope

• A model is not objects stored in the browser (local,websql,…)

• A model are not instances of any sort of data

• A model is not plain data

SINGLE SOURCE OF TRUE

WHAT IS A MODEL?

• You can find your model

• There is only one instance for each “concept” (all books with the same id are the same object)

• Models may be computed from other models

• It does not matters how many views do you have, all views use the same models (as do services also)

That is something that ember programmers knows very well.

REFACTOR - ADD MODELBook

app.value(‘Book’, function Book(id) { this.id = id; this.isSciFi = function() { return !!this.tags.sciFi; }; this.takeData = function(bookData) { angular.extend(this, bookData); }; });

We discover new responsibilities: • R5: update and extract information of a book

REFACTOR - ADD MODELbooksDictionary

app.value(‘booksDictionary’, (Book) => { this.list = []; this.map = {}; this.getOrCreate = (id) => { var book = this.map[id]; if (!book) { book = new Book(id); this.list.push(book); this.list.map[id] = book; }; return book; }; this.takeDatas = (bookDatas) => { bookInfos.forEach((bookData) => { this.getOrCreate(bookData.id).takeData(bookData); }); }; });

We discover new responsibilities: • R6: manage book instances to avoid replicates • R7: store books so anyone can access to them

Note that listis also a model.

booksService (loadAll)

app.service(‘booksService’, (booksDictionary, $http) => { this.getList = () => { if (!this.list) { this.list = $http.get(‘/api/v1/books’).then(response => { booksDictionary.takeDatas(response.data); return booksDictionary.list;

}); } return this.list; }); }; … });

REFACTOR - ADD MODEL

getList returns always the same instance of list.

booksService (load)

app.service(‘booksService’, (booksDictionary, $http) => { … this.books = {}; this.load = (id) => { if (!this.books[id]) { this.books[id] = $http.get(‘/api/v1/books/’+id).then(response => { var book = booksDictionary.getOrCreate(id); book.takeData(reponse.data); return book; }); } return this.books[id]; }; … });

REFACTOR - ADD MODEL

Load(id) returns always the same instance of book for the same id. And the instance will be contained by the list in getList.

booksService (update)

app.service(‘booksService’, (booksDictionary, $http) => { … this.update = (book) => { var id = book.id; this.books[id] = $http.put(‘/api/v1/books/’+id).then(response => { var book = booksDictionary.getOrCreate(id); book.takeData(response.data); return book; }); }; });

REFACTOR - ADD MODEL

Update returns always the same instance than get and getList, regardless the input. And also updates the same instance.

three layers

REFACTOR - ADD MODEL

SERVICES

MODELS VIEWS

callsupdates

are $watched

NEW FEATURE - UPDATE BOOK

DONE!

NEW FEATURE - UPDATE BOOK

DONE?

What about Sci-Fi list? • Is computed:

it has to be recomputed each time that a book o list changes

• A filter is expensive • Watch a derive function is expensive • Watch all books (aka $watch(,,true)) is expensive

NEW FEATURE - UPDATE BOOK

SHIT!

Solved in the next talk…

MVS: MVC IN ANGULAR

FRONT MVC

three kinds of components

ORIGINAL MVC

CONTROLLERS

MODELS VIEWS

are listenedupdates

are observed

the truth

ORIGINAL MVC

MODELS VIEWSknows and queries

are listenedupdates

are observed

knows and queries

CONTROLLERS

ORIGINAL MVC

SHIT!

ORIGINAL MVC

SHIT!

It is not as beautiful and easy as it seems. May be it worked in ’78, with just few Kb… and low complexity, but now…

FB comments about MVC

ARCHITECTURES VARIATIONS

https://youtu.be/nYkdrAPrdcw?t=10m20s

FB comments about MVC

ARCHITECTURES VARIATIONS

“MVC works pretty well for small applications…

but it doesn’t make room for new features.”

“Flux is a single direction data flow, that avoids all the arrows

going on all directions what makes really

hard to understand the system.”

FB comments about MVC

ARCHITECTURES VARIATIONS

“Let’s see a real good example: FB chat”

“How we get to the point, so we were annoying our users so much the just they wanted us to fix chat?”

“The problems here were: • The code has no structure • It was very imperative, that makes it fragile • It loose a lot of the original intend behind it, its hard to tell what it tries [to do] • Add more features only gets this code larger • We had our most annoying chat bug happen over and over • We were always fixing some pretty good edge case, the whole system was fragile • … • This code becomes more fragile with the time. • No member of the team wanted to touch it, they wanted to jump to any other bug.”

f lux

ARCHITECTURES VARIATIONS

AND COMMUNITY ALSO CONTRIBUTED…

ARCHITECTURES VARIATIONS

redux

ARCHITECTURES VARIATIONS

cyclejs

ARCHITECTURES VARIATIONS

Key points are:

• Single direction flow

• Avoid imperative code

• Decoupled state from views

• Computed data consistency (example: cart.price = ∑item.price * item.quantity)

ARCHITECTURES VARIATIONS

mvc

FROM MVC TO MVS

MODELS VIEWSknows and queries

are listenedupdates

are observed

knows and queries

CONTROLLERS

mvc-angular

FROM MVC TO MVS

MODELS VIEWSknows and queries

are $watched

CONTROLLERS

the same f low

FROM MVC TO MVS

SERVICES

MODELS VIEWS

callsupdates

are $watched

MVS RULES (I)

SERVICES

MODELS VIEWS

calls

updates

are $watched

• Views can call to services

• Services manages models

• Models are watched by views through $watch

• Views cannot modify models but temporary copies

• Each services has its own models and do not modify other services models

Key points are:

• Single direction flow

• Avoid imperative code

• Decoupled state from views

• Computed data consistency (example: sciFiBooks = books.filter(isSciFi))

ANGULAR MVS

SOLUTION FOR SCI-FI-BOOKS

• Create a new model for SciFiBooks

• Add a method getSciFiBooks() to bookService

• Recompute the list each time that an update() method is called

BUT!

SOLUTION FOR SCI-FI-BOOKS

BUT!

It does not scale. ¿What happens with more kind of books? ¿What happens with more complex data?

SOLUTION FOR SCI-FI-BOOKS

SOLUTION FOR SCI-FI-BOOKS

• We need a synchronization mechanism

• ex: events or observables

SOLUTION FOR COMPUTED VALUES

MODELS

• Models launch update: ex $broadcast(‘booksChanged’)

for angular1

fire events

SOLUTION FOR COMPUTED VALUES

MODELS

• Models launch update: ex $broadcast(‘booksChanged’)

• Rules:

• Each model has its own event (1 <-> 1)

• We do not pass any information (ex: previous and new value)

• Change event is fired by the Service that manages the model

• Events can be debounced (multiple collapsed in one)

rules

fire events

booksService (update)

app.service(‘booksService’, (booksDictionary, $http, $broadcast) => { … this.update = (book) => { var id = book.id; this.books[id] = $http.put(‘/api/v1/books/’+id).then(response => { var book = booksDictionary.getOrCreate(id); book.takeData(response.data); $broadcast(‘booksChanged’); return book; }); }; });

SOLUTION FOR SCI-FI-BOOKS

<APP-SCI-FI -BOOKS-LIST> (update)

SOLUTION FOR SCI-FI-BOOKS

app.component(‘appSciFiBooksList’, { template: ‘…’, controller(booksService, $scope) { getSciFiBooks(); $scope.$on(‘booksChanged’, getSciFiBooks);

function getSciFiBooks() { booksService.getList().then(books => { this.books = books.filter(isSciFi); }); } }, });

<APP-SCI-FI -BOOKS-LIST> (update)

SOLUTION FOR SCI-FI-BOOKS

app.component(‘appSciFiBooksList’, { template: ‘…’, controller(booksService, $scope) { getSciFiBooks(); $scope.$on(‘booksChanged’, getSciFiBooks);

function getSciFiBooks() { booksService.getList().then(books => { this.books = books.filter(isSciFi); }); } }, });

It smells to a service function…

sciFiBooksService (update)

app.service(‘sciFiBooksService’, (booksService, $rootScope) => { this.list = []; this.getList = () => this.list; updateList(); $rootScope.$on(‘booksChanged’, updateList); function updateList() { booksService.getList().then(books => { angular.copy(books.filter(isSciFi), this.list); $broadcast(‘sciFiChanged’); }); } });

SOLUTION FOR SCI-FI-BOOKS

<APP-SCI-FI -BOOKS-LIST> (update)

SOLUTION FOR SCI-FI-BOOKS

app.component(‘appSciFiBooksList’, { template: ‘…’, controller(sciFiBooksService, $scope) { sciFiBooksService.getList().then(books => { this.books = books; }); }, });

<APP-SCI-FI -BOOKS-LIST> (update)

SOLUTION FOR SCI-FI-BOOKS

app.component(‘appSciFiBooksList’, { template: ‘…’, controller(booksService, $scope) { getSciFiBooks(); $scope.$on(‘booksChanged’, getSciFiBooks);

function getSciFiBooks() { booksService.getList().then(books => { this.books = books.filter(isSciFi); }); } }, });

It smells to a service function…

Hierarchy

SOLUTION FOR SCI-FI-BOOKS

book Service

sciFiBooks Service

is listened

calls

Main Rule: no cycles of the arrow same color

Hierarchy

SOLUTION FOR SCI-FI-BOOKS

product Service

Main Rule: no cycles of the arrow same color

showroom Service

user Service

cart Service

shipping Service

payment method Service

addresses Service

MVS: MVC IN ANGULAR

FRONT MVC

the same f low

MVS

SERVICES

MODELS VIEWS

callsupdates

are $watched

the same f low

MVS

SERVICES

MODELS VIEWS

calls

updates

are $watched

Dumb Views

Smart Views

BROWSER

uses listens

uses listens

the same f low

MVS

SERVICES

MODELS VIEWS

calls

updates

are $watched

Dumb Views

Smart Views

BROWSER

uses listens

uses listens

Services

Remotes Storages

callscalls

the same f low

MVS

SERVICES

MODELS VIEWS

calls

updates

are $watched

Dumb Views

Smart Views

BROWSER

uses listens

uses listens

Services

Remotes Storages

callscalls

State Holders

State Singletons

State Dictionaries

CI

CI

CICICI

computed data f low

MVS

SERVICES

MODELS VIEWS

calls

updates

are $watched

Dumb Views

Smart Views

BROWSER

uses listens

uses listens

Services

Remotes Storages

callscalls

State Holders

State Singletons

State Dictionaries

CI

CI

CICICI

computed data f low

MVS

SERVICES

MODELS VIEWS

calls

updates

are $watched

Dumb Views

Smart Views

BROWSER

uses listens

uses listens

Services

Remotes Storages

callscalls

State Holders

State Singletons

State Dictionaries

CI

CI

CICICI

are listened

are listened

step by step

COMPUTED DATA FLOW

1. call

5. is fired

Books Service

Books Dictionary

Sci-Fi Service

Sci-Fi Dictionary

2. updates

3. fire $update

7. updates

8. fire $update

$UPDATE EVENT MANAGER

4. fire $update

6. reads

Recommended