空对象模式

January 1, 2019

空对象模式(Null Object Pattern),指通过特定的、没有负载有效数据的空对象,表示实例不存在的默认状态。可用在不能接受 null 的逻辑,或者利用空对象实现区别于实例为空的应用场景。

这是一般场景的用户模型,包含用户id和昵称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User(private val userId: String,
           private val roomId: String,
           private val nickname: String) {

    fun getUserId(): String {
        return userId
    }
    
    fun getRoomId(): String {
        return roomId
    }

    fun getRemark(): String {
        return nickname
    }
}

实现空对象的形式有很多,比较正式的实现方法是:子类继承父类并重写所有可见成员方法,通过抛出异常的方式,阻止代码调用空对象方法。这样做的目的是强调不要通过空对象获取无效数据。

这种方式在父类有很多可重写方法是会略显麻烦,可自行约束对空对象的使用达到相同目的,或不妨考虑使用动态代理,代理所有方法并抛出同一个提示。

1
2
3
4
5
6
7
8
9
10
11
class UserNull() : User("", "", "") {

    // 重写父类方法,禁止通过空对象实例获取无效数据
    override fun getUserId(): String {
        throw IllegalAccessException("Access to this method is denied.")
    }

    override fun getRemark(): String {
        throw IllegalAccessException("Access to this method is denied.")
    }
}

当然,父类可(根据实际)允许子类重写部分父类方法,以下是 User 方法使用 open 修饰允许重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
open class User(private val userId: String,
                private val roomId: String,
                private val nickname: String) {

    open fun getUserId(): String {
        return userId
    }

    open fun getRoomId(): String {
        return roomId
    }

    open fun getRemark(): String {
        return nickname
    }
   
    // 同时增加此方法判断实例是否为空对象
    fun isNullObject(): Boolean {
        return this == User.USER_NULL_OBJ
    }

    companion object {
        // 定义单例空对象,避免创建多余实例
        val USER_NULL_OBJ = UserNull()
    }
}

对于 RxJava 这种不能接受 null 对象的应用场景来说,使用空对象表达 null 最合适不过。查不到用户时使用 空对象 代替 null,既能利用 filter 提前结束操作,又能避免 fromCallable 返回 null 引起空指针异常堆栈跟踪的性能损耗。

1
2
3
4
5
Observable.fromCallable { UserDao.load(userId) ?: User.USER_NULL_OBJ }
        .filter { !it.isNullObject() }
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe { removeUser(it.userId) }