Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cutting Angular's Crosscuts

Cutting Angular's Crosscuts

No matter how DRY our code gets, there are times when the object-oriented paradigm is just not powerful enough to handle all code duplications. Applying logging and authorisation, for example, makes us copy and paste snippets all around our code-base without being able to isolate them in separate modules. This makes our code more coupled; less reusable and maintainable.

The aspect-oriented programming comes with a solution for such “cross-cutting concerns”. It is already widely used in the Java world, in AspectJ and Spring. In my talk I will bring the aspect-oriented programming paradigm to Angular. I’ll explain how to deal with duplications and make our code more maintainable using AOP with the new ECMAScript 2016 decorators’ syntax.

Minko Gechev

October 21, 2015
Tweet

More Decks by Minko Gechev

Other Decks in Technology

Transcript

  1. Agenda • Title 1 • Subtitle 1 • Subtitle 2

    • Title 2 • Subtitle 1 https://www.flickr.com/photos/cinamonas/3191659268/ Why it was invented?
  2. Agenda • Title 1 • Subtitle 1 • Subtitle 2

    • Title 2 • Subtitle 1 https://www.flickr.com/photos/fastlizard4/5391914387/ …something more powerful was required
  3. user.ts export class User { public id: number; public name:

    string; public email: string; public website: string; }
  4. user.ts export class User { // ... save() { return

    new Promise((resolve, reject) => { var params = this._serialize(); http.open('POST', URL, true); http.setRequestHeader(‘Content-type', 'application/x-www-form-urlencoded'); http.onreadystatechange = _ => { if (http.readyState === 4) { if (http.status === 200) { resolve(); } else { reject(); } } } http.send(params); }); } }
  5. user.ts export class User { // ... save() { return

    new Promise((resolve, reject) => { var params = this._serialize(); http.open('POST', URL, true); http.setRequestHeader(‘Content-type', 'application/x-www-form-urlencoded'); http.onreadystatechange = _ => { if (http.readyState === 4) { if (http.status === 200) { resolve(); } else { reject(); } } } http.send(params); }); } }
  6. http.ts export class Http { get(url) { return fetch(url) .then(res

    => { return res.json(); }); } post(url, data) { return fetch(url, { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(res => { return res.json(); }); } }
  7. http.ts export class Http { get(url) { return fetch(url) .then(res

    => { return res.json(); }); } post(url, data) { return fetch(url, { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(res => { return res.json(); }); } }
  8. http.ts export class Http { get(url) { return fetch(url) .then(res

    => { return res.json(); }); } post(url, data) { return fetch(url, { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(res => { return res.json(); }); } }
  9. user_finder.ts export class UserFinder { constructor( @Inject(Http) private http:Http, @Inject(API_SERVER)

    private url:string) { } get(id: number): Promise<User> { return this.http.get(this.url + '/' + id) .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; return user; }); } }
  10. export class UserFinder { constructor( @Inject(Http) private http:Http, @Inject(API_SERVER) private

    url:string) { } get(id: number): Promise<User> { return this.http.get(this.url + '/' + id) .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; return user; }); } } user_finder.ts
  11. export class UserFinder { constructor( @Inject(Http) private http:Http, @Inject(API_SERVER) private

    url:string) { } get(id: number): Promise<User> { return this.http.get(this.url + '/' + id) .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; return user; }); } } user_finder.ts
  12. export class UserFinder { // ... get(id: number): Promise<User> {

    let user = JSON.parse( localStorage.getItem(id.toString()) || 'null' ); let promise; if (user) { promise = Promise.resolve(user); } else { promise = this.http.get(this.url + '/' + id); } return promise .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; localStorage.setItem(id.toString(), JSON.stringify(user)); return user; }); } } user_finder.ts
  13. export class UserFinder { // ... get(id: number): Promise<User> {

    let user = JSON.parse( localStorage.getItem(id.toString()) || 'null' ); let promise; if (user) { promise = Promise.resolve(user); } else { promise = this.http.get(this.url + '/' + id); } return promise .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; localStorage.setItem(id.toString(), JSON.stringify(user)); return user; }); } } user_finder.ts
  14. http.ts export class Http { // ... get(url:string) { if

    (cache.has(url)) { return Promise.resolve(cache.get(url)); } else { return fetch(url) .then(res => { return res.json(); }) .then(data => { cache.set(url, data); return data; }); } } }
  15. http.ts export class Http { // ... get(url:string) { if

    (cache.has(url)) { return Promise.resolve(cache.get(url)); } else { return fetch(url) .then(res => { return res.json(); }) .then(data => { cache.set(url, data); return data; }); } } }
  16. export class UserFinder { // ... get(id: number): Promise<User> {

    let user = JSON.parse( localStorage.getItem(id.toString()) || 'null' ); let promise; if (user) { promise = Promise.resolve(user); } else { promise = this.http.get(this.url + '/' + id); } return promise .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; localStorage.setItem(id.toString(), JSON.stringify(user)); return user; }); } } user_finder.ts
  17. export class UserFinder { // ... get(id: number): Promise<User> {

    let user = JSON.parse( localStorage.getItem(id.toString()) || 'null' ); let promise; if (user) { promise = Promise.resolve(user); } else { promise = this.http.get(this.url + '/' + id); } return promise .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; localStorage.setItem(id.toString(), JSON.stringify(user)); return user; }); } } user_finder.ts
  18. http.ts export class Http { // ... get(url:string) { let

    promise; if (cache.has(url)) { promise = Promise.resolve(cache.get(url)); } else { promise = fetch(url) .then(res => { return res.json(); }) .then(data => { cache.set(url, data); return data; }); } return promise .then(data => { return data; }); } }
  19. http.ts export class Http { // ... get(url:string) { let

    promise; if (cache.has(url)) { promise = Promise.resolve(cache.get(url)); } else { promise = fetch(url) .then(res => { return res.json(); }) .then(data => { cache.set(url, data); return data; }); } return promise .then(data => { return data; }); } }
  20. export class UserFinder { // ... get(id: number): Promise<User> {

    let user = JSON.parse( localStorage.getItem(id.toString()) || 'null' ); let promise; if (user) { promise = Promise.resolve(user); } else { promise = this.http.get(this.url + '/' + id); } return promise .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; localStorage.setItem(id.toString(), JSON.stringify(user)); return user; }); } } user_finder.ts
  21. export class UserFinder { // ... get(id: number): Promise<User> {

    let user = JSON.parse( localStorage.getItem(id.toString()) || 'null' ); let promise; if (user) { promise = Promise.resolve(user); } else { promise = this.http.get(this.url + '/' + id); } return promise .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; localStorage.setItem(id.toString(), JSON.stringify(user)); return user; }); } } user_finder.ts
  22. export class UserFinder { // ... get(id: number): Promise<User> {

    return this.http.get(this.url + '/' + id) .then(res => { let user = new User(); user.id = res.id; user.name = res.name; user.email = res.email; user.website = res.website; return user; }); } } user_finder.ts
  23. http.ts export class Http { // ... get(url:string) { let

    promise; if (cache.has(url)) { promise = Promise.resolve(cache.get(url)); } else { promise = fetch(url) .then(res => { return res.json(); }) .then(data => { cache.set(url, data); return data; }); } return promise .then(data => { return data; }); } }
  24. http.ts export class Http { // ... get(url:string) { let

    promise; if (cache.has(url)) { promise = Promise.resolve(cache.get(url)); } else { promise = fetch(url) .then(res => { return res.json(); }) .then(data => { cache.set(url, data); return data; }); } return promise .then(data => { return data; }); } }
  25. http.ts export class Http { // ... get(url:string) { return

    fetch(url) .then(res => { return res.json(); }); } }
  26. cache_aspect.ts export class CacheAspect { @before(/^UserMapper$/, /^get/) beforeUserFinderGetInvocation(meta, id) {

    this.queryCache(lsCache, meta.method, id); } @after(/^UserMapper$/, /^get/) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  27. cache_aspect.ts export class CacheAspect { @before(/^UserMapper$/, /^get/) beforeUserFinderGetInvocation(meta, id) {

    this.queryCache(lsCache, meta.method, id); } @after(/^UserMapper$/, /^get/) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  28. cache_aspect.ts export class CacheAspect { @before(/^UserMapper$/, /^get/) beforeUserFinderGetInvocation(meta, id) {

    this.queryCache(lsCache, meta.method, id); } @after(/^UserMapper$/, /^get/) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  29. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  30. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  31. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  32. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  33. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  34. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  35. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  36. cache_aspect.ts export class CacheAspect { @beforeMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/

    }) beforeUserFinderGetInvocation(meta, id) { this.queryCache(lsCache, meta.method, id); } @afterMethod({ classNamePattern: /^UserFinder$/, methodNamePattern: /^get/ }) afterUserFinderGetInvocation(meta, id) { this.setCache(lsCache, meta.method, id); } // same for Http queryCache(cache, method, id) { } setCache(cache, method, id) { } }
  37. cache_aspect.ts export class CacheAspect { // ... queryCache(cache, method, id)

    { let res = cache.get(id); if (res) { method.proceed = false; return Promise.resolve(res); } } setCache(cache, method, id) { return method.result.then(data => { cache.set(id, data); return data; }); } }
  38. cache_aspect.ts export class CacheAspect { // ... queryCache(cache, method, id)

    { let res = cache.get(id); if (res) { method.proceed = false; return Promise.resolve(res); } } setCache(cache, method, id) { return method.result.then(data => { cache.set(id, data); return data; }); } }
  39. cache_aspect.ts export class CacheAspect { // ... queryCache(cache, method, id)

    { let res = cache.get(id); if (res) { method.proceed = false; return Promise.resolve(res); } } setCache(cache, method, id) { return method.result.then(data => { cache.set(id, data); return data; }); } }
  40. cache_aspect.ts export class CacheAspect { // ... queryCache(cache, method, id)

    { let res = cache.get(id); if (res) { method.proceed = false; return Promise.resolve(res); } } setCache(cache, method, id) { return method.result.then(data => { cache.set(id, data); return data; }); } }
  41. export class CacheAspect { @before(/.*/, /^get/) before(m, param) { }

    @after(/.*/, /^get/) after(m, id) { } } @Wove() export class Http { get(url:string) { // ... } } http.ts cache_aspect.pseudo
  42. http.ts export class Http { get(url) { CacheAspect.before(metadata); Http.originalGet(url); CacheAspect.after(metadata);

    } } http.pseudo @Wove() export class Http { get(url:string) { // ... } } export class CacheAspect { @before(/.*/, /^get/) before(m, param) { } @after(/.*/, /^get/) after(m, id) { } } cache_aspect.pseudo
  43. http.ts export class Http { get(url) { CacheAspect.before(metadata); Http.originalGet(url); CacheAspect.after(metadata);

    } } http.pseudo @Wove() export class Http { get(url:string) { // ... } } export class CacheAspect { @before(/.*/, /^get/) before(m, param) { } @after(/.*/, /^get/) after(m, id) { } } cache_aspect.pseudo
  44. http.ts export class Http { get(url) { CacheAspect.before(metadata); Http.originalGet(url); CacheAspect.after(metadata);

    } } http.pseudo @Wove() export class Http { get(url:string) { // ... } } export class CacheAspect { @before(/.*/, /^get/) before(m, param) { } @after(/.*/, /^get/) after(m, id) { } } cache_aspect.pseudo
  45. http.ts export class Http { get(url) { CacheAspect.before(metadata); Http.originalGet(url); CacheAspect.after(metadata);

    } } http.pseudo @Wove() export class Http { get(url:string) { // ... } } export class CacheAspect { @before(/.*/, /^get/) before(m, param) { } @after(/.*/, /^get/) after(m, id) { } } cache_aspect.pseudo
  46. http.ts export class Http { get(url) { CacheAspect.before(metadata); Http.originalGet(url); CacheAspect.after(metadata);

    } } http.pseudo @Wove() export class Http { get(url:string) { // ... } } export class CacheAspect { @before(/.*/, /^get/) before(m, param) { } @after(/.*/, /^get/) after(m, id) { } } cache_aspect.pseudo
  47. – Ryan Singer “So much complexity in software comes from

    trying to make one thing do two things.”