1. 程式人生 > >依賴注入和註解,為什麼 Java 比你想象的要好

依賴注入和註解,為什麼 Java 比你想象的要好

我坦白\: 我喜歡 Java。

我真的喜歡!

也許這並不會讓你感到吃驚,因為我畢竟確實參與編著過一本滿是 Java 程式碼的書。但是事實上,當我開始編寫 Android 應用的時候我並不是一個喜歡 Java 的人,而當我開始編寫書蟲程式設計指南的時候,我也很難稱得上是粉絲,甚至當我們完成編寫的時候,我也始終不能算是一名超級粉絲。這個事實其實讓我自己都很吃驚!

我原本並非想抱怨什麼,也並非要深刻反思一番。但是下面列出的這些內容卻是一直困擾我的問題:

  • Java 很冗長。沒有任何簡短的類似 Blocks 或者 Lambda 表示式的語法來執行回撥(當然,Java8已經開始支援這一特性),所以你必須編寫非常多的模板程式碼來實現,有時甚至只是一個簡單的介面。如果你需要一個物件來儲存四個屬性,你必須建立一個擁有四個命名欄位的類。

  • Java 很死板。要編寫清楚的Java程式, 你通常要正確的指定需要捕獲的異常型別,以及要接受的引數型別,還有仔細檢查並確保你的引用非空,甚至還要匯入你所使用的每一個類。另外在執行時雖然有一定的靈活性,但是和 Objective-C 的 runtime 沒有任何相似的地方,更不用說和 Ruby 或者 Python 相比了。

這是我眼中的Java,它的程式碼就像這樣:

public class NumberStack {
    List<Integer> mNumbers = new ArrayList<Integer>();

    public void pushNumber(int number) {
        mNumbers.add(number);
    }

    public Integer popNumber() {
        if (mNumber.size() == 0) {
            return null;
        } else {
            return mNumber.remove(mNumber.size() - 1);
        }
    }
}

我學習過並且會在工作中混合使用一些內部類和介面。雖然編寫Java程式這並不是世界上最糟糕的事情,但是我還是希望Java能夠擁有其他語言的特點和靈活性。類似 “天啊,我多麼希望這能更像 Java” 的感嘆從沒有出現過。

但是,我的想法改變了。

Java 獨有的特性

說來也奇怪,改變我想法的恰恰是 Java 獨有的特性。請思考下面的程式碼:

public class Payroll {
    ...

    public long getWithholding(long payInDollars) {
        ...
        return withholding;
   }

    public long getAfterTaxPay(Employee employee) {
        long basePay = EmployeeDatabase.getInstance()
           .getBasePay(employee);
        long withholding = getWithholding(basePay);

        return basePay - withholding;
    }
}

這個類在 getAfterTaxPay() 方法中需要依賴一個 EmployeeDatabase 物件。有很多種方式可以建立該物件,但在這個例子中, 我使用了單例模式,呼叫一個靜態的 getInstance 方法。

Java 中的依賴關係是非常嚴格的。所以任何時間我都像這樣編寫程式碼:

        long basePay = EmployeeDatabase.getInstance()
           .getBasePay(employee);

EmployeeDatabase 類中我建立了一個嚴格依賴。不僅如此,我是利用EmployeeDatabase類的特定方法 getInstance() 建立的嚴格依賴。而在其他語言裡,我也許可以使用 swizzle 或者 monkey patch 的方式來處理這樣的事情.當然並不是說這樣的方法有什麼好處,但它至少存在實現的可能。但是在 Java 裡是不可能的。

而建立依賴的其他方式比這更加嚴格。就讓我們來看看下面這行:

        long basePay = new EmployeeDatabase()
           .getBasePay(employee);

當使用關鍵字 new 時,我會採用與呼叫靜態方法相同的方式,但有一點不同:呼叫 new EmployeeDatabase() 方法一定會返回給我們一個 EmployeeDatabase 類的例項。無論你如何努力,你都沒有辦法重寫這個建構函式來讓它返回一個 mock 的子類物件。

依賴注入

我們解決此類問題通常採用依賴注入技術。它並非 Java 獨有的特性,但對於上述提到的問題,Java 尤其需要這個特性。

依賴注入簡單的說,就是接受合作物件作為構造方法的引數而不是直接獲取它們自身。所以 Payroll 類的實現會相應地變成這樣:

public class Payroll {
    ...

    EmployeeDatabase mEmployeeDatabase;

    public Payroll(EmployeeDatabase employeeDatabase) {
        mEmployeeDatabase = employeeDatabase;
    }

    public long getWithholding(long payInDollars) {
        ...
        return withholding;
   }

    public long getAfterTaxPay(Employee employee) {
        long basePay = mEmployeeDatabase.getBasePay(employee);
        long withholding = getWithholding(basePay);

        return basePay - withholding;
    }
}

EmployeeDatabase 是一個單例?一個模擬出來的子類?還是一個上下文相關的實現? Payroll 類不再需要知道這些。

用宣告依賴進行程式設計

上述這些僅僅介紹了我真正要講的內容——依賴注入器。

(旁白:我知道在真正開始討論前將這兩個問題講的比較深入是很奇怪的,但是我希望你們能夠容忍我這麼做。正確的理解 Java 比起其他語言要花費更多地時間。困難的事物往往都是這樣。)

現在我們通過建構函式傳遞依賴,會導致我們的物件更加難以使用,同時也很難作出更改。在我使用依賴注入之前,我會像這樣使用 Payroll 類:

    new Payroll().getAfterTaxPay(employee);

但是,現在我必須這樣寫:

    new Payroll(EmployeeDatabase.getInstance())
        .getAfterTaxPay(employee);

還有,任何時候如何我改變了 Payroll 的依賴, 我都不得不修改使用了 new Payroll 的每一個地方。

而依賴注入器允許我不再編寫用來明確提供依賴的程式碼。相反,我可以直接宣告我的依賴物件,讓工具來自動處理相應操作。有很多依賴注入的工具,下面我將用 RoboGuice 來舉個例子。

為了這樣做,我使用“註解“這一 Java 工具來描述程式碼。我們通過為建構函式新增簡單的註解宣告:

    @Inject
    public Payroll(EmployeeDatabase employeeDatabase) {
        mEmployeeDatabase = employeeDatabase;
    }

註解 @Inject 的含義是“建立一個 Payroll 類的例項,執行它的構造方法,傳遞所有的引數值。”而之後當我真的需要一個 Payroll 例項的時候,我會利用依賴注入器來幫我建立,就像這樣:

    Payroll payroll = RoboGuice.getInjector(getContext())
        .getInstance(Payroll.class);

    long afterTaxPay = payroll.getAfterTaxPay(employee);

一旦我採用這種方式建立例項,就能使用注入器來設定足夠令人滿意的依賴。是否需要 EmployeeDatabase 是一個單例?是否需要一個可自定義的子類?所有這些都可以在同一個地方指定。

宣告式 Java 的廣闊世界

這是一種很容易使用的描述工具,但是很難比較在 Java 中是否使用依賴注入的根本差距。如果沒有依賴注入器,重構和測試驅動開發會是一項艱苦的勞動。而使用它,這些工作則會毫不費力。對於一名 Java 開發者來說,唯一比依賴注入器更重要的就是一個優秀的 IDE 了。

不過,這只是廣泛可能性中的第一點。 對於 Google 之外的 Android 開發者來說,最令人興奮的就是基於註解的 API 了。

舉個例子,我們可以使用 ButtreKnife。通常情況下,我們會花費大量的時間為 Android 的檢視物件編寫監聽器,就像這樣:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_content);

    View okButton = findViewById(R.id.ok_button);
    okButton.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
            onOkButtonClicked();
        }
    });
}

public void onOkButtonClicked() {
    // 處理按鈕點選
}

ButterKnife 允許我們只提供很少的程式碼來描述“在 ID 為 R.id.ok_button 的檢視控制元件被點選時呼叫 onOkButtonClicked 方法”這件事情,就像這樣:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_content);

    ButterKnife.inject(this);
}

@OnClick(R.id.ok_button);
public void onOkButtonClicked() {
    // 處理按鈕點選
}

我能繼續寫很多這樣的例子。有很多庫可以通過註解來實現序列化與反序列化 Json,在 savedInstanceState 方法內部儲存欄位,或者是生成 REST 網路服務的介面程式碼等操作。

編譯時和執行時註解處理對比

儘管有些使用註解的工具會產生相似的效果,不過 Java 允許使用不同的方式實現。下面我用 RoboGuice 和 Dagger 來舉個例子。它們都是依賴注入器,也同樣都使用 @Inject 註解。但是 RoboGuice 會在執行時讀取你的程式碼註解,而 Dragger 則是在編譯時生成對應的程式碼。

這樣會有一些重要的好處。它能在更早的時間發現註解中的語義錯誤。Dagger 能夠在編譯時提醒你可能存在的迴圈依賴,但是 RoboGuice 不能。

而且這對提高效能也很有幫助。使用預先生成的程式碼可以減少啟動時間,並在執行時避免讀取註解。因為讀取註解需要使用 Java 反射相關的 API,這在 Android 裝置上是很耗時的。

執行時進行註解處理的例子

我會通過展示一個如何定義和處理執行時註解的簡單例子,來結束今天的內容。 假設你是一個很沒有耐心地人,並且厭倦了在你的 Android 程式中打出一個完整的靜態限定常量,比如:

public class CrimeActivity {
    public static final String ACTION_VIEW_CRIME = 
        “com.bignerdranch.android.criminalintent.CrimeActivity.ACTION_VIEW_CRIME”;
}

你可以使用一個執行時註解來幫你做這些事情。首先,你要建立一個註解類:

@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD })
public @interface ServiceConstant { }

這段程式碼聲明瞭一個名為 ServiceConstant 的註解。 而程式碼本身被 @Retention@Target 註解。@Retention 表示註解將會停留的時間。在這裡我們將它設定為執行時觸發。如果我們想僅僅在編譯時處理註解,可以將其設定為 RetentionPolicy.SOURCE

另一個註解 @Target,表示你放置註解的位置。當然有很多的資料型別可以選擇。因為我們的註解僅需要對欄位有效,所以只需要提供 ElementType.FIELD 的宣告。

一旦定義了註解,我們接著就要寫些程式碼來尋找並自動填充帶註解的欄位:

public static void populateConstants(Class<?> klass) {
    String packageName = klass.getPackage().getName();
    for (Field field : klass.getDeclaredFields()) {
        if (Modifier.isStatic(field.getModifiers()) && 
                field.isAnnotationPresent(ServiceConstant.class)) {
            String value = packageName + "." + field.getName();
            try {
                field.set(null, value);
                Log.i(TAG, "Setup service constant: " + value + "");
            } catch (IllegalAccessException iae) {
                Log.e(TAG, "Unable to setup constant for field " + 
                        field.getName() +
                        " in class " + klass.getName());
            }
        }
    }
}

最後,我們為程式碼增加註解,然後呼叫我們充滿魔力的方法:

public class CrimeActivity {
    @ServiceConstant
    public static final String ACTION_VIEW_CRIME;

    static {
        ServiceUtils.populateConstants(CrimeActivity.class);
}

總結

這些就是我瞭解的全部內容。有太多與 Java 註解相關的部分。我不能保證所有這些能夠立刻讓你對 Java 的感受變得和我一樣,但是我希望你能確實看到很多有趣的東西。雖然通常 Java 在表達性上還欠缺一些,但是在 Java 的工具包中有一些基本的構建模組,能夠讓高階開發人員可以構建更強大的工具,從而擴大整個社群的生產力。

如果你對此很感興趣,並且打算深入瞭解這些,你會發現通過註解驅動程式碼生成的過程非常有趣。有時候並不一定要真的閱讀或者寫出漂亮的程式碼,但是人們可以利用這些工具創造出漂亮的程式碼。假如你對於實際場景如何應用依賴注入的原理很感興趣的話,ButterKnife 的原始碼還是相當簡單的。