JavaScriptでアスペクト指向
JavaScriptでオブジェクトにアスペクトを適用するコードを書いてみた。
アスペクトは単なる関数として書きます。
JavaScriptでアスペクト指向やってもあんまりおいしいところはないと思ってたけどそれなりに使える。
Dateオブジェクトにアスペクトを適用するテストケース。
function test_Object_Aspect_around() {
var d1 = new Date("2006/09/16");
var status = null;
var aspect = function(invocation){
var oldValue = invocation.target.getFullYear();
var result = invocation.proceed();
status = (oldValue == invocation.target.getFullYear())?"nochange":"changed";
return result;
};
Object.Aspect.around(d1, "setFullYear", aspect);
d1.setFullYear(2006);
assertEquals("nochange", status);
assertEquals(2006, d1.getFullYear());
d1.setFullYear(2007);
assertEquals("changed", status);
assertEquals(2007, d1.getFullYear());
}
function test_Object_Aspect_after() {
var d1 = new Date("2006/09/16");
var log = null;
var aspect = function(invocation){
log = invocation.methodName+"("+ $A(invocation.arguments).join(",") +")";
};
Object.Aspect.after(d1, ["setFullYear","setMonth"], aspect);
d1.setFullYear(2006);
assertEquals("setFullYear(2006)", log);
assertEquals(2006, d1.getFullYear());
log = null;
d1.setMonth(3);
assertEquals("setMonth(3)", log);
assertEquals(3, d1.getMonth());
}
function test_Object_Aspect_before() {
var d1 = new Date("2006/09/16");
var log = null;
var aspect = function(invocation){
log = invocation.methodName+"("+ $A(invocation.arguments).join(",") +")";
if (invocation.methodName == "setMonth"){
log += " cancelled";
invocation.cancelled = true;
return;
}
};
Object.Aspect.before(d1, ["setFullYear","setMonth"], aspect);
d1.setFullYear(2006);
assertEquals("setFullYear(2006)", log);
assertEquals(2006, d1.getFullYear());
log = null;
d1.setMonth(3);
assertEquals("setMonth(3) cancelled", log);
assertEquals(8, d1.getMonth());
}以下実装コードです。
Object.Aspect = {
_around: function(target, methodName, aspect){
var method = target[methodName];
target[methodName] = function() {
var invocation = {
"target":this,
"method":method,
"methodName":methodName,
"arguments":arguments,
"proceed": function(){
return method.apply(target, this.arguments);
}
};
return aspect.apply(null, [invocation]);
};
},
_before: function(target, methodName, aspect){
var method = target[methodName];
target[methodName] = function() {
var invocation = {
"target":this,
"method":method,
"methodName":methodName,
"arguments":arguments,
"cancelled": false
};
aspect.apply(null, [invocation]);
return (invocation["cancelled"]) ?
invocation["result"] : method.apply(target, arguments);
};
},
_after: function(target, methodName, aspect){
var method = target[methodName];
target[methodName] = function() {
var invocation = {
"target":this,
"method":method,
"methodName":methodName,
"arguments":arguments
};
invocation["result"] = method.apply(target, arguments);
return aspect.apply(null, [invocation]);
};
},
_apply: function(func, target, methodNames, aspect){
methodNames = methodNames||this.getMethodNames(target);
methodNames = (methodNames.each)?methodNames:[methodNames];
methodNames.each(function(methodName){
func(target, methodName, aspect);
});
},
getMethodNames: function(target){
var result = [];
for(var attr in target) {
try{
var value = target[attr];
if (value.constructor == Function)
result.push(attr);
}catch(ex){
}
}
return result;
},
around: function(target, methodNames, aspect){
this._apply(this._around, target, methodNames, aspect);
},
before: function(target, methodNames, aspect){
this._apply(this._before, target, methodNames, aspect);
},
after: function(target, methodNames, aspect){
this._apply(this._after, target, methodNames, aspect);
}
}