鍍金池/ 教程/ iOS/ 依賴注入和注解,為什么 Java 比你想象的要好
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

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

我坦白\: 我喜歡 Java。

我真的喜歡!

也許這并不會讓你感到吃驚,因?yàn)槲耶吘勾_實(shí)參與編著過一本滿是 Java 代碼的書。但是事實(shí)上,當(dāng)我開始編寫 Android 應(yīng)用的時(shí)候我并不是一個(gè)喜歡 Java 的人,而當(dāng)我開始編寫書蟲編程指南的時(shí)候,我也很難稱得上是粉絲,甚至當(dāng)我們完成編寫的時(shí)候,我也始終不能算是一名超級粉絲。這個(gè)事實(shí)其實(shí)讓我自己都很吃驚!

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

  • Java 很冗長。沒有任何簡短的類似 Blocks 或者 Lambda 表達(dá)式的語法來執(zhí)行回調(diào)(當(dāng)然,Java8已經(jīng)開始支持這一特性),所以你必須編寫非常多的模板代碼來實(shí)現(xiàn),有時(shí)甚至只是一個(gè)簡單的接口。如果你需要一個(gè)對象來保存四個(gè)屬性,你必須創(chuàng)建一個(gè)擁有四個(gè)命名字段的類。

  • Java 很死板。要編寫清楚的Java程序, 你通常要正確的指定需要捕獲的異常類型,以及要接受的參數(shù)類型,還有仔細(xì)檢查并確保你的引用非空,甚至還要導(dǎo)入你所使用的每一個(gè)類。另外在運(yùn)行時(shí)雖然有一定的靈活性,但是和 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);
        }
    }
}

我學(xué)習(xí)過并且會在工作中混合使用一些內(nèi)部類和接口。雖然編寫Java程序這并不是世界上最糟糕的事情,但是我還是希望Java能夠擁有其他語言的特點(diǎn)和靈活性。類似 “天啊,我多么希望這能更像 Java” 的感嘆從沒有出現(xiàn)過。

但是,我的想法改變了。

Java 獨(dú)有的特性

說來也奇怪,改變我想法的恰恰是 Java 獨(dú)有的特性。請思考下面的代碼:

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;
    }
}

這個(gè)類在 getAfterTaxPay() 方法中需要依賴一個(gè) EmployeeDatabase 對象。有很多種方式可以創(chuàng)建該對象,但在這個(gè)例子中, 我使用了單例模式,調(diào)用一個(gè)靜態(tài)的 getInstance 方法。

Java 中的依賴關(guān)系是非常嚴(yán)格的。所以任何時(shí)間我都像這樣編寫代碼:

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

EmployeeDatabase 類中我創(chuàng)建了一個(gè)嚴(yán)格依賴。不僅如此,我是利用EmployeeDatabase類的特定方法 getInstance() 創(chuàng)建的嚴(yán)格依賴。而在其他語言里,我也許可以使用 swizzle 或者 monkey patch 的方式來處理這樣的事情.當(dāng)然并不是說這樣的方法有什么好處,但它至少存在實(shí)現(xiàn)的可能。但是在 Java 里是不可能的。

而創(chuàng)建依賴的其他方式比這更加嚴(yán)格。就讓我們來看看下面這行:

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

當(dāng)使用關(guān)鍵字 new 時(shí),我會采用與調(diào)用靜態(tài)方法相同的方式,但有一點(diǎn)不同:調(diào)用 new EmployeeDatabase() 方法一定會返回給我們一個(gè) EmployeeDatabase 類的實(shí)例。無論你如何努力,你都沒有辦法重寫這個(gè)構(gòu)造函數(shù)來讓它返回一個(gè) mock 的子類對象。

依賴注入

我們解決此類問題通常采用依賴注入技術(shù)。它并非 Java 獨(dú)有的特性,但對于上述提到的問題,Java 尤其需要這個(gè)特性。

依賴注入簡單的說,就是接受合作對象作為構(gòu)造方法的參數(shù)而不是直接獲取它們自身。所以 Payroll 類的實(shí)現(xiàn)會相應(yīng)地變成這樣:

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 是一個(gè)單例?一個(gè)模擬出來的子類?還是一個(gè)上下文相關(guān)的實(shí)現(xiàn)? Payroll 類不再需要知道這些。

用聲明依賴進(jìn)行編程

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

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

現(xiàn)在我們通過構(gòu)造函數(shù)傳遞依賴,會導(dǎo)致我們的對象更加難以使用,同時(shí)也很難作出更改。在我使用依賴注入之前,我會像這樣使用 Payroll 類:

    new Payroll().getAfterTaxPay(employee);

但是,現(xiàn)在我必須這樣寫:

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

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

而依賴注入器允許我不再編寫用來明確提供依賴的代碼。相反,我可以直接聲明我的依賴對象,讓工具來自動(dòng)處理相應(yīng)操作。有很多依賴注入的工具,下面我將用 RoboGuice 來舉個(gè)例子。

為了這樣做,我使用“注解“這一 Java 工具來描述代碼。我們通過為構(gòu)造函數(shù)添加簡單的注解聲明:

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

注解 @Inject 的含義是“創(chuàng)建一個(gè) Payroll 類的實(shí)例,執(zhí)行它的構(gòu)造方法,傳遞所有的參數(shù)值。”而之后當(dāng)我真的需要一個(gè) Payroll 實(shí)例的時(shí)候,我會利用依賴注入器來幫我創(chuàng)建,就像這樣:

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

    long afterTaxPay = payroll.getAfterTaxPay(employee);

一旦我采用這種方式創(chuàng)建實(shí)例,就能使用注入器來設(shè)置足夠令人滿意的依賴。是否需要 EmployeeDatabase 是一個(gè)單例?是否需要一個(gè)可自定義的子類?所有這些都可以在同一個(gè)地方指定。

聲明式 Java 的廣闊世界

這是一種很容易使用的描述工具,但是很難比較在 Java 中是否使用依賴注入的根本差距。如果沒有依賴注入器,重構(gòu)和測試驅(qū)動(dòng)開發(fā)會是一項(xiàng)艱苦的勞動(dòng)。而使用它,這些工作則會毫不費(fèi)力。對于一名 Java 開發(fā)者來說,唯一比依賴注入器更重要的就是一個(gè)優(yōu)秀的 IDE 了。

不過,這只是廣泛可能性中的第一點(diǎn)。 對于 Google 之外的 Android 開發(fā)者來說,最令人興奮的就是基于注解的 API 了。

舉個(gè)例子,我們可以使用 ButtreKnife。通常情況下,我們會花費(fèi)大量的時(shí)間為 Android 的視圖對象編寫監(jiān)聽器,就像這樣:

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() {
    // 處理按鈕點(diǎn)擊
}

ButterKnife 允許我們只提供很少的代碼來描述“在 ID 為 R.id.ok_button 的視圖控件被點(diǎn)擊時(shí)調(diào)用 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() {
    // 處理按鈕點(diǎn)擊
}

我能繼續(xù)寫很多這樣的例子。有很多庫可以通過注解來實(shí)現(xiàn)序列化與反序列化 Json,在 savedInstanceState 方法內(nèi)部存儲字段,或者是生成 REST 網(wǎng)絡(luò)服務(wù)的接口代碼等操作。

編譯時(shí)和運(yùn)行時(shí)注解處理對比

盡管有些使用注解的工具會產(chǎn)生相似的效果,不過 Java 允許使用不同的方式實(shí)現(xiàn)。下面我用 RoboGuice 和 Dagger 來舉個(gè)例子。它們都是依賴注入器,也同樣都使用 @Inject 注解。但是 RoboGuice 會在運(yùn)行時(shí)讀取你的代碼注解,而 Dragger 則是在編譯時(shí)生成對應(yīng)的代碼。

這樣會有一些重要的好處。它能在更早的時(shí)間發(fā)現(xiàn)注解中的語義錯(cuò)誤。Dagger 能夠在編譯時(shí)提醒你可能存在的循環(huán)依賴,但是 RoboGuice 不能。

而且這對提高性能也很有幫助。使用預(yù)先生成的代碼可以減少啟動(dòng)時(shí)間,并在運(yùn)行時(shí)避免讀取注解。因?yàn)樽x取注解需要使用 Java 反射相關(guān)的 API,這在 Android 設(shè)備上是很耗時(shí)的。

運(yùn)行時(shí)進(jìn)行注解處理的例子

我會通過展示一個(gè)如何定義和處理運(yùn)行時(shí)注解的簡單例子,來結(jié)束今天的內(nèi)容。 假設(shè)你是一個(gè)很沒有耐心地人,并且厭倦了在你的 Android 程序中打出一個(gè)完整的靜態(tài)限定常量,比如:

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

你可以使用一個(gè)運(yùn)行時(shí)注解來幫你做這些事情。首先,你要?jiǎng)?chuàng)建一個(gè)注解類:

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

這段代碼聲明了一個(gè)名為 ServiceConstant 的注解。 而代碼本身被 @Retention、@Target 注解。@Retention 表示注解將會停留的時(shí)間。在這里我們將它設(shè)置為運(yùn)行時(shí)觸發(fā)。如果我們想僅僅在編譯時(shí)處理注解,可以將其設(shè)置為 RetentionPolicy.SOURCE。

另一個(gè)注解 @Target,表示你放置注解的位置。當(dāng)然有很多的數(shù)據(jù)類型可以選擇。因?yàn)槲覀兊淖⒔鈨H需要對字段有效,所以只需要提供 ElementType.FIELD 的聲明。

一旦定義了注解,我們接著就要寫些代碼來尋找并自動(dòng)填充帶注解的字段:

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());
            }
        }
    }
}

最后,我們?yōu)榇a增加注解,然后調(diào)用我們充滿魔力的方法:

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

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

總結(jié)

這些就是我了解的全部內(nèi)容。有太多與 Java 注解相關(guān)的部分。我不能保證所有這些能夠立刻讓你對 Java 的感受變得和我一樣,但是我希望你能確實(shí)看到很多有趣的東西。雖然通常 Java 在表達(dá)性上還欠缺一些,但是在 Java 的工具包中有一些基本的構(gòu)建模塊,能夠讓高級開發(fā)人員可以構(gòu)建更強(qiáng)大的工具,從而擴(kuò)大整個(gè)社區(qū)的生產(chǎn)力。

如果你對此很感興趣,并且打算深入了解這些,你會發(fā)現(xiàn)通過注解驅(qū)動(dòng)代碼生成的過程非常有趣。有時(shí)候并不一定要真的閱讀或者寫出漂亮的代碼,但是人們可以利用這些工具創(chuàng)造出漂亮的代碼。假如你對于實(shí)際場景如何應(yīng)用依賴注入的原理很感興趣的話,ButterKnife 的源碼還是相當(dāng)簡單的。

上一篇:從 UIKit 到 AppKit下一篇:截圖測試