編寫高質量代碼:改善Java程式的151個建議(第7章:泛型和反射___建議106~109)

来源:http://www.cnblogs.com/selene/archive/2016/10/10/5941809.html
-Advertisement-
Play Games

建議106:動態代理可以使代理模式更加靈活 Java的反射框架提供了動態代理(Dynamic Proxy)機制,允許在運行期對目標類生成代理,避免重覆開發。我們知道一個靜態代理是通過主題角色(Proxy)和具體主題角色(Real Subject)共同實現主題角色(Subject)的邏輯的,只是代理角 ...


建議106:動態代理可以使代理模式更加靈活

  Java的反射框架提供了動態代理(Dynamic Proxy)機制,允許在運行期對目標類生成代理,避免重覆開發。我們知道一個靜態代理是通過主題角色(Proxy)和具體主題角色(Real Subject)共同實現主題角色(Subject)的邏輯的,只是代理角色把相關的執行邏輯委托給了具體角色而已,一個簡單的靜態代理如下所示:

interface Subject {
    // 定義一個方法
    public void request();
}

// 具體主題角色
class RealSubject implements Subject {
    // 實現方法
    @Override
    public void request() {
        // 實現具體業務邏輯
    }

}

class Proxy implements Subject {
    // 要代理那個實現類
    private Subject subject = null;

    // 預設被代理者
    public Proxy() {
        subject = new RealSubject();
    }

    // 通過構造函數傳遞被代理者
    public Proxy(Subject _subject) {
        subject = _subject;
    }

    @Override
    public void request() {
        before();
        subject.request();
        after();
    }

    // 預處理
    private void after() {
        // doSomething
    }

    // 善後處理
    private void before() {
        // doSomething
    }
}

  這是一個簡單的靜態代理。Java還提供了java.lang.reflect.Proxy用於實現動態代理:只要提供一個抽象主題角色和具體主題角色,就可以動態實現其邏輯的,其實例代碼如下:

interface Subject {
    // 定義一個方法
    public void request();
}

// 具體主題角色
class RealSubject implements Subject {
    // 實現方法
    @Override
    public void request() {
        // 實現具體業務邏輯
    }

}

class SubjectHandler implements InvocationHandler {
    // 被代理的對象
    private Subject subject;

    public SubjectHandler(Subject _subject) {
        subject = _subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // 預處理
        System.out.println("預處理...");
        //直接調用被代理的方法
        Object obj = method.invoke(subject, args);
        // 後處理
        System.out.println("後處理...");
        return obj;
    }

}

  註意這裡沒有代理主題角色,取而代之的是SubjectHandler 作為主要的邏輯委托處理,其中invoke方法是介面InvocationHandler定義必須實現的,它完成了對真實方法的調用。

  我們來詳細解釋一下InvocationHandler介面,動態代理是根據被代理的介面生成的所有方法的,也就是說給定一個或多個介面,動態代理會宣稱“我已經實現該介面下的所有方法了”,那大家想想看,動態代理是怎麼才能實現介面中的方法呢?在預設情況下所有方法的返回值都是空的,是的,雖然代理已經實現了它,但是沒有任何的邏輯含義,那怎麼辦?好辦,通過InvocationHandler介面的實現類來實現,所有的方法都是由該Handler進行處理的,即所有被代理的方法都由InvocationHandler接管實際的處理任務。

  我們開看看動態代理的場景,代碼如下: 

public static void main(String[] args) {
        //具體主題角色,也就是被代理類
        Subject subject = new RealSubject();
        //代理實例的處理Handler
        InvocationHandler handler =new SubjectHandler(subject);
        //當前載入器
        ClassLoader cl = subject.getClass().getClassLoader();
        //動態代理
        Subject proxy = (Subject) Proxy.newProxyInstance(cl,subject.getClass().getInterfaces(),handler);
        //執行具體主題角色方法
        proxy.request();
    }

  此時就實現了,不用顯式創建代理類即實現代理的功能,例如可以在被代理的角色執行前進行許可權判斷,或者執行後進行數據校驗。

  動態代理很容易實現通用的代理類,只要在InvocationHandler的invoke方法中讀取持久化的數據即可實現,而且還能實現動態切入的效果,這也是AOP(Aspect Oriented Programming)變成理念。

建議107:使用反射增加裝飾模式的普適性

  裝飾模式(Decorator Pattern)的定義是“動態的給一個對象添加一些額外的職責。就增加功能來說,裝飾模式相比於生成子類更為靈活”,不過,使用Java的動態代理也可以實現裝飾模式的效果,而且其靈活性、適應性都會更強。

  我們以卡通片《貓和老鼠》(Tom and Jerry)為例,看看如何包裝小Jerry讓它更強大。首先定義Jerry的類:老鼠(Rat類),代碼如下: 

interface Animal{
    public void doStuff();
}

class Rat implements Animal{
    @Override
    public void doStuff() {
        System.out.println("Jerry will play with Tom ......");
    }
    
}

  接下來,我們要給Jerry增加一些能力,比如飛行,鑽地等能力,當然使用繼承也很容易實現,但我們這裡只是臨時的為Rat類增加這些能力,使用裝飾模式更符合此處的場景,首先定義裝飾類,代碼如下:

//定義某種能力
interface Feature{
    //載入特性
    public void load();
}
//飛行能力
class FlyFeature implements Feature{

    @Override
    public void load() {
        System.out.println("增加一對翅膀...");
    }
}
//鑽地能力
class DigFeature implements Feature{
    @Override
    public void load() {
        System.out.println("增加鑽地能力...");
    }
    
}

  此處定義了兩種能力:一種是飛行,另一種是鑽地,我們如果把這兩種屬性賦予到Jerry身上,那就需要一個包裝動作類了,代碼如下: 

class DecorateAnimal implements Animal {
    // 被包裝的動物
    private Animal animal;
    // 使用哪一個包裝器
    private Class<? extends Feature> clz;

    public DecorateAnimal(Animal _animal, Class<? extends Feature> _clz) {
        animal = _animal;
        clz = _clz;
    }

    @Override
    public void doStuff() {
        InvocationHandler handler = new InvocationHandler() {
            // 具體包裝行為
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                Object obj = null;
                if (Modifier.isPublic(method.getModifiers())) {
                    obj = method.invoke(clz.newInstance(), args);
                }
                animal.doStuff();
                return obj;
            }
        };
        //當前載入器
        ClassLoader cl = getClass().getClassLoader();
        //動態代理,又handler決定如何包裝
        Feature proxy = (Feature) Proxy.newProxyInstance(cl, clz.getInterfaces(), handler);
        proxy.load();
    }

}

  註意看doStuff方法,一個裝飾類型必然是抽象構建(Component)的子類型,它必須實現doStuff方法,此處的doStuff方法委托給了動態代理執行,並且在動態代理的控制器Handler中還設置了決定裝飾方式和行為的條件(即代碼中InvocationHandler匿名類中的if判斷語句),當然,此處也可以通過讀取持久化數據的方式進行判斷,這樣就更加靈活了。

  抽象構建有了,裝飾類也有了,裝飾動作類也完成了,那我們就可以編寫客戶端進行調用了,代碼如下: 

public static void main(String[] args) {
        //定義Jerry這隻老鼠
        Animal jerry = new Rat();
        //為Jerry增加飛行能力
        jerry = new DecorateAnimal(jerry, FlyFeature.class);
        //jerry增加挖掘能力
        jerry = new DecorateAnimal(jerry, DigFeature.class);
        //Jerry開始戲弄毛了
        jerry.doStuff();
    }

  此類代碼只一個比較通用的裝飾模式,只需要定義被裝飾的類及裝飾類即可,裝飾行為由動態代理實現,實現了對裝飾類和被裝飾類的完全解耦,提供了系統的擴展性。

建議108:反射讓模板方法模式更強大

  模板方法模式(Template Method Pattern)的定義是:定義一個操作中的演算法骨架,將一些步驟延遲到子類中,使子類不改變一個演算法的結構即可重定義該演算法的某些特定步驟。簡單的說,就是父類定義抽象模板作為骨架,其中包括基本方法(是由子類實現的方法,並且在模板方法中被調用)和模板方法(實現對基本方法的調度,完成固定的邏輯),它是用了簡單的繼承和覆寫機制,我麽來看一個基本的例子。

  我們經常會開發一些測試或演示程式,期望系統在啟動時自動初始化,以方便測試或講解,一般的做法是寫一個SQL文件,在系統啟動前自動導入,不過,這樣不僅麻煩而且容易出錯,於是我們就手寫了一個自動初始化數據的框架:在系統(或容器)自動啟動時自行初始化數據。但問題是每個應用程式要初始化的內容我們並不知道,只能由實現者自行編寫,那我們就必須給作者預留介面,此時就得考慮使用模板方法模式了,代碼如下:

 1 public abstract class AbsPopulator {
 2     // 模板方法
 3     public final void dataInitialing() throws Exception {
 4         // 調用基本方法
 5         doInit();
 6     }
 7 
 8     // 基本方法
 9     protected abstract void doInit();
10 }

  這裡定義了一個抽象模板類AbsPopulator,它負責數據初始化,但是具體要初始化哪些數據則是由doInit方法決定的,這是一個抽象方法,子類必須實現,我們來看一個用戶表數據的載入:  

public class UserPopulator extends AbsPopulator{
    @Override
    protected void doInit() {
        //初始化用戶表,如創建、載入數據等
    }

}

  該系統在啟動時查找所有的AbsPopulator實現類,然後dataInitialing實現數據的初始化。那大家可能要想了,怎麼讓容器指導這個AbsPopulator類呢?很簡單,如果是使用Spring作為Ioc容器的項目,直接在dataInitialing方法上加上@PostConstruct註解,Spring容器啟動完畢後自動運行dataInitialing方法。具體大家看spring的相關知識,這裡不再贅述。

  現在問題是:初始化一張User表需要非常多的操作,比如先建表,然後篩選數據,之後插入,最後校驗,如果把這些都放入到一個doInit方法里會非常龐大(即使提煉出多個方法承擔不同的責任,代碼的可讀性依然很差),那該如何做呢?又或者doInit是沒有任何的也無意義的,是否可以起一個優雅而又動聽的名字呢?

  答案是我們可以使用反射增強模板方法模式,使模板方法實現對一批固定的規則的基本方法的調用。代碼是最好的交流語言,我們看看怎麼改造AbsPopulator類,代碼如下:

public abstract class AbsPopulator {
    // 模板方法
    public final void dataInitialing() throws Exception {
        // 獲得所有的public方法
        Method[] methods = getClass().getMethods();
        for (Method m : methods) {
            // 判斷是否是數據初始化方法
            if (isInitDataMethod(m)) {
                m.invoke(this);
            }
        }
    }

    // 判斷是否是數據初始化方法,基本方法鑒定器
    private boolean isInitDataMethod(Method m) {
        return m.getName().startsWith("init")// init開始
                && Modifier.isPublic(m.getModifiers())// 公開方法
                && m.getReturnType().equals(Void.TYPE)// 返回值是void
                && !m.isVarArgs()// 輸出參數為空
                && !Modifier.isAbstract(m.getModifiers());// 不能是抽象方法
    }
}

  在一般的模板方法模式中,抽象模板(這裡是AbsPopulator類)需要定義一系列的基本方法,一般都是protected訪問級別的,並且是抽象方法,這標志著子類必須實現這些基本方法,這對子類來說既是一個約束也是一個負擔。但是使用了反射後,不需要定義任何抽象方法,只需要定義一個基本方法鑒定器(例子中的isInitDataMethod)即可載入符合規則的基本方法。鑒別器在此處的作用是鑒別子類方法中哪些是基本方法,模板方法(例子中的dataInitaling)則需要基本方法鑒定器返回的結果通過反射執行相應的方法。

  此時,如果需要進行大量的初始化工作,子類的實現就非常簡單了,代碼如下:

public class UserPopulator extends AbsPopulator {

    public void initUser() {
        /* 初始化用戶表,如創建、載入數據等 */
    }

    public void initPassword() {
        /* 初始化密碼 */
    }

    public void initJobs() {
        /* 初始化工作任務 */
    }
}

  UserPopulator類中的方法只要符合基本方法鑒別器條件即會被模板方法調用,方法的數據量也不再受父類的約束,實現了子類靈活定義基本方法、父類批量調用的功能,並且縮減了子類的代碼量。

  如果大家熟悉JUnit的話,就會看出此處的實現與JUnit非常相似,JUnit4之前要求測試的方法名必須是以test開頭的,並且無返回值、無參數,而且是public修飾,其實現的原理與此非常類似,大家有興趣可以看看Junit的源碼。

建議109:不需要太多關註反射效率

  反射的效率是一個老生常談的問題,有"經驗" 的開發人員經常會使用這句話恐嚇新人:反射的效率是非常低的,不到萬不得已就不要使用。事實上,這句話前半句是對的,後半句是錯的。

  反射的效率相對於正常的代碼執行確實低很多,但它是一個非常有效的運行期工具類,只要代碼結構清晰、可讀性好那就先開發起來,等到進行性能測試時證明此處性能確實有問題再修改也不遲(一般情況下,反射並不是性能的終極殺手,而代碼結構混亂、可讀性差則可能會埋下性能隱患)。我們看這樣一個例子,在運行期獲得泛型類的泛型,代碼如下: 

class Utils {
    // 獲得一個泛型類的實際泛型類型
    public static <T> Class<T> getGenricClassType(Class clz) {
        Type type = clz.getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) type;
            Type[] types = pt.getActualTypeArguments();
            if (types.length > 0 && types[0] instanceof Class) {
                // 若有多個泛型參數,依據位置索引返回
                return (Class<T>) types[0];
            }
        }
        return (Class<T>) Object.class;
    }
}

  前面我們講過,Java泛型只存在於編譯器,那為什麼這個工具類可以取得運行期的泛型類型呢?那是因為該工具只支持繼承的泛型類,如果是在Java編譯時已經確定了泛型類的類型參數,那當然可以通過泛型類獲得了。例如有這樣一個泛型類: 

abstract class BaseDao<T>{
    //獲得T運行期的類型
    private Class<T> clz = Utils.getGenricClassType(getClass());
    //根據主鍵獲得一條記錄
    public void get(long id){
        session.get(clz,id);
    }
}
//操作user表
class UserDao extends BaseDao<String>{
    
}

  對於UserDao類,編譯器編譯時已經明確了其參數類型是String,因此可以通過反射的方式來獲取其類型,這也是getGenricClassType方法使用的場景。

  BaseDao和UserDao是ORM中的常客,BaseDao實現對資料庫的基本操作,比如增刪改查,而UserDao則是一個比較具體的資料庫操作,其作用是對User表進行操作,如果BaseDao能夠提供足夠多的基本方法,比如單表的增刪改查,哪些與UserDao類似的BaseDao子類就可以省卻大量的開發工作。但問題是持久層的session對象(這裡模擬的是Hibernate  Session)需要明確一個具體的類型才能操作,比如get查詢,需要獲得兩個參數:實體類類型(用於確定映射的數據表)和主鍵,主鍵好辦,問題是實體類類型怎麼獲得呢?

  子類進行傳遞?麻煩,而且也容易產生錯誤。

  讀取配置問題?可行,但效率不高。

  最好的辦法就是父類泛型化,子類明確泛型參數,然後通過反射讀取相應的類型即可,於是就有了我們代碼中clz變數:通過反射獲得泛型類型。如此實現後,UserDao可不用定義任何方法,繼承過來的父類操作方法已經滿足基本需求了,這樣的代碼結構清晰,可讀性又好。

  想想看,如果考慮反射效率問題,沒有clz變數,不使用反射,每個BaseDao的子類都要實現一個查詢操作,代碼將會大量重覆,違反了"  Don't  Repeat Yourself " 這條最基本的編碼規則,這會致使項目重構、優化難度加大,代碼的複雜度也會提高很多。

      對於反射效率的問題,不要做任何的提前優化和預期,這基本上是杞人憂天,很少有項目是因為反射問題引起系統效率故障的(除非是拷貝的垃圾代碼),而且根據二八原則,80%的性能消耗在20%的代碼上,這20%的代碼才是我們關註的重點,不要單單把反射作為重點關註對象。

  註意:反射效率低是個真命題,但因為這一點而不使用它就是個假命題。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • TCP : Transmission Control Protocol,傳輸控制協議,類似於打電話 UDP : User Datagram Protocol,用戶數據報協議,類似於寫信 IP : Internet Protocol互聯網協議,是上述兩種協議的底層協議 IP地址(IP Address) ...
  • 模型: 1. 初始化並打開有名信號量:sem_open() 創建/獲得無名信號量:sem_init() 操作信號量:sem_wait()/sem_trywait()/sem_timedwait()/sem_post()/sem_getvalue() 退出有名信號量:sem_close() 銷毀有名信 ...
  • 模型: 1. 創建/獲取消息隊列fd :mq_open() 設置/獲取消息隊列屬性 :mq_get() 發送/接收消息 :mq_send()/mq_receive() 脫接消息隊列 :mq_close() 刪除消息隊列 :mq_unlink() 頭文件 POSIX mq VS Sys V mq的優勢 ...
  • 模型 1. 創建/獲取共用記憶體fd :shm_open() 2. 創建者調整文件大小 :ftruncate() 3. 映射fd到記憶體 :mmap() 4. 去映射fd :munmap() 5. 刪除共用記憶體 :shm_unlink() 頭文件 shm_open oflag Access Mode: ...
  • DNS簡單來說就是進行功能變數名稱和IP的轉換,那該如何轉換呢?既然要轉換,肯定有轉換表,那表應該存 哪個伺服器上,怎樣去請求功能變數名稱伺服器來進行轉換,所以,這個轉換的過程都是什麼。而面試的時 經常會有這道題:當在瀏覽器輸入網址按下回車之後,到瀏覽器回顯網頁,詳細描述一下中間發生了神馬? 一般來說,在windo ...
  • 模型 1. 獲取key ftok() 2. 創建/獲取信號量集 semget() 3. 初始化信號量集 semctl() 4. 操作信號量集 semop() 3. 刪除信號量集 semctl() 使用的頭文件: ftok() pathname :文件名 proj_id : 1~255的一個數,表示p ...
  • 剛開始學習python,首先要瞭解一下python解釋器。 什麼是python解釋器? 編寫python代碼保存後,我們會得到一個以.py為擴展名的文本文件。要運行此文件,就需要python解釋器去執行.py文件。這裡,我們介紹3種解釋器。 1、CPython 當我們從Python官方網站下載並安裝 ...
  • 本文章向碼農們介紹 php 給圖片加水印的兩種方法,感興趣的碼農可以參考一下本文章的源代碼。 方法一:PHP最簡單的加水印方法 方法二:php給圖片加文字水印 原文地址:http://www.manongjc.com/article/593.html ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...