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是這樣的。若是你尚未意識到解耦的重要性,那麼儘管去寫代碼吧!終有一天你接手了一份如千絲萬縷雜糅在一塊兒的代碼時,你便會清醒意識到。解耦,解耦,解耦!

相關文章
相關標籤/搜索