MVS: An angular MVC
-
Upload
david-rodenas -
Category
Internet
-
view
57 -
download
0
Transcript of MVS: An angular MVC
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