AOP和DI最通俗的讲解

前言

吹牛了哈!网上有关DI和AOP的讲解数不胜数,多如牛毛。但你们站的位置好像有点高,没有顾及像我这种菜鸟,我通过通常恶补以后,终于大体弄明白DI和AOP是什么?决定将本身的一些想法写一下。php

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

正文

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

DI是什么?

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

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

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表示小学生,二、中学生 三、大学生
    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表示小学生,二、中学生 三、大学生
    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依赖注入的其中一种方法——构造函数注入。依赖经过构造函数的参数传入,只需引入一个接口。springboot

AOP是什么?

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

好了,接着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表示小学生,二、中学生 三、大学生
    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表示小学生,二、中学生 三、大学生
    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是这样的。若是你尚未意识到解耦的重要性,那么尽管去写代码吧!终有一天你接手了一份如千丝万缕杂糅在一块儿的代码时,你便会清醒意识到。解耦,解耦,解耦!

相关文章
相关标签/搜索