(译)Effective Kotlin系列之考虑使用静态工厂方法替代构造器(一)

5,097 阅读12分钟

翻译说明:

原标题: Effective Java in Kotlin, item 1: Consider static factory methods instead of constructors

原文地址: blog.kotlin-academy.com/effective-j…

原文作者: Marcin Moskala

由Joshua Bloch撰写的Effective Java这本书是Java开发中最重要的书之一。我经常引用它,这也就是为什么我经常被要求提及更多有关于它的原因。我也对它和Kotlin相关的一些内容非常感兴趣,这就是为什么我决定用Kotlin去一个一个去阐述它们,这是Kotlin学院的博客。只要我看到读者的兴趣,我就继续下去;)

这是Effective Java的第一条规则:

考虑使用静态工厂方法替代构造器

让我们一起来探索吧。

内容前情回顾

Effective Java的第一条规则就表明开发者应该更多考虑使用静态工厂方法来替代构造器。静态工厂方法是一个被用来创建一个对象实例的静态方法。这里有一些有关静态工厂方法使用的Java例子:

Boolean trueBoolean = Boolean.valueOf(true);
String number = String.valueOf(12);
List<Integer> list = Arrays.asList(1, 2, 4);

静态工厂方法是替代构造器一种非常高效的方法。这里列举了一些他们优点:

  • 与构造器不同的是,静态工厂方法他们有方法名. 方法名就表明了一个对象是怎么创建以及它的参数列表是什么。例如,正如你所看到的下列代码: new ArrayList(3).你能猜到3代表什么意思吗?它是应该被认为是数组的第一元素还是一个集合的size呢?这无疑不能做到一目了然。例如,ArrayList.withSize(3)这个拥有方法名场景就会消除所有疑惑。这是方法名非常有用的一种:它解释了对象创建的参数或特征方式。拥有方法名的另一个原因是它解决了具有相同参数类型的构造函数之间的冲突。
  • 与构造器不同的是,每次调用它们时无需创建新对象. 当我们使用静态工厂方法时,可以使用缓存机制去优化一个对象的创建,这种方式可以提升对象创建时性能。我们还可以定义这样的静态工厂方法,如果对象不能被创建就直接返回一个null,就像Connections.createOrNull()方法一样,当Connection对象由于某些原因不能被创建时就返回一个null.
  • 与构造器不同的是,他们可以返回其返回类型的任何子类的对象. 这个可以在不同的情况下被用来提供更灵活的对象。当我们想要去隐藏接口后面的真正对象时,静态工厂方法就显得尤为重要了。例如,在kotlin中所有的Collection都是被隐藏接口背后的。这点很重要是因为在不同平台的底层引擎下他们是不同的类。当我们调用listOf(1,2,3),如果是在Kotlin/JVM平台下运行就会返回一个ArrayList对象。相同的调用如果是在Kotlin/JS平台将会返回一个JavaScript的数组。这是一个优化的实现,并不是一个已存在的问题,因为两者的集合类都是实现了Kotlin中的List接口。listOf返回的类型是List,这是一个我们正在运行的接口。一般来说隐藏在底层引擎下的实际类型和我们并没有多大关系. 类似地,在任何静态工厂方法中,我们可以返回不同类型甚至更改类型的具体实现,只要它们隐藏在某些超类或接口后面,并且被指定为静态工厂方法返回类型即可。
  • 与构造器不同的是,他们可以减少创建参数化类型实例的冗长程度. 这是一个Java才会有的问题,Kotlin不存在该问题,因为Kotlin中有更好的类型推断。关键是当我们调用构造函数时,我们必须指定参数类型,即使它们非常明确了。然而在调用静态工厂方法时,则可以避免使用参数类型。

虽然以上那些都是支持静态工厂方法的使用非常有力的论据,但是Joshua Bloch也指出了一些有关静态工厂方法缺点:

  • 它们不能用于子类的构造. 在子类构造中,我们需要使用父类构造函数,而不能使用静态工厂方法。

  • 它们很难和其他静态方法区分开来. 除了以下情况:valueOf,of,getInstance,newInstance, getTypenewType.这些是不同类型的静态工厂方法的通用名称。

在以上论点讨论完后,得出的直观结论是,用于构造对象或者和对象结构紧密相关的对象构造的函数应该被指定为构造函数。另一方面,当构造与对象的结构没有直接关联时,则很有可能应该使用静态工厂方法来定义。

让我们来到Kotlin吧,当我在学习Kotlin的时候,我感觉有人正在设计它,同时在他面前有一本Effective Java。它解答了本书中描述的大多数Java问题。Kotlin还改变了工厂方法的实现方式。让我们一起来分析下吧。

伴生工厂方法

在Kotlin中不允许有static关键字修饰的方法,类似于Java中的静态工厂方法通常被称为伴生工厂方法,它是一个放在伴生对象中的工厂方法:

class MyList {
    //...
    companion object {
        fun of(vararg i: Int) { /*...*/ }
    }
}

用法与静态工厂方法的用法相同:

MyList.of(1,2,3,4)

在底层实现上,伴生对象实际上就是个单例类,它有个很大的优点就是Companion对象可以继承其他的类。这样我们就可以实现多个通用工厂方法并为它们提供不同的类。使用的常见例子是Provider类,我用它作为DI的替代品。我有下列这个类:

abstract class Provider<T> {
     var original: T? = null
     var mocked: T? = null
     abstract fun create(): T
     fun get(): T = mocked ?: original ?: create()
           .apply { original = this }
     fun lazyGet(): Lazy<T> = lazy { get() }
}

对于不同的元素,我只需要实现具体的创建函数即可:

interface UserRepository {
    fun getUser(): User
    companion object: Provider<UserRepository>() {
        override fun create() = UserRepositoryImpl()
    }
}

有了这样的定义,我可以通过UserReposiroty.get()方法获取repository实例对象,或者在代码的任何地方通过val user by UserRepository.lazyGet()这种懒加载方式获得相应的实例对象。我也可以为测试例子指定不同的实现或者通过UserRepository.mocked = object: UserRepository { /*...*/ }实现模拟测试的需求。

与Java相比,这是一个很大的优势,其中所有的SFM(静态工厂方法)都必须在每个对象中手动实现。此外通过使用接口委托来重用工厂方法的方式仍然被低估了。在上面的例子中我们可以使用这种方式:

interface Dependency<T> {
    var mocked: T?
    fun get(): T
    fun lazyGet(): Lazy<T> = lazy { get() }
}
abstract class Provider<T>(val init: ()->T): Dependency<T> {
    var original: T? = null
    override var mocked: T? = null
     
    override fun get(): T = mocked ?: original ?: init()
          .apply { original = this }
}
interface UserRepository {
    fun getUser(): User
    companion object: Dependency<UserRepository> by Provider({
        UserRepositoryImpl() 
    }) 
}

用法是相同的,但请注意,使用接口委托我们可以从单个伴生对象中的不同类获取工厂方法,并且我们只能获得接口中指定的功能(依据接口隔离原则的设计非常好)。了解更多有关接口委托

扩展工厂方法

请注意另一个优点就是考虑将工厂方法放在一个伴生对象里而不是被定义为一个静态方法: 我们可以为伴生对象定义扩展方法。因此如果我们想要把伴生工厂方法加入到被定义在外部类库的Kotlin类中,我们还是可以这样做的(只要它能定义任意的伴生对象)

interface Tool {
   companion object { … }
}
fun Tool.Companion.createBigTool(…) : BigTool { … }

或者,伴生对象被命名的情况:

interface Tool {
   companion object Factory { … }
}
fun Tool.Factory.createBigTool(…) : BigTool { … }

让我们从代码中共享外部库的使用这是一种很强大的可能,据我所知 Kotlin现在是唯一提供这种可能性的语言。

顶层函数

在Kotlin中,更多的是定义顶层函数而不是CFM(伴生对象工厂方法)。比如一些常见的例子listOf,setOf和mapOf,同样库设计者正在制定用于创建对象的顶级函数。它们将会被广泛使用。例如,在Android中,我们传统上定义一个函数来创建Activity Intent作为静态方法:

//java
class MainActivity extends Activity {
    static Intent getIntent(Context context) {
        return new Intent(context, MainActivity.class);
    }
}

在Kotlin的Anko库中,我们可以使用顶层函数intentFor加reified类型来替代:

intentFor<MainActivity>()

这种解决方案的问题在于,虽然公共的顶层函数随处可用,但是很容易让用户丢掉IDE的提示。这个更大的问题在于当有人创建顶级函数时,方法名不直接指明它不是方法。使用顶级函数创建对象是小型和常用对象创建方式的完美选择,比如List或者Map,因为listOf(1,2,3)List.of(1,2,3)更简单并且更具有可读性。但是公共的顶层函数需要被谨慎使用以及不能滥用。

伪构造器

Kotlin中的构造函数与顶级函数的工作方式类似:

class A()
val a = A()

它们也可以和顶层函数一样被引用:

val aReference = ::A

类构造函数和函数之间唯一的区别是函数名不是以大写开头的。虽然技术上允许,但是这个事实已经适用于Kotlin的很多不同地方其中包括Kotlin标准库在内。ListMutableList都是接口,但是它们没有构造器,但是Kotlin开发者希望允许以下List的构造:

List(3) { "$it" } // same as listOf("0", "1", "2")

这就是为什么在Collections.kt中就包含以下函数(自Kotlin 1.1起):

public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)

public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
    val list = ArrayList<T>(size)
    repeat(size) { index -> list.add(init(index)) }
    return list
}

它们看起来很像构造器,很多开发人员都没有意识到它们是底层实现的顶层函数。同时,它们具有SFM(静态工厂方法)的一些优点:它们可以返回类型的子类型,并且它们不需要每次都创建对象。它们也没有构造器相关的要求。例如,辅助构造函数需要立即调用超类的主构造函数或构造函数。当我们使用伪构造函数时,我们可以推迟构造函数的使用:

fun ListView(config: Config) : ListView {
    val items = … // Here we read items from config
    return ListView(items) // We call actual constructor
}

顶层函数和作用域

我们可能想要在类之外创建工厂方法的另一个原因是我们想要在某个特定作用域内创建它。就像我们只在某个特定的类或文件中需要工厂方法一样。

有些人可能会争辩说这种使用会产生误导,因为对象创建作用域通常与该类可见作用域相关联。所有的这些可能性都是表达意图的强有力的工具,它们需要被理智地使用。虽然对象创建的具体方式包含有关它的信息,但在某些情况下使用这种可能性是非常有价值的。

主构造器

Kotlin中有个很好的特性叫做主构造器。在Kotlin类中只能有一个主构造器,但是它们比Java中已知的构造函数(在Kotlin中称为辅助构造器)更强大。主构造器的参数可以被用在类创建的任何地方。

class Student(name: String, surname: String) {
    val fullName = "$name $surname"
}

更重要的是,可以直接定义这些参数作为属性:

class Student(val name: String, val surname: String) {
    val fullName 
        get() = "$name $surname"
}

应该清楚的是,主构造器与类创建是密切相关的。请注意,当我们使用带有默认参数的主构造器时,我们不需要伸缩构造器。感谢所有这些,主构造器经常被使用(我在我的项目上创建了数百个类,我发现只有少数没有用主构造器),并且很少使用辅助构造器。这很棒。我认为就应该是这样的。主构造器与类结构和初始化紧密相关,因此在我们应该定义构造器而不是工厂方法时,它完全符合需求条件。对于其他情况,我们很可能应该使用伴随对象工厂方法或顶级函数而不是辅助构造器。

创建对象的其他方式

Kotlin的工厂方法优点并不仅仅是Kotlin如何改进对象的创建。在下一篇文章中,我们将描述Kotlin如何改进构建器模式。例如,包含多个优化,允许用于创建对象的DSL:

val dialog = alertDialog {
    title = "Hey, you!"
    message = "You want to read more about Kotlin?"
    setPositiveButton { makeMoreArticlesForReader() }
    setNegativeButton { startBeingSad() }
}

我想起来了。在本文中,我最初只描述了静态工厂方法的直接替代方法,因为这是Effective Java的第一项。与本书相关的其他吸引人的Kotlin功能将在下一篇文章中描述。如果您想收到通知,请订阅消息推送。

总结

虽然Kotlin在对象创建方面做了很多改变,但是Effective Java中有关静态工厂方法的争论仍然是最火的。改变的是Kotlin排除了静态成员方法,而是我们可以使用如下具有SFM优势的替代方法:

  • 伴生对象工厂方法
  • 顶层函数
  • 伪构造器
  • 扩展工厂方法

它们中的每一个都是在不同的需求场景下使用,并且每个都具有与Java SFM不同的优点。

一般规则是,在大多数情况下,我们创建对象所需的全部是主构造器,默认情况下连接到类结构和创建。当我们需要其他构建方法时,我们应该最有可能使用一些SFM替代方案。

译者有话说

首先,说下为什么我想要翻译有关Effective Kotlin一系列的文章。总的来说Kotlin对大家来说已经不陌生,相信有很多小伙伴,无论是在公司项目还是自己平时demo项目去开始尝试使用kotlin.当学完了Kotlin的基本使用的时候,也许你是否能感受已经到了一个瓶颈期,那么我觉得Effective Kotlin就是不错选择。Effective Kotlin教你如何去写出更好更优的Kotlin代码,有的人会有疑问这和Java有什么区别,我们都知道Kotlin和Java极大兼容,但是它们区别还是存在的。如果你已经看过Effective Java的话,相信在对比学习状态,你能把Kotlin理解更加透彻。

然后还有一个原因,在Medium上注意到该文章的作者已经几乎把对应Effective Java 的知识点用Kotlin过了一遍,从中指出了它们相同点、不同点以及Kotlin在相同的场景下实现的优势所在。

最后,用原作者一句话结尾 “I will continue as long as I see an interest from readers”,这也是我想说只要读者感兴趣,我会一直坚持把该系列文章翻译下去,一起学习。

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~