依赖注入和注解,为什么Java比你想象的更好

原文地址

http://objccn.io/issue-11-6/
https://www.objc.io/issues/11-android/dependency-injection-in-java/

我坦白:我喜欢Java。
我真的喜欢!

也许这并不会让你感到吃惊,因为我毕竟确实参与编著过一本满是Java代码的书。但事实上,当我开始编写Android应用的时候我并不是一个喜欢Java的人,而当我开始编写书虫编程指南的时候,我也很难称得上粉丝,甚至我们完成编写的时候,我也始终不能算是一名超级粉丝。这个事实其实让我自己都很吃惊!

我原本并非想抱怨什么,也并不非要深刻反思一番。但是下面列出的这些内容却是一直困扰我的问题:

  • Java 很冗长。没有任何简单的类似Blocks或者Lambda表达式的语法来执行回调(当然,Java8已经开始支持这一特性),所以,你必须编写非常多的模板代码来实现,有时甚至只是一个简单的接口。如果你需要一个对象来保存四个属性,你必须创建一个拥有四个命名字段的类。
  • Java 很死板。要编写清楚的Java程序,你通常要正确的指定需要捕获的异常类型,以及要接受的参数类型,还有仔细检查并确保你的引用非空,甚至还要导入你所使用的每一个类。另外在运行时虽然有一定的灵活性,但是和Object-C的runtime没有任何相似的地方,更不用说和Ruby或者Python相比了。

这是我眼中的Java,它的代码就像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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独有的特性。请思考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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中的依赖关系是非常严格的。所以任何时间我都像这样编写代码:

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

EmployeeDatabase类中我创建了一个严格的依赖,不仅如此,我是利用EmployeeDatabase类的特定方法getInstance()创建的严格依赖。而在其他语言里,我们可以使用swizzle或则monkey patch的方法来处理这样的事情。当然并不是说这样的方法有什么好处,但它至少存在实现的可能。但是在Java里是不可能的。

而我创建依赖的其他方式比这更加严格。就让我们来看看下面这行:

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

当使用关键字new时,我会采用与调用静态方法相同的方式,但有一点不同:调用new EmployeeDatabase()方法一定会返回给我们一个EmployeeDatabase类的实例。无论你如何努力,你都没有办法重写这个构造函数来让它返回一个mock的子类对象。

依赖注入

我们解决此类问题通常采用依赖注入技术。它并非Java独有的特性,但对于上述提到的问题,Java尤其需要这个特性。
依赖注入简单的说,就是接受合作的对象作为构造方法的参数而不是直接获取它们自身。所以Payroll类的实现会相应地变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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类:

1
new Payroll().getAfterTaxPay(employee);

但是,我现在必须这样写:

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

还有,任何时候如果我改变了Payroll的依赖,我都不得不修改使用了new Payroll的每一个地方。

而依赖注入器允许我不再编写用来明确提供依赖的代码。相反,我可以直接声明我的依赖对象,让工具来自动处理相应操作。有很多依赖注入的工具,下面我将用RoboGuice来举个例子。

为了这样做,我使用“注解”这一Java工具来描述代码。我们通过为构造函数添加简单的注解声明:

1
2
3
4
@Inject
public Payroll(EmployeeDatabase employeeDatabase) {
mEmployeeDatabase = employeeDatabase;
}

注解@Inject的含义是创建一个Payroll类的实例,执行它的构造方法,传递所有的参数值。而之后当我真的需要一个Payroll实例的时候,我会利用依赖注入器来帮我创建,就像这样:

1
2
3
4
Payroll payroll = RoboGuice.getInjector(getContext())
.getInstance(Payroll.class);

long afterTaxPay = payroll.getAfterTaxPay(employee);

一旦我们采用这种方式创建实例,就能使用注入器来设置足够令人满意的依赖。是否需要EmployeeDatabase是一个单例?是否需要一个可自定义的子类?所有这些都可以在同一个地方指定。

声明式Java的广阔世界

这是一种很容易使用的描述工具,但是很难比较在Java中是否使用依赖注入的根本差距。如果没有依赖注入器,重构和测试驱动开发会是一项艰苦的劳动。而使用它,这些工作则会毫不费力。对于一名Java开发者来说,唯一比依赖注入器更要重的就是一个优秀的IDE了。

不过,这只是广泛可能性中的第一点。对于Google之外的Android开发者来说,最令人兴奋的就是基于注解的API了。
举个例子,我们可以使用ButterKnife。通常情况下,我们会花费大量时间为Android的视图编写监听器,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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方法”这件事情,就像这样:

1
2
3
4
5
6
7
8
9
10
11
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会在运行时读取你的代码注解,而Dagger则是在编译时生成对应的代码。

这样会有一些重要的好处。它能在更早的时间发现注解中的语义错误。Dagger能够在编译时提醒你可能存在的循环依赖,但是RoboGuide不能。

而且这对提高性能也很有帮助。使用预先生成的代码可以减少启动时间,并在运行时避免读取注解。因为读取注解需要使用Java反射相关的API,这在Android设备上是很耗时的。

运行时进行注解处理的例子

我会通过展示一个如何定义和处理运行时注解的简单例子,来结束今天的内容。假设你是一个很没有耐心地人,并且厌倦了在你的Android程序中打出一个完整的静态限定长量,比如:

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

你可以使用一个运行时的注解来帮你做这些事情。首先,你要创建一个注解类:

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

这段代码声明了一个名为ServiceConstant的注解。而代码本身被@Retention@Target注解。@Retention表示注解将会停留的时间。在这里我们将它设置为运行时触发。如果我们想仅仅在编译时处理注解,可以将其设置为RetentionPolicy.SOURCE

另一个注解@Target,表示你放置注解的位置。当然有很多的数据类型可以选择。因为我们的注解仅需要对字段有效,所以只需要提供ElementType.FIELD的声明。

一旦定义了注解,我们接着就要写些代码来寻找并自动填充注解的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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());
}
}
}
}

最后,我们为代码增加注解,然后调用我们充满魔力的方法:

1
2
3
4
5
6
7
8
public class CrimeActivity {
@ServiceConstant
public static final String ACTION_VIEW_CRIME;

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

总结

这些就是我了解的全部内容。有太多与Java注解相关的部分。我不能保证这些能够立刻让你对Java的感受变得和我一样,但是我希望你能确实看到很多有趣的东西。虽然通常Java在表达性上还欠缺一些,但是在Java的工具包中有一些基本的构建模块,能够让高级开发人员可以构建更强大的工具,从而扩大整个社区的生产力。

如你你对此很感兴趣,并且打算深入了解这些,你会发现通过注解驱动代码生成的过程非常有趣。有时候并不是一定要真的阅读或者写出漂亮的代码,但是人们可以利用这些工具创造出漂亮的代码。假如你对于实际场景如何应用依赖注入的原理很感兴趣的话,ButterKnife的源码还是很简单的。