面向切面编程

作者: EvinK | 来源:发表于2018-08-06 00:03 被阅读0次
    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/

    相关文章

      网友评论

        本文标题:面向切面编程

        本文链接:https://www.haomeiwen.com/subject/jegivftx.html