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. Cutting Angular’s Crosscuts Minko Gechev github.com/mgechev twitter.com/mgechev blog.mgechev.com https://www.flickr.com/photos/davefrost/15704668417/

  2. github.com/mgechev twitter.com/mgechev blog.mgechev.com

  3. Lets talk about object-oriented programming

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

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

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

    • Title 2 • Subtitle 1
  7. Large project

  8. None
  9. https://www.flickr.com/photos/jackson22/16706843701 Simple tools were not enough

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

    • Title 2 • Subtitle 1 https://www.flickr.com/photos/fastlizard4/5391914387/ …something more powerful was required
  11. https://www.flickr.com/photos/bcbusinesshub/17020592469/ A new idea

  12. Brings 4 powerful principles

  13. Abstraction

  14. Inheritance Abstraction

  15. Inheritance Encapsulation Abstraction

  16. Inheritance Encapsulation Abstraction Polymorphism

  17. Inheritance Encapsulation Abstraction Polymorphism

  18. Abstraction helps us handle complexity

  19. User

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

    string; public email: string; public website: string; }
  21. User

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

  25. SOLID

  26. High-level modules should not depend on low-level modules…

  27. Abstractions should not depend on details…

  28. User

  29. User Http

  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(); }); } }
  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(); }); } }
  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(); }); } }
  33. UserFinder Http User

  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; }); } }
  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
  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
  37. Layers of Abstraction

  38. UserFinder Http User

  39. UserFinder Http User

  40. UserFinder Http User

  41. UserFinder Http User

  42. UserFinder Http User Cache users in localStorage

  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
  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
  45. UserFinder Http User Cache requests in memory

  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; }); } } }
  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; }); } } }
  48. Caching crosscut levels of abstraction

  49. Cross-cutting concerns

  50. What part of the method is not related to the

    class purpose?
  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
  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
  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; }); } }
  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; }); } }
  55. Cannot be cleanly decomposed and can result in either scattering

    or tangling
  56. Object-oriented programming is not powerful enough

  57. None
  58. Aspect-oriented programming

  59. What if we could…

  60. Move the crosscuts into separate modules

  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
  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
  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
  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; }); } }
  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; }); } }
  66. http.ts export class Http { // ... get(url:string) { return

    fetch(url) .then(res => { return res.json(); }); } }
  67. …move the entire caching logic into separate module

  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) { } }
  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) { } }
  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) { } }
  71. How to connect everything together?

  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) { } }
  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) { } }
  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) { } }
  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) { } }
  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) { } }
  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) { } }
  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) { } }
  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) { } }
  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; }); } }
  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; }); } }
  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; }); } }
  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; }); } }
  84. None
  85. aspect.js

  86. Lets reveal the magic…

  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
  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
  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
  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
  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
  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
  93. Proxy-based AOP

  94. What about AngularJS 1.x?

  95. None
  96. None
  97. – Ryan Singer “So much complexity in software comes from

    trying to make one thing do two things.”
  98. Switching to Angular 2

  99. Thank you! github.com/mgechev twitter.com/mgechev blog.mgechev.com