前言
吹牛了哈!网上有关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是这样的。如果你还没有意识到解耦的重要性,那么尽管去写代码吧!终有一天你接手了一份如千丝万缕杂糅在一起的代码时,你便会清醒意识到。解耦,解耦,解耦!