第30条:最小化元素的可见性

当我们设计一个 api 时,有很多原因可以解释为什么我们希望它尽可能的精简,让我们列出最重要的原因:

更小的接口更容易学习和维护,比起那些能做十几件事的接口,只做少数几件事的接口更容易让我们去理解,当我们做出改变时,我们通常需要理解所有实现类,当可见的元素的越少时,需要维护和测试的成本就越少。

当我们想要进行更改时,公开一个新元素要比隐藏现有元素容易的多。所有公共可见的元素都是我们的公共 API 的一部分,它可以在外部使用,一个元素的可见时间越长,它的外部用途就越多。因此,更改这些元素就更加困难,因为它们将需要更新所有的用法。限制可见性将是一个更大的挑战。如果是这样,就需要知悉每种业务用法并提供替代方案。 提供一个替代方案并不轻松,特别是如果它是由另外一个开发人员实现的。了解现在业务的需求可能也很困难,如果是公共库,限制某些元素的可见性可能会让一些用户感到气愤。他们将需要调整他们的实现,还将面临这样的问题 —— 他们将需要在代码开发多年之后实现一个替代的解决方案。最好一开始就强制开发人员使用较小的API。

当表示这种状态的属性可以从外部更改时,类就无法控制自己的这个状态了。我们假设一个状态可以满足一个类的需要,当这个状态可以从外部更改时,这个类就不能保证它的行为了,因为它可能被不知道内部约定的开发者从外部更改了。看一下第二章的 ConterSet,我们正确地限制了 elementsAdded setter 的可见性,如果没有限制,有人可能会把它从外部更改为任何值,我们就无法相信这个值能否代表这个容器有多少个元素。注意 , 只有 setter 是私有的,这是一个非常有用的技巧:

class CounterSet<T>(
    private val innerSet: MutableSet<T> = mutableSetOf()
) : MutableSet<T> by innerSet {
    var elementsAdded: Int = 0
        private set
    
    override fun add(element: T): Boolean {
        elementsAdded++
        return innerSet.add(element)
    }
    
    override fun addAll(elements: Collection<T>): Boolean {
        elementsAdded += elements.size
        return innerSet.addAll(elements)
    }
}

对于许多情况,默认情况下在 Kotlin 中封装所有属性是非常有用的,因为我们总是可以限制具体访问器 (getter / setter)的可见性:

当我们有相互依赖的属性时,保护内部对象状态尤其重要,例如,下面 mutableLazy 的委托实现,我们期望如果 initialized 是 true,那么 value 就已经被初始化,类型是 T。无论我们做什么 initialized 的 setter 都不应该暴露,否则它不能被信任,否则会导致属性在 get 时会出现一个丑陋的异常。

class MutableLazyHolder<T>(val initializer: () -> T) {
    private var value: Any? = Any()
    private var initialized = false
    
    fun get(): T {
        if (!initialized) {
            value = initializer()
            initialized = true
        }
        return value as T
    }
    
    fun set(value: T) {
        this.value = value
        initialized = true
    }
}

当类的可见性受到限制时,跟踪类的变化是很容易的,这使得属性状态更容易理解。当我们处理并发性时,这一点尤其重要。状态变化是并行编程的一个问题,最好尽可能地控制和限制它。

使用可见性修饰符

为了实现一个表面上很小,而内部可能很复杂的接口,我们限制了元素的可见性。一般来说,如果没有理由让一个元素可见,我们宁愿将其隐藏。这就是为什么当如果没有很好的理由限制较少的可见性类型,那么一个好的实践就是让类和元素的可见性尽可能小。我们使用可见性修饰符来做到这一点。

对于类成员,我们可以使用以下4个可见性修饰符:

  • public(default) —— 对于看到能声明该类的客户端,被修饰的属性任何位置都可见,

  • private —— 仅在类内部可见

  • protected —— 仅在类内部和其子类中可见

  • internal —— 对于看到能声明该类的客户端,在模块内部任意位置可见

顶层元素有3个可见性修饰符:

  • public(default) —— 任何地方可见

  • private —— 只在同个文件中可见

  • internal —— 只在同个模块中可见

请注意,模块与包不同,在 Kotlin 中,它被定义为一组一起编译的 Kotlin 资源,这意味着它可以是:

  • 一个 Gradle 源码集

  • 一个 Maven 项目

  • 一个 Intellij IDEA 模块

  • 用一次 Ant 任务编译的一组文件

如果你的模块可能被其他模块使用,请更改你不想公开的元素的可见性,如果一个元素是为继承而设计的,并且只在一个类和子类中使用,那么将其设置为 protected 。如果只在同一个文件或类中使用元素,则将其设置为 private。Kotlin 支持这种约定,因为它会在 IDE 中提出建议:如果一个元素只在本文件使用,则需要将可见性限制为 private

此规则不该被应用于保存数据的类(数据模型类)中的属性,如果你的服务器返回一个带有年龄的用户,并且你要解析它,你就不需要将这个这些元素隐藏起来,它在那里的意义就是被使用,所以最好让它可见,如果你不需要它,那你得完全摆脱这个属性:

class User(
    val name: String,
    val surname: String,
    val age: Int
)

一个很大的限制是,当我们继承一个 API 时,我们不能通过覆盖它来限制成员的可见性,这是因为子类总是可以用作它的超类。这只是我们倾向选择组合而不是继承的另一个原因(第36条:优先使用组合而不是继承)。

总结

经验法则是:元素的可见性应该尽可能被限制。可见元素构成了公共 API,我们希望它尽可能的精简,因为:

  • 更小的接口更容易学习和维护

  • 当我们想要做出改变时,暴露比隐藏要容易的多

  • 当代表这个状态的属性可以从外部被改变时,一个类就不能对它自己的状态负责了

  • 当 API 的可见性受限制时,更容易跟踪 API 的变化

Last updated