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
}
...
}
¿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
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:
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!
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***!
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.
We're skipping some technical concerns...
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"
DemoApp.config(function ($provide, executeProvider) {
executeProvider.annotate($provide, {
ArticlesCollection: [{
jointPoint: 'before', //beforeAsync, onRejectOf, around ...
advice: 'Logger', //angular factory fn
}, {
//aspect 2
}, {
//aspect n ..
}]
});
});
// 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";
}]
})
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));
}
);
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(){
...
}
}
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 :(
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.
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
})
})
Some folks say that..
Happy coding!