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.

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

October 21, 2015
Tweet

Transcript

  1. 4.

    Agenda • Title 1 • Subtitle 1 • Subtitle 2

    • Title 2 • Subtitle 1 https://www.flickr.com/photos/cinamonas/3191659268/ Why it was invented?
  2. 6.
  3. 8.
  4. 10.

    Agenda • Title 1 • Subtitle 1 • Subtitle 2

    • Title 2 • Subtitle 1 https://www.flickr.com/photos/fastlizard4/5391914387/ …something more powerful was required
  5. 19.
  6. 20.

    user.ts export class User { public id: number; public name:

    string; public email: string; public website: string; }
  7. 21.
  8. 22.

    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); }); } }
  9. 23.

    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); }); } }
  10. 24.
  11. 25.
  12. 28.
  13. 29.
  14. 30.

    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(); }); } }
  15. 31.

    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(); }); } }
  16. 32.

    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(); }); } }
  17. 34.

    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; }); } }
  18. 35.

    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
  19. 36.

    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
  20. 43.

    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. 44.

    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. 46.

    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; }); } } }
  23. 47.

    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; }); } } }
  24. 51.

    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
  25. 52.

    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
  26. 53.

    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; }); } }
  27. 54.

    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; }); } }
  28. 57.
  29. 61.

    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
  30. 62.

    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
  31. 63.

    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
  32. 64.

    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; }); } }
  33. 65.

    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; }); } }
  34. 66.

    http.ts export class Http { // ... get(url:string) { return

    fetch(url) .then(res => { return res.json(); }); } }
  35. 68.

    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) { } }
  36. 69.

    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) { } }
  37. 70.

    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) { } }
  38. 72.

    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) { } }
  39. 73.

    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) { } }
  40. 74.

    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) { } }
  41. 75.

    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) { } }
  42. 76.

    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) { } }
  43. 77.

    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) { } }
  44. 78.

    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) { } }
  45. 79.

    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) { } }
  46. 80.

    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; }); } }
  47. 81.

    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; }); } }
  48. 82.

    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; }); } }
  49. 83.

    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; }); } }
  50. 84.
  51. 85.
  52. 87.

    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
  53. 88.

    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
  54. 89.

    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
  55. 90.

    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
  56. 91.

    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
  57. 92.

    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
  58. 95.
  59. 96.
  60. 97.

    – Ryan Singer “So much complexity in software comes from

    trying to make one thing do two things.”