关于契约编程
契约编程(Contract Programming),我个人理解就好像商业行为一样,买卖双方彼此订立好契约,相互遵守,互利共赢,如果有一方没有遵守契约,那就意味着买卖可能不成立。
这套逻辑同样也适用于开发,提供 API 的一方不但要完成功能,还要尽可能地写清楚使用条件(相当于契约),文档或注释是一种形式,但是我们无法判断使用者到底有没有遵守约定。
举个栗子,定义一个方法 setItemId
,参数为 String itemId
。
void setItemId(final String itemId)
- 1
那么,如何保证 Fail-safe 呢?当然最简单的方法就是在方法体内判断一下 itemId 是否为空。
void setItemId(final String itemId) {
if (TextUtils.isEmpty(itemId)) return;
....
}
- 1
- 2
- 3
- 4
这样一来,虽然保证了 setItemId
的安全性,但同时也带来了新问题,当参数不合法时,无法及时暴露问题,这样可能会给 Debug 带来一定困难。fresco 的做法比较直接,如果参数不合法,直接抛异常。
public void addControllerListener(ControllerListener<? super INFO> controllerListener) {
Preconditions.checkNotNull(controllerListener);
...
}
- 1
- 2
- 3
- 4
其中 Preconditions
是 guava 提供的一个类,用于判断变量是否符合前置条件,如果不符合则直接抛异常,及时地暴露问题。这也符合 infer 提倡的编码习惯:
Program safely, annotate nothing!
翻译成土话就是『安全编码,啥也不标』,如果没有任何标记,则意味着参数不能空,如果为空,不好意思,直接抛异常。但是如果可以接受参数为 null 时,怎样通知调用方呢?这时候就要用到 Annotations 了,这个我们下面再讲。
『安全编码,啥也不标』这种习惯比较适用于会写单元测试的开发者,但是像我这种喜欢偷懒,不写单测,还追求极致性能的码农来说,面对setItemId(final String itemId)
这样的API,很容易变得纠结,因为很想知道它到底资瓷不资瓷空值,如果支持,那我就不需要再判空了。幸好有Android Annotations
, 她让这件事变得比较简单。
Android Annotations
Annotations 可以帮助你写出更有意义的契约,它的表现力要大于注释和文档,而且 Android Studio 可以利用这些 Annotations 帮你检测出潜伏的bug。
国际惯例,使用之前,添加依赖包:
compile 'com.android.support:support-annotations:23.0.1
- 1
空值标记
NonNull
和 Nullable
是一对,通过源码的 @Target
可以看出,它们可以出现在方法声明、参数声明以及成员变量的声明。
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD})
public @interface Nullable {
}
- 1
- 2
- 3
- 4
下面这段代码,setItemId(null)
肯定会导致 Crash,IDE 也没有提示 bug 风险。
作为 setItemId
方法本身,为了保证安全,需要对参数 itemId
做一次判空处理:
public void setItemId(final String itemId) {
if (!TextUtils.isEmpty(itemId)) {
Toast.makeText(this, itemId, Toast.LENGTH_SHORT).show();
}
}
- 1
- 2
- 3
- 4
- 5
调用方并不知道方法内部做了判空处理,很有可能又做了一次判空操作。明明只需要一次判空就OK了,由于信息不对称,导致调用方和提供方各自进行了一次判空操作,造成性能开销。
但是,如果给参数 final String itemId
加上 NonNull
,方法提供方就可以放心大胆地不进行判空操作,因为已经通过 annotation 订立了使用”契约” -参数不能为空。
IED 也提示了 bug 风险。
Nullable
也就意味着调用方无需再进行判空操作,道理是一样的。
资源标记
当 API 参数是一个表示资源 ID 的 int 时,可以加上资源标记。
看一下 StringRes
的定义,支持的类型除了方法、参数、成员变量之外,甚至还支持 局部变量。
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface StringRes {
}
- 1
- 2
- 3
- 4
- 5
资源标记除了针对任何资源都是用的 @AnyRes
,还包括每种资源所对应的特有的标记。
@AnimatorRes
, @AnimRes
, @ArrayRes
, @AttrRes
, @BoolRes
, @ColorRes
, @DimenRes
, @DrawableRes
, @FractionRes
, @IdRes
, @IntegerRes
, @InterpolatorRes
,@LayoutRes
, @MenuRes
, @PluralsRes
, @RawRes
, @StringRes
, @StyleableRes
, @StyleRes
, @XmlRes
.
值标记
与资源标记类似,我们还可以使用 @ColorInt
来标记一个表示颜色值 int 变量。甚至还还可以通过 @FloatRang
和 @IntRange
来表示变量的取值范围。
Proguard
Fresco 中使用 @DoNotStrip
来防止混淆,并在 proguard 文件中添加了一条规则:
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.common.internal.DoNotStrip *;
}
- 1
- 2
- 3
- 4
- 5
现在 Android Annotations 也提供了一个类似的标记 @Keep
,与 @DoNotStrip
相比其优势在于可以不用添加 Proguard 规则,Android 的 Gradle 插件自动会帮我们完成这步操作(还在开发中)。
其他
出了以上列举的一些常用的之外,还有很多其他的很有用的 Annotation,例如 @CheckResults
、@CallSuper
等等,具体可以参考 Android 官方文档 -Improving Code Inspection with Annotations,
总结
Android Annotations 绝对是个好东西,不但能够提高代码可读性、而且会大大提高健壮性,希望大家编码时能充分利用起来,也欢迎分享心得。
参考文章
- https://medium.com/sebs-top-tips/annotations-to-support-your-contracts-609ff259d5df
- https://developer.android.com/tools/debugging/annotations.html
关于我
转自:一介码农