视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Javascript中的AOP编程
2020-11-27 20:31:23 责编:小采
文档

 Duck punch

  我们先不谈AOP编程,先从duck punch编程谈起。

  如果你去wikipedia中查找duck punch,你查阅到的应该是monkey patch这个词条。根据解释,Monkey patch这个词来源于 guerrilla patch,意为在运行中悄悄的改变代码,而 guerrilla 这个词与 gorilla 同音,而后者意又与monkey相近(前者为“猩猩”的意思),最后就演变为了monkey patch。

  如果你没有听说过duck punch,但你或许听说过duck typing。举一个通俗的例子,如何辨别一只鸭子:

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

  没错,如果我发现有一类动物像鸭子一样叫,像鸭子一样游泳,那么它就是一只鸭子!

 这个检测看上去似乎有一些理所当然和无厘头,但却非常的实用。 并且在编程中可以用来解决一类问题——对于Javascript或者类似的动态语言,如何实现“接口”或者“基类”呢?我们可以完全不用在乎它们的过去如何,我们只关系在使用它们的时候,方法的类型或者参数是否是我们需要的:

var quack = someObject.quack;
 
if (typeof quack == "function" && quck.length == arguLength)
{
 // This thing can quack
}

扯远了,其实我想表达的是duck punch其实是由duck typing演化而来的:

if it walks like a duck and talks like a duck, it’s a duck, right? So if this duck is not giving you the noise that you want, you’ve got to just punch that duck until it returns what you expect.

  当你想一只鸭子发出驴的叫声怎么办,揍到它发出驴的叫声为止……话说这让我想到一个非常形象的笑话:

为了测试美国、、中国三地的实力, 联合国将三只兔子放在三个森林中,看三地谁先找出兔子。任务:找出兔子。 (中间省略……) 最后是某国,只有四个,先打了一天麻将,黄昏时一人拿一警棍进入森林,没五分钟,听到森林里传来一阵动物的惨叫,某国一人抽着一根烟有说有笑的出来,后面拖着一只鼻青脸肿的熊,熊奄奄一息的说到:“不要再打了,我就是兔子……”

  虽然duck punch有些暴力,但不失为一个有效的方法。落实到代码上来说就是让原有的代码兼容我们需要的功能。比如Paul Irish博客上的这个例子:

/**
 我们都知道jQuery的`$.css`方法可以通过使用颜色的名称给元素进行颜色赋值。
 但jQuery内置的颜色并非是那么丰富,如果我们想添加我们自定义的颜色名称应该怎么办?比如我们想添加`Burnt Sienna`这个颜色
*/
 
(function($){
 
 // 把原方法暂存起来:
 var _oldcss = $.fn.css;
 
 // 重写原方法:
 $.fn.css = function(prop,value){
 
 // 把自定义的颜色写进分支判断里,特殊情况特殊处理
 if (/^background-?color$/i.test(prop) && value.toLowerCase() === 'burnt sienna') {
 return _oldcss.call(this,prop,'#EA7E5D');
 
 // 一般情况一般处理,调用原方法
 } else {
 return _oldcss.apply(this,arguments);
 }
 };
})(jQuery);
 
// 使用方法:
jQuery(document.body).css('backgroundColor','burnt sienna')

同时可以推倒出duck punch的模式不过如此:

(function($){
 
 var _old = $.fn.method;
 
 $.fn.method = function(arg1,arg2){
 
 if ( ... condition ... ) {
 return ....
 } else { // do the default
 return _old.apply(this,arguments);
 }
 };
})(jQuery);

但是这么做有一个问题:需要修改原方法。这违背了“开放-封闭”原则,本应对拓展开放,对修改关闭。怎么解决这个问题呢?使用AOP编程。

 AOP

  入门

  AOP全称为Aspect-oriented programming,很明显这是相对于Object-oriented programming而言。Aspect可以翻译为“切面”或者“侧面”,所以AOP也就是面向切面编程。

  怎么理解切面?

  在面向对象编程中,我们定义的类通常是领域模型,它的拥有的方法通常是和纯粹的业务逻辑相关。比如:

Class Person
{
 private int money;
 public void pay(int price)
 {
 this.money = this.money - price; 
 }
}

但通常实际情况会更复杂,比如我们需要在付款的pay方法中加入授权检测,或者用于统计的日志发送,甚至容错代码。于是代码会变成这样:

Class Person
{
 private int money
 public void pay(price)
 {
 try
 {
 if (checkAuthorize() == true) {
 this.money = this.money - price; 
 sendLog();
 }
 }
 catch (Exception e)
 {
 
 } 
 }
}

 更可怕的是,其他的方法中也要添加相似的代码,这样以来代码的可维护性和可读性便成了很大的问题。我们希望把这些零散但是公共的非业务代码收集起来,更友好的使用和管理他们,这便是切面编程。切面编程在避免修改远代码的基础上实现了代码的复用。就好比把不同的对象横向剖开,关注于内部方法改造。而面向对象编程更关注的是整体的架构设计。

  实现

  在上一节中介绍的duck punch与切面编程类似,都是在改造原方法的同时保证原方法功能。但就像结尾说的一样,直接修改原方法的模式有悖于面向对象最佳实践的原则。

  Javascript可以采用装饰者模式(给原对象添加额外的职责但避免修改原对象)实现AOP编程。注意在这里强调的是实现,我进一步想强调的是,切面编程只是一种思想,而装饰者模式只是实践这种思想的一种手段而已,比如在Java中又可以采用代理模式等。切面编程在Java中发挥的余地更多,也更标准,本想把Java的实现模式也搬来这篇文章中,但不才Java水平有限,对Java的实现不是非常理解。在这里就只展示Javascript的实现。

  AOP中有一些概念需要介绍一下,虽然我们不一定要严格执行

joint-point:原业务方法;

advice:拦截方式

point-cut:拦截方法

  关于这三个概念我们可以串起来可以这么理解:

  当我们使用AOP改造一个原业务方法(joint-point)时,比如加入日志发送功能(point-cut),我们要考虑在什么情况下(advice)发送日志,是在业务方法触发之前还是之后;还是在抛出异常的时候,还是由日志发送是否成功再决定是否执行业务方法。

  比如gihub上的meld这个开源项目,就是一个很典型的AOP类库,我们看看它的API:

// 假设我们有一个对象myObject, 并且该对象有一个doSomething方法:
 
var myObject = {
 doSomething: function(a, b) {
 return a + b;
 }
};
 
// 现在我们想拓展它,在执行那个方法之后打印出刚刚执行的结果:
 
var remover = meld.after(myObject, 'doSomething', function(result) {
 console.log('myObject.doSomething returned: ' + result);
});
 
// 试试执行看:
 
myObject.doSomething(1, 2); // Logs: "myObject.doSomething returned: 3"
 
// 这个时候我们想移除刚刚的修改:
 
remover.remove();

由此可以看出,AOP接口通常需要三个参数,被修改的对象,被修改对象的方法(joint-point),以及触发的时机(adivce),还有触发的动作(point-cut)。上面说了那么多的概念,现在可能要让各位失望了,Javascript的实现原理其实非常简单

function doAfter(target, method, afterFunc){
 var func = target[method];
 return function(){
 var res = func.apply(this, arguments);
 afterFunc.apply(this, arguments);
 return res; 
 };
}

当然,如果想看到更完备的解决方案和代码可以参考上面所说的meld项目

 结束语

  这一篇一定让你失望了,代码简单又寥寥无几。本篇主要在于介绍有关duck和AOP的这几类思想,我想编程的乐趣不仅仅在于落实在编码上,更在于整个架构的设计。提高代码的可维护性和可拓展性会比高深莫测的代码更重要。

  其实上面

 参考文献:

How to Fulfill Your Own Feature Request -or- Duck Punching With jQuery!

Duck Punching JavaScript - Metaprogramming with Prototype

Does JavaScript have the interface type (such as Java’s ‘interface’)?

AOP技术基础

下载本文
显示全文
专题