AOP和DI小讲

1,394 阅读9分钟

前言

吹牛了哈!网上有关DI和AOP的讲解数不胜数,多如牛毛。但大家站的位置好像有点高,没有顾及像我这种菜鸟,我经过一般恶补之后,终于大致弄明白DI和AOP是什么?决定将自己的一些想法写一下。

这篇文章应该不会是最通俗的讲解,但我能够保证的是我将站在初学者的角度讲解,毕竟我也是初学者,可能更能体会初学者的无奈。本文只会告诉大家DI和AOP是什么,不会涉及怎么去运用。

正文

DI和AOP是JAVA后端框架Spring的核心,要想搞懂Spring,这个东西不弄懂,即便spring用得再好也枉然。简单的说,这两者都是一种编程思想,类似于五大编程原则之类的,但这两个思想确实挺颠覆。多说一句,我不建议新手一开始就直接上手springboot,直接接触springboot会有各种疑问,很多东西都是云里雾里,还是得从spring上手,推荐一本spring书籍《spring实战》,这本书已经出到第五版了,真的是Java程序员居家旅行、必备之物。

DI是什么?

DI全名 Dependency Injection,中文译为依赖注入,它就是用来注入依赖的。先忘记这个,我们来看一个场景,现在我们打算用程序来记录学生的学习,学生有小学生、中学生和大学生,不同级的学生学习的课程自然也不一样。

传统的编我们很自然地想建立至少两个类来进行实现,一个是学生的Student类,一个是用来描述学习的Learning类。别跟我说什么一个类就可以解决问题只需要将学习行为当做Student类的一个方法的话。我们这里的学习是很复杂的行为,需要分很多科目,需要很多代码,这样子就不适合把所有代码都放在一个类,一个文件中了,毕竟在一个文件中写个上千行代码是很危险的事情(被接手的同事打)。不同年级的学习行为很不一样,这些我们也不应该放到一起,所以应该有PrimaryStudentLearning、MiddleStudentLearning、CollegeStudentLearning等类。代码实现如下:

package com.student.learning;

public class PrimaryLearning {
    public void learning() {
        System.out.println("小学生学习数学、语文、英语");
    }
}
package com.student.learning;

public class MiddleLearning {
    public void learning() {
        System.out.println("中学生学习物理、生物、化学");
    }
}
package com.student.learning;

public class CollegeLearning {
    public void learning() {
        System.out.println("大学生学习高数、毛概、线代");
    }
}
package com.student.learning;

public class Student {

    // 1表示小学生,2、中学生 3、大学生
    private int grade;

    public Student(int grade) {
        this.grade = grade;
    }

    public void doLearning() {
        if (grade == 1) {
            PrimaryLearning pl = new PrimaryLearning();
            pl.learning();
        } else if(grade ==2) {
            MiddleLearning ml = new MiddleLearning();
            ml.learning();
        } else {
            CollegeLearning cl = new CollegeLearning();
            cl.learning();
        }
    }
}

package com.student.learning;

public class Main {
    public static void main(String[] args) {
        Student student = new Student(2);
        student.doLearning();
    }
}

各个类职责分明,貌似没什么问题,但仔细一想Student类引入了三个学习的类,如果下一步来了一个统计运动时间的需求,是要继续增加吗?我们必须明白一个关联很多组件的组件,它的可维护性、可扩展性、健壮性都将变得极低,它会变得极难测试。“高内聚,低耦合”的组件设计思想前后端是通用的。新手如果不理解,那就记得:一个组件或类关联其它部分越少越好,但是又是不可能完全没有关联的,因为各部分只有协同才能完成整体功能。

好了!上面欣赏了一波传统的编程思路,接下来我们看看使用DI是怎样写的:

package com.student.learning;

public interface Learning {
    void learning();
}
package com.student.learning;

public class PrimaryLearning implements Learning{
    @Override
    public void learning() {
        System.out.println("小学生学习数学、语文、英语");
    }
}

package com.student.learning;

public class MiddleLearning implements Learning {
    @Override
    public void learning() {
        System.out.println("中学生学习物理、生物、化学");
    }
}
package com.student.learning;

public class CollegeLearning implements Learning {
    @Override
    public void learning() {
        System.out.println("大学生学习高数、毛概、线代");
    }

}
package com.student.learning;

public class Student {
    // 1表示小学生,2、中学生 3、大学生
    private int grade;
    private Learning l;

    public Student(int grade, Learning learning) {
        this.grade = grade;
        this.l = learning;
    }

    public void doLearning() {
        this.l.learning();
    }
}
package com.student.learning;


public class Main {
    public static void main(String[] args) {
        Learning learning = new CollegeLearning();
        Student student = new Student(2, learning);
        student.doLearning();
    }
}

可以看到我们先是定义了一个Learning的接口,接着另外三个learning类都实现了这个接口,然后在Student类中使用的时候,我们不再直接将另外三个类引入,而是通过Student引入一个Learning类型的实例,这样子你如果传入PrimaryLearning的实例,我便执行小学生的学习方法,如此达到了你传入什么实例我便执行什么方法,Student完成不需要知道你传入的是什么,将会做什么事,它只关注自身即可,有点关注点分离的意思。现在整个Student类只引入了一个接口,而接口只是一种规范,跟具体的业务无关,这极大地降低了代码的耦合。这便是spring依赖注入的其中一种方法——构造函数注入。依赖通过构造函数的参数传入,只需引入一个接口。

AOP是什么?

AOP全名Aspect Oriented Programming,中文为面向切面编程。我只听过面向对象编程,面向切面编程是什么鬼?这应该是大部分人的想法了。这个思想比DI给我冲击更大,它让你把遍布各处的业务提取出来形成可重用组件。啥?这句话什么意思?抱歉!我也不懂。

好了,接着DI那个例子,假如现在又有了一个新的需求,需要记录每个学生的学习时间。嗯!我有上中下三策。

下策:直接在Student中加入这个功能,在学习前记录一下当前时间,在学习完成后又记录一下当前时间,这两者相减得到学习时间。这样把不属于Student这个类的业务也放在了Student中,当只有几行代码时,问题似乎不大,但当这个时间统计业务代码量很大的时候,Student这个会变得极其复杂,再有一点,如果其它业务也需要记录时间的时候,难道我要每一个地方都要写一个。

中策:我会将时间统计业务抽离出来,搞一个可复用的组件。使用这个组件我可以完成时间记录功能。代码如下:

package com.student.learning;

import java.util.Date;

public class RecordingTime {
    private long startTime;
    private long endTime;

    public void recordStartTime() {
        startTime = new Date().getTime();
    }

    public void recordEndTime() {
        endTime = new Date().getTime();
        System.out.println("学习了"+ (endTime - startTime) +"ms");
    }

}
package com.student.learning;

public class Student {

    // 1表示小学生,2、中学生 3、大学生
    private int grade;
    private Learning l;

    public Student(int grade, Learning learning) {
        this.grade = grade;
        this.l = learning;
    }

    public void doLearning() {
        RecordingTime rt = new RecordingTime();
        rt.recordStartTime();
        try {
            Thread.currentThread().sleep(3000);
            this.l.learning();
            rt.recordEndTime();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面我将记录时间的代码抽离为一个RecordingTime类,但即便我将这个业务抽离为一个组件,我还是需要在Student中调用。这个记录学习时间的事情不应该是学生做才对,学生也不应该知道这件事情,毕竟大家都会摸鱼和划水。那有没有什么方法在学生不知道的情况下做了这件事情呢?

上策:利用AOP是完全可以在学生不知情的情况下完成这件事情。实现一个AOP库当然可以,奈何实力不够,但可以直接使用spring,下面看看spring是如何使用AOP达到业务完全分离的。直接亮代码:

public class Student {

    // 1表示小学生,2、中学生 3、大学生
    private int grade;
    private Learning l;

    public Student(int grade, Learning learning) {
        this.grade = grade;
        this.l = learning;
    }

    public void doLearning() {
        try {
            Thread.currentThread().sleep(3000);
            this.l.learning();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

student.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="Student" class="com.student.learning.Student">
        <constructor-arg value="3"/>
        <constructor-arg ref="CollegeStudent" />
    </bean>
    <bean id="CollegeStudent" class="com.student.learning.CollegeLearning"/>
    <bean id="PrimaryStudent" class="com.student.learning.CollegeLearning"/>
    <bean id="MiddleStudent" class="com.student.learning.CollegeLearning"/>
    <bean id="RecordingTime" class="com.student.learning.RecordingTime" />

    <aop:config>
        <aop:aspect ref="RecordingTime">
            <aop:pointcut id="learning" expression="execution(* *.doLearning(..))" />
            <aop:before method="recordStartTime" pointcut-ref="learning" />
            <aop:after method="recordEndTime" pointcut-ref="learning" />
        </aop:aspect>
    </aop:config>
</beans>
package com.student.learning;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("com/student/learning/student.xml");
        Student student = context.getBean(Student.class);
        student.doLearning();
    }
}
记录开始学习时间点:Thu Nov 28 17:00:36 CST 2019
大学生学习高数、毛概、线代
学习了3029ms

上面的Student类完全没有去调用RecordTime的方法,它对时间记录业务是无感知的。那究竟是怎样做到的呢?核心是student.xml配置文件,我们把Student、ColldgeLearning这些类都通过bean声明在配置文件中,每一个bean都有一个ID,这里我直接将类名当作ID。在bean声明之后,还声明了aop配置,这一块我们逐行解读:

 <bean id="RecordingTime" class="com.student.learning.RecordingTime" />
 <aop:config>  //声明一个aop配置
         // 声明一个aop切面,这个面引用RecordingTime bean,也可以这个切面的上下文就是RecordingTime
        <aop:aspect ref="RecordingTime">
            // 这里定义一个切面点,这个点的id是learning, expression关于这个切面点的描述,
            // execution(* *.doLearning(..)) 表示当切面点是doLearning方法执行的时候
            <aop:pointcut id="learning" expression="execution(* *.doLearning(..))" />
            // before表示在切面点及doLearning执行之前调用 RecordingTime的recordStartTime方法
            <aop:before method="recordStartTime" pointcut-ref="learning" />
            // before表示在切面点及doLearning执行之后调用 RecordingTime的recordEndTime方法
            <aop:after method="recordEndTime" pointcut-ref="learning" />
        </aop:aspect>
    </aop:config>

而在程序入口main函数中,通过ClassPathXmlApplicationContext直接加载解析这个文件,这样可以拿到所有声明的类,在需要的时候创建对应的实例,比如context.getBean(Student.class);就会创建一个Student的实例返回。通过xml配置文件声明bean和aop只是spring的一种配置方法,目前比较流行的是通过注解进行声明。

看到了这里,大家对于AOP是什么应该有一个大致的印象?我们可以将其看做第三方,一方和另外一方如果想没有关联,那么必须存在一个第三方来协调两者。spring对于我们的类就是一个第三方,一个类要想在和其它类没有关联的情况完成协作,那么必然需要第三方协调这两者。只不过spring的权利有点大,它全权负责了我们实例的创建、使用和管理。

总结

DI和AOP本质上都是一种用以降低各个组件耦合性的设计,以松散的耦合组织代码,提高各个组件的独立性,使各个组件更易于测试。编程走过了几十年的路,我认为AOP和DI是一个很大的突破,至少对于JAVA是这样的。如果你还没有意识到解耦的重要性,那么尽管去写代码吧!终有一天你接手了一份如千丝万缕杂糅在一起的代码时,你便会清醒意识到。解耦,解耦,解耦!