面向切面编程

Q: 什么是食物?
A: 食物通常以碳水化合物、脂肪、蛋白质或水构成,能够借由进食或是饮用为人类或者生物提供营养或愉悦的物质。
   食物的来源可以是植物、动物或者其他界的生物,例如真菌,亦或发酵产品像是酒精。
   生物摄取食物后,被生物的细胞同化,提供能量,维持生命及刺激成长。 

前段时间讨论到Java中的回调函数时,提到了面向切面(Aspect Oriented Programming)编程。它是一种程序设计思维,常常与面向对象(Object Oriented Programming)编程相比较,但是也常常被单独拎出来讨论。

OOP的诞生源自于人类与生俱来的分类抽象能力,回想一下,读小学的你,是否就能轻而易举地分辨“零食”和“正餐”的区别呢?零食美味而充满诱惑,但是对于你来说昂贵,而且吃太多存在挨骂的风险,相对的,正餐则略显的无聊和平淡,但是在你饿了的时候往往能让你的肚子不会咕咕叫。

于是自然而然你就可以将它们的共同点抽象出来,比如它们可以被食用,但是它们的味道、价格、食用时间乃至“副作用”之类的都不太一样。它们中相同的属性,就可以分配给一个只对这些相同的属性敏感的父元素。而不同的部分,就单独分配给父元素下属的不同的子元素。这样一来,子元素就不光含有自己的属性,也拥有了父元素的所有属性。

所以现在名词解释对你来说,会不会变得更容易呢?

面向切面

现在你知道了,事物中对于某些相同属性的抽象,构成了面向对象的基石。

来写一个简单的OOP的例子

    // JavaScript ES6

    class Person {
        constructor(name) {
            this.name = name;
        }
        greeting() {
            console.log(`hello, my name is ${this.name}, `);
        }
    }

    class Engineer extends Person {
        constructor(name, level) {
            super(name);
            this.level = level;
        }
        // override
        greeting() {
            super.greeting();
            console.log(`a ${this.level} Engineer.`);
        }
    }

    class Architect extends Engineer {
        constructor(name, level) {
            super(name, level);
        }
        design(work) {
            console.log(`right now, i\`m ${work}.`);
        }
        playGames(game, platform) {
            console.log(`${this.name} is playing ${game} on ${platform} now.`);
        }
    }

    let ted = new Architect('Ted', 'Junior');
    ted.greeting();
    ted.design('painting some blueprints');

    >>> 
    hello, my name is Ted,
    a Junior Engineer.
    right now, i'm painting some blueprints.

侵入式切面

假设我们想要在对象 Person 的所有方法执行前,加入一段逻辑。我们有什么好办法呢?

代理函数是个很好的选择,我们在进行具体的业务调用时,直接调用一个代理函数就行了。


    function before(person, fn, ...args) {
        console.log('something running before');
        return fn.apply(person, args);
    }

函数 before,将我们要调用的函数包裹起来,然后我们只需要这样调用这个代理函数,就能在其中的某个部分中添加逻辑。

    before(ted, ted.design, 'painting some blueprints');

上面的全局函数,还可以直接添加进对象中,防止被不必要的类或者方法使用。


    Person.prototype.before = function (fn, ...args) {
        console.log('do sthing before');
        return fn.call(this, args);
    };

    ted.before(ted.design, 'painting some blueprints');

虽然这种形式大体上能解决我们的需求,但是这种写法,仍然破坏了对象原有的调用形式。


    // 原来的调用形式
    ted.greeting();
    ted.playingGames('DotA2', 'PC');
    ted.design('painting some blueprints');

    // 代理函数的调用形式
    ted.before(ted.greeting);
    ted.before(ted.playingGames, 'DotA2', 'PC');
    ted.before(ted.design, 'painting some blueprints');

清一色的 before 函数,不仅写起来头晕,而且当你要改动的时候,所有调用这个代理函数的地方都要进行改动。这并不是我们想要的。

非侵入式切面

代理函数帮我们执行了函数,虽然能按照我们的期望执行程序,但我们更希望它直接返回函数给我们,这样我们就无须费劲心思地去到处修改旧代码,同时往代码中添加了新的功能。


    function before(originTarget, fn) {
        return function (...args) {
            console.log('do sthing before');
            return fn.apply(originTarget, args);
        };
    }
    ted.playGames = before(ted, ted.playGames);

    // 正常调用
    ted.playGames('DotA2', 'PC');

以上代码还可以写成更加通用的写法


    function before (clazz, fn) {
        return function (...args) {
            console.log(`now time: ${new Date()}`);
            return clazz.prototype[fn].apply(this, args);
        }
    }
    ted.greeting = before(Person, 'greeting');

    // 正常调用
    ted.greeting();

程序中那些无法通过父元素所聚合的共同属性,但是又切实地影响到了程序的写作——这种时候,切面编程就能发挥出它的用处。它将业务中的相同的部分抽象出来,组成一个个可拆卸的业务组件。面向对象通过继承和多态的纵轴让代码松耦合,而面向切面则是在业务并行展开的水平线上让代码松耦合。

中间件

提起AOP,总免不了让人想到Java Spring框架中的监听器、拦截器、过滤器。它们是典型的AOP编程思想的结晶——一条正常的程序流,被几个中间件拦截检查。

这启发了我们,在程序设计上的一条新思路:程序暴露出一段执行流,并且给开发者一把剪刀。它们可以随意在允许的范围内剪开程序,并织入想要执行的内容。最后这一段执行流合重新合并起来,收入程序的深处。

简单模拟一个中间件设计


    // 切入点函数队列
    let fns = [];
    // 切点计数
    let fnCounter = 0;

    // 启动函数
    function main() {
        next();
    }

    // 执行下一个函数
    function next() {
        let fn = fns[fnCounter++];  // 取出函数数组里的下一个函数
        if (!fn) {    // 如果函数不存在,return
            return;
        }
        fn(next);   // 否则,执行下一个函数
    }

    // 将自定义的函数推入函数队列
    function processer(fn) {
        fns.push(fn);
    }

    function loginCheck(next) {
        if (...)
            next();
        else
            return;
    }

    function characterFilter(next) {
        // doing some character filtering
        next();
    }

    function mainService() {
        // 主业务
        console.log('some main services');
    }

    processer(characterFilter);
    processer(loginCheck);
    processer(mainService);
    main(); // 模拟程序启动

原文地址: https://code.evink.me/2018/07/post/Aspect-Oriented-Programming/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章