solid angular
TRANSCRIPT
HOW ANGULAR CAN SOLVE ALL YOUR PROBLEMS
Nir Kaufman
IT CAN’T
Nir Kaufman
- I don’t really need glasses to see - This photo is a photoshop - In reality I’m in color (and fatter)
Head of AngularJS Development @ 500Tech
INTRODUCTION
“We are not happy with our app. it should be modular, easy to extend and maintain. It’s hard to understand the flow, feels like a spaghetti of presentation and business logic.
- frontend team at {{ company.name }}
WE NEED A BETTER FRAMEWORK
WHAT DO WE NEED?
COMPONENTS
A clean way of organizing your UI code into self-contained, reusable chunks
// component controller class likeBoxController { constructor(params) { this.chosenValue = params.value; } like() { this.chosenValue('like'); } dislike() { this.chosenValue('dislike'); } } // component definition function likeBoxComponent() { return { viewModel: likeBoxController, template: likeBoxTemplate } } // component registration ko.components.register('like-widget', likeBoxComponent());
// component controller class likeBoxController { constructor() { this.chosenValue = null; } like() { this.chosenValue = 'like'; } dislike() { this.chosenValue = 'dislike'; } } // component definition function likeBoxComponent() { return { controller: likeBoxController, scope: { params: ‘=chosenValue' }, controllerAs: 'LikeBox', bindToController: true, template: likeBoxTemplate } } angular.module('app', []) .directive('likeWidget', likeBoxComponent);
<div class="like-or-dislike" data-bind="visible: !chosenValue()"> <button data-bind="click: like">Like it</button> <button data-bind="click: dislike">Dislike it</button> </div> <div class="result" data-bind="visible: chosenValue"> You <strong data-bind="text: chosenValue"></strong> it </div>
<div class="like-or-dislike" ng-hide="LikeBox.chosenValue"> <button ng-click="LikeBox.like()">Like it</button> <button ng-click="LikeBox.dislike()">Dislike it</button> </div> <div class="result" ng-show="LikeBox.chosenValue"> You <strong ng-bind="LikeBox.chosenValue"></strong> it </div>
class LikeWidget extends React.Component { constructor(props) { super(props); this.state = { chosenValue: null }; this.like = this.like.bind(this); this.dislike = this.dislike.bind(this); this._likeButtons = this._likeButtons.bind(this) } like() { this.setState({ chosenValue: 'like' }) } dislike() { this.setState({ chosenValue: 'dislike' }) } _likeButtons() { if (this.state.chosenValue) { return null } return ( <div> <button onClick={this.like}>Like it</button> <button onClick={this.dislike}>Dislike it</button> </div> ) } render() { return ( <div> { this._likeButtons() } { this.state.chosenValue ? <div>You <strong>{ this.state.chosenValue }</strong> it </div> : null} </div> ) } } React.render(<LikeWidget/>, document.getElementById('app'));
MVW PATTERN
Keep your business logic separate from your user interface
class MyViewModel { constructor() { this.products = [ new Product('Garlic bread'), new Product('Pain au chocolat'), new Product('Seagull spaghetti', 'like') ]; } } ko.applyBindings(new MyViewModel());
class MyViewModel { constructor() { this.products = [ new Product('Garlic bread'), new Product('Pain au chocolat'), new Product('Seagull spaghetti', 'like') ]; } } angular.module('app', []) .controller('MyViewModel', MyViewModel);
Backbone.Model.extend({ defaults: { coverImage: 'img/placeholder.png', title: 'No title', author: 'Unknown', releaseDate: 'Unknown', keywords: 'None' } });
DS.Model.extend({ title: DS.attr('No title'), author: DS.attr('Unknown'), releaseDate: DS.attr('Unknown'), keywords: DS.attr('None') });
class Book { constructor() { this.coverImage = 'img/placeholder.png'; this.title = 'No title'; this.author = 'Unknown'; this.releaseDate = 'Unknown'; this.keywords = 'None'; } }
LETS GET TO THE POINT
All major frameworks introduce the same concepts.
Don’t make a switch for the wrong reasons. Switching to another framework won’t solve your design problems.
OBJECT ORIENTED PROGRAMMING
CONSIDER TYPESCRIPT
I used to hate it…
SOLID PRINCIPLESSingle Responsibility
Open / Closed
Liskov Substitution
Interface Segregation
Dependency Inversion
S.O.L.I.DSingle Responsibility Principle
A module should have one, and only one reason to change
// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) .config(($stateProvider, $httpProvider, localStorageServiceProvider) => { // start routing $stateProvider .state('dashboard', { url: '/dashboard', templateUrl: 'states/dashboard/dashboard.html', controller: 'DashboardController' }) .state('users', { url: '/users', templateUrl: 'states/users/users.html', controller: 'UsersController' }); // http configuration $httpProvider.useApplyAsync(true); $httpProvider.useLegacyPromiseExtensions(false); $httpProvider.interceptors.push(($log) => { return { 'request': function (config) { $log.debug(config); return config; } }; }); // storage configurations localStorageServiceProvider .setPrefix('myApp') .setStorageType('sessionStorage') }); // start engines angular.bootstrap(document, ['app']);
4 reasons to change this module:
add dependency add new state configure http service configure storage service
// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) .config(routes) .config(http) .config(storage) // start engines angular.bootstrap(document, ['app']);
export function routes($stateProvider) { $stateProvider .state('dashboard', { url: '/dashboard', templateUrl: 'states/dashboard/dashboard.html', controller: 'DashboardController' }) .state('users', { url: '/users', templateUrl: 'states/users/users.html', controller: 'UsersController' }); }
routes.ts
app.ts
export function http ($httpProvider) { $httpProvider.useApplyAsync(true); $httpProvider.useLegacyPromiseExtensions(false); }
http.ts
export function storage(localStorageServiceProvider) { localStorageServiceProvider .setPrefix('myApp') .setStorageType('sessionStorage') }
storage.ts
// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) .config(routes) .config(http) .config(storage) // start engines angular.bootstrap(document, ['app']);
Are we there yet?
2 reasons to change
// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) // start engines angular.bootstrap(document, ['app']);
1 responsibility
S.O.L.I.DOpen / Closed Principle
A module should be open for extension, but closed for modification.
class Modal { constructor($modal) { this.modal = $modal; } show(type) { switch (type) { case 'login': this.showLoginModal(); break; case 'info': this.showInfoModal(); break; } } showLoginModal() { this.modal.open({ template: 'loginModal.html', controller: ‘loginModalController’ }) } showInfoModal() { this.modal.open({ template: 'infoModal.html', controller: 'infoModalController' }) } }
class Controller { constructor(Modal) { this.Modal = Modal; } showLogin(){ this.Modal.show('login'); } }
We need to add new Modals to our system
class Modal { constructor($modal) { this.modal = $modal; this.modals = new Map(); } register(type, config) { this.modals.set(type, config) } $get() { return { show: this.show } } show(type) { if(this.modals.has(type)){ this.modal.open(this.modals.get(type)) } } }
angular.module('app', []) .config(ModalProvider => { ModalProvider.register('lostPassword', { template: 'lostPassword.html', controller: 'lostPasswordController' }) });
class Controller { constructor(Modal) { this.Modal = Modal; } showLogin(){ this.Modal.show('lostPassword'); } }
Write code that can be extended
S.O.L.I.DLiskov Substitution Principle
Child classes should never break the parent class type definitions
IT’S ABOUT INHERITANCE
DON’T DO IT
S.O.L.I.DInterface Segregation Principle
Many specific interfaces are better than one generic interface
I just want to make a copy
class LocalStorage { private storage; private $window; constructor($window, $q) { this.$window = $window; } setStorageType(type:string) { if (type === 'local') { return this.storage = this.$window.localStorage; } if (type === 'db') { return this.storage = new PouchDB('DB'); } } setLocalItem(key:string, data) { if (this.db) { return this.db.put(JSON.parse(data)) } return this.storage.setItem(key, JSON.stringify(data)); }
put(data) { this.storage.put(JSON.parse(data)) }
getLocalItem(key:string):string { let deferred = this.$q.defer(); if (this.db) { this.db.get(key).then( result => deferred.resolve() ) } deferred.resolve(this.storage.getItem()); return deferred.promise; } }
No client should be forced to depend on methods it doesn’t use
class LocalStorage { private storage; constructor($window) { this.storage = $window.localStorage; } setItem(key:string, data) { return this.storage.setItem(key, JSON.stringify(data)); } getItem(key:string):string { return this.storage.getItem(); } }
class SessionStorage { private storage; constructor($window, $q) { this.storage = $window.sessionStorage; } setItem(key:string, data) { return this.storage.setItem(key, JSON.stringify(data)); } getItem(key:string):string { return this.storage.getItem(); } }
localStorage.ts
sessionStorage.ts
class DBStorage { private db; constructor(PouchDB) { this.db = new PouchDB('DB'); } put(data) { this.db.put(data) } get(id){ return this.db.get(id); } }
dbStorage.ts
UserComponent.ts (client)class UserComponent { private storage; constructor(LocalStorage) { this.storage = LocalStorage } }
S.O.L.I.DDependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
NATIVE API’s
ANGULAR
3RD PARTY MODULES
APLLICATION CODE
INTERFACES
Application Layers
YOUR MODULES
Abstraction
Abstraction
interface IStorage { setItem(key:string, data:any); getItem(key:string, data:any); }
IStorgae.ts UserComponent.ts (client)class UserComponent { private storage; constructor(Storage: IStorage) { this.storage = LocalStorage } }
LocalStorage.ts
class LocalStorage implements IStorage { private storage; constructor($window) { this.storage = $window.localStorage; } setItem(key:string, data) { return this.storage.setItem(key, JSON.stringify(data)); } getItem(key:string):string { return this.storage.getItem(); } }
The ‘client’ can work with any kind of storage that implements the IStorage interface
export class WelcomeController { constructor($modal) { this.$modal = $modal; } login() { let loginModalInstance = this.$modal.open({ templateUrl: 'states/welcome/login_modal.html', keyboard: false, backdrop: 'static', controller: LoginModalController, controllerAs: 'Login' }); loginModalInstance.result .then(result => console.log(result)) } }
Angular Bootstrap Modal
export class DashboardController { constructor($modal) { this.$modal = $modal; } showInfo() { let infoModalInstance = this.$modal.open({ templateUrl: 'states/dashboard/info_modal.html', keyboard: false, backdrop: 'static', controller: InfoModalController, controllerAs: 'Info' }); infoModalInstance.result .then(result => console.log(result)) } }
What is the problem?
class Modal { constructor($modal) { this.modal = $modal; this.modals = new Map(); } register(type, config) { this.modals.set(type, config) } $get() { return { show: this.show } } show(type) { if(this.modals.has(type)){ this.modal.open(this.modals.get(type)) } } }
Abstraction without TypeScript
SUMMARY
DON’T MAKE A SWITCH FOR THE WRONG
REASONS
DESIGN PATTENS
MATTER
GOOD DESIGN IS FRAMEWORK
AGNOSTIC
THANK YOU!
Angular ES6 / TypeScript Startershttps://github.com/nirkaufman/angular-webpack-starter
https://github.com/nirkaufman/angular-webpack-typescript-starter
resourceshttps://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design
http://code.tutsplus.com/series/the-solid-principles--cms-634
http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Read our blog:http://blog.500tech.com