Aspect Oriented Programming in JS

by @k1r0s feat Party Parrot

Raw Object oriented programming


class Invoice {
    ...
    applyTaxes(productPrice){
        console.log(`'applyTaxes' - arguments: ${productPrice}`)
        result = productPrice * selectedCountry.tax
        console.log(`'applyTaxes' - return value: ${result}`)
        return result
    }
    ...
}
                    

OOP with Aspect oriented programming


class Invoice {
    ...
    @log
    applyTaxes(productPrice){
        return productPrice * selectedCountry.tax
    }
    ...
}
                    

mm... Okay, wut...

¿What is AOP?

AOP is a paradigm which aims to increase modularity by allowing the separation of cross-cutting concerns


AOP is about abstraction of your infrastructure code by solving common problems with common solutions

AOP is not ...

  • not language dependent
  • not a technology
  • not a patern
  • not very common in frontend environments.. why? :'(
  • not a solution for all problems, neither almost every problem

AOP is ...

  • a technique
  • framework agnostic
  • a toolkit against repetition
  • hard to master
  • an extension for OOP


Some frontend frameworks use it.., but does not allow you to use as you see fit!

let's jump into the code again!!

It looks nice! so simple!


class Invoice {
    ...
    doCheckout(shippingDetails){
        this.shippingDetails = shippingDetails
    }
    ...
}
                        

But now we're used to save customer's invoice using any ajax service...


class Invoice {
    ...
    doCheckout(shippingDetails){
        this.shippingDetails = shippingDetails
        $OurAjaxService.post({ //if we're using IoC (Dependency Injection)
            url: this.url,
            data: this
        }).then((response) => {
            //...notify the user or something, trigger the state
        })
    }
    ...
}
                        

And then, we need to validate customer's 'shippingDetails'


class Invoice {
    ...
    doCheckout(shippingDetails){
        if(this.validate(shippingDetails)){
            this.shippingDetails = shippingDetails
            $OurAjaxService.post({ //if we're using IoC (Dependency Injection)
                url: this.url,
                data: this
            }).then((response) => {
                //...notify the user or something, trigger the state
            })
        }
    }
    ...
}
                        

Let's say that our ajax service does not include JSON serialization


class Invoice {
    ...
    doCheckout(shippingDetails){
        if(this.validate(shippingDetails)){
            this.shippingDetails = shippingDetails
            var serializedInstance = JSON.stringify(this)
            $OurAjaxService.post({ //if we're using IoC (Dependency Injection)
                url: this.url,
                data: serializedInstance
            }).then((response) => {
                //...notify the user or something, trigger the state
            })
        }
    }
    ...
}
                        

It's like our infrastucture code is hidding our business logic or what this method is really trying to do

These are corss-cutting concerns.


Manage all in the same place, that is AOP.

Corss-cutting concerns are often about:


  • Validation
  • Exception handling
  • Caching
  • Persistence
  • Security
  • Specific product features
  • logging ..RLY!?
  • DOM manipulation/events
  • Animations
  • Async code, AJAX, websockets and more..

So, don't repeat yourself


class Invoice {
    ...
    @before('validate')
    @after('serialize')
    @after('POST: "url"')
    @after('notify')
    doCheckout(shippingDetails){
        this.shippingDetails = shippingDetails
    }
    ...
}
                        

WTF! WHAT KIND OF SORCERY IS THIS?!1

HOW DO I DO THAT!?

hurry up, explain this or GTFO!

How we can implement this?

Step by step

Early example of AOP in vanilla JS:


function logBefore(fn){
    return function(){
        console.log("called " + fn.name + " with " + arguments);
        return fn.apply(this, arguments);
    }
}

var dummyObject = {
    ...
    doSomething: function (someArgument){
        var somethign = someArgument + this.someAttribute;
        ...

        return something;
    }
};

dummyObject.doSomething = logBefore(dummyObject.doSomething);
                        

Well, yeah... but my problems are quite harder than this piece of S***!

Calm down..!

First of all we need to understand the purpose of that technique!

Like DI does. For example..


require('reflect-metadata');
var di = require('injection-js');

var Http = di.Class({
  constructor: function () {}
});

var Service = di.Class({
  constructor: [Http, function (http) {
    this.http = http;
  }]
});

var injector = di.ReflectiveInjector.resolveAndCreate([Http, Service]);

console.log(injector.get(Service) instanceof Service);
                        

In a nutshell DI says "give me that dependency, I don't care how".

AOP is an extension of IoC technique, we need to understand briefly how it works.

Technical definition


  • Aspects
  • Joinpoints

We're skipping some technical concerns...

known AOP implementations in JS and Examples!

cujojs/meld


var myObject = {
    doSomething: function(a, b) {
        return a + b;
    }
};

// Call a function after myObject.doSomething returns
meld.after(myObject, 'doSomething', function(result) {
    console.log('myObject.doSomething returned: ' + result);
});

myObject.doSomething(1, 2); // Logs: "myObject.doSomething returned: 3"
                        

mgechev/angular-aop


DemoApp.config(function ($provide, executeProvider) {
  executeProvider.annotate($provide, {
    ArticlesCollection: [{
      jointPoint: 'before', //beforeAsync, onRejectOf, around ...
      advice: 'Logger', //angular factory fn
    }, {
      //aspect 2
    }, {
      //aspect n ..
    }]
  });
});
                        

k1r0s/kaop


// MyClass.prototype.myMethod.apply(this, args);

Aspects.push(
    Phase.EXECUTE,
    function override() {
        meta.args.unshift(meta.parentScope[meta.methodName].bind(this));
    }
);

FrontProgrammer = Class.inherits(Programmer, {
  //aspect 'override' inject the SuperClass.constructor as first parameter
  constructor: ["override", function(parent, name, dborn){
    parent(name, dborn) //like super() does
    this.favLang = "Javascript";
  }]
})
                        

Another example..

but our infrastructure code often is asynchronous :)


//KBase is an abstract dom element following webcomponents guidelines
KInclude = Class.inherits(KBase, {
    attachedCallback: ["override", function(parent) {
        parent();
        this.path = this.attr("path");
        this.fetchTemplate();
    }],
    fetchTemplate: ["$GET: 'path'", function(raw) {
        this.append(raw);
    }]
});
                        
(only in Chrome) <k-include path="inc/partial.html" />

Async aspect implementation with kaop, here


Aspects.push(
    Phase.EXECUTE,
    function $GET(key) {
        $AnyAjaxService.get(this[key]).then(function(raw) {
            meta.args.unshift(raw);
            next();
        });
    }
)
                        

An RxJS example dealing with DOM events


var ViewTest = Class({
    btnId: null,
    constructor: ["clickListener: 'handleClick'", function(btnId){
        this.btnId = btnId;
    }],
    handleClick: [function(evt) {
        ...
    }, "AsyncRequest", "AsyncRequest", ...]
});

new ViewTest('myBtn');
                        

This aspect is executed when a class is instantiated


//Define a service to retrieve click events
Aspects.locals.domObserver = rx.Observable.fromEvent(document, "click");

Aspects.push(
    //Like a joinPoint, this aspect will execute when a class is instanciated
    Phase.INSTANCE,
    function clickListener(handler) {
        domObserver
            .map(evt => evt.target)
            //get only html elements where id is equals to the first param of the constructor
            .filter(target => target.getAttribute("id") === meta.args[0])
            //finally attach a callback which is the instance method
            .subscribe(this[handler].bind(this));
    }
);
                        

known AOP implementations in TS/Babel and Examples!

Are ES7 Decorators an inflection point? Definitely yes!.

Raw AOP implementation using ES7 decorators proposal


function logBefore(target, key, descriptor){
    let originalMethod = descriptor.value
    descriptor.value = function(...args){
        console.log(`Log inside ${key} with params: ${args.join(", ")}`);
        return originalMethod.apply(this, args)
    }
    return descriptor
}

class Dummy {
    @logBefore
    doSomething(){
        ...
    }
}
                        

mgechev/aspect.js

An example Aspect definition by mgechev


class LoggerAspect {
  @beforeMethod({
    classNamePattern: /^Article/,
    methodNamePattern: /^(get|set)/
  })
  invokeBeforeMethod(meta: Metadata) {
    // meta.woveMetadata == { bar: 42 }
    // meta.ClassName, meta.MethodName, meta.args.. pretty handy
    console.log(`Inside of the logger. Called ... `);
  }
}
                        

@Wove merges the aspects with the class Definition


@Wove({ bar: 42 })
class ArticleCollection {
  articles: Article[] = [];
  getArticle(id: number) { //matches jointpoint
    console.log(`Getting article with id: ${id}.`);
    return this.articles.filter(a => {
      return a.id === id;
    }).pop();
  }
  setArticle(article: Article) { //matches jointpoint
    console.log(`Setting article with id: ${article.id}.`);
    this.articles.push(article);
  }
}
                        

An advice executed after async call (returning a promise)


class LoggerAspect {
  @afterMethod({
    classNamePattern: /^Article/,
    methodNamePattern: /^(get|set)/
  })
  invokeAfterMethod(meta: Metadata) {
    meta.result.then(() => {
        ...stuff
    })
  }
}

@Wove()
class ArticleCollection {
  articles: Article[] = [];
  getArticle(id: number) {
    return asyncOperation("foo")
  }
}

                        

logs, logs.. logs make me cry :(

k1r0s/kaop-ts

Building a forum API


@kaop.instance("Resource: 'uri'")
class Controller {
    uri: string
    constructor(uri){
        this.uri = uri
    }
    GET(res, req){
        //database logic
    }
    /* POST, PUT, DELETE */
}

new Controller("/user")
new Controller("/message")
new Controller("/thread")
new Controller("/topic")
                            


Aspects.push(
    function Resource(attributeUri){
        NodejsHttpServer.on("request", (req, res) => {
            if(req.url === this[attributeUri]){
                this[req.method].call(this, res, req)
            }
        })
    }
)
                        

so.. a request to [GET] ~/user/?name=JimmyJazz ..


@kaop.instance("Resource: 'uri'")
class Controller {
    uri: string
    constructor(uri){
        this.uri = uri
    }
    @kaop.before("Query") //dealing with database
    GET(res, req, queryResult){

        res.json(queryResult)
    }
    /* POST, PUT, DELETE */
}
                        

Aspects.push(
    function Query(){
        // this         instance            ...
        // this.uri     instance property   all property or methods
        // meta.key     method name         where this aspect is called from
        // meta.scope   instance alias      ...
        // meta.proto   instance prototype  ... not so useful
        // meta.args    arguments           req info {query params...}
        // meta.result  returned value      mostly 'after' stack

        mysqlService.query(meta.key, { uri: this.uri, where: meta.args[0].query }).then((result) => {
            meta.args.push(result)
            next()
        })
    }
)
                        

Aspects.push(
    function Auth(){
        var req = meta.args[0], res = meta.args[1]
        try {
            if(!req.headers.token) throw new Error
            //somehow perform a query which returns a user if token is valid
            mysqlService.query("GET", { ... }).then((err, result) => {
                if (result.length) {
                    meta.session = result.pop() //asign to a variable which can be accessed
                    next() //delegate into the next fn
                } else { throw err || new Error }
            })
        } catch() {
            res.end("You are not allowed to parrot!", 401)
        }
    }
)
                        

So pretty..


@kaop.instance("Resource: 'uri'")
class Controller {
    uri: string
    constructor(uri){
        this.uri = uri
    }

    @kaop.before("Auth")
    @kaop.before("Query")
    GET(res, req, queryResult){

        res.json(queryResult)
    }
    /* POST, PUT, DELETE */
}
                        

Maybe this example was not useful in terms of real problems, but self descriptive about its capabilities.

Testing

You have to test functions. We need to mock 'meta', 'next' and 'this'.

Simple test


describe("testing Query aspect", () => {
    it(...)
    it(...)
    it("It should retrieve JimmiJazz from /user/?name=JimmyJazz", (done) => {

        let meta = { //Mock the request as is
            args: [{query: {name: 'JimmyJazz'}}],
            key: "GET"
        }

        let next = done
        let self = {uri: '/user'}

        Query.call(self) //somehow invoke the aspect

        meta.args.pop() //should be Jimmy
    })
})
                        

Criticism

Some folks say that..

  • Debugging
  • Magic
  • Performance
  • Hard to master
  • 'duplication is better than bad abstraction' :\

Thank u so much · Muchas gracias :)

Happy coding!