第33条:考虑使用工厂方法代替构造函数
在 Kotlin 中,类允许客户端获取实例的最常见方式是:提供一个主构造函数:
构造函数不是创建对象的唯一方法,有很多用于对象实例化的创建者设计模式,它们中的大多数都围绕着这样的思想:“函数可以为我们创建对象,而不用我们直接去创建对象”。例如,下面的顶级函数创建了 MyLinkedList
的实例:
作为构造函数的替代函数被称为“工厂函数”,因为它们产生一个对象,使用工厂函数而不是构造函数有很多优点,包括:
与构造函数不同,函数有名称。名称解释了对象是如何创建的,以及需要的参数是什么。例如,假设你看到下面的代码:
ArrayList(3)
,你能猜出里面的3是什么意思吗?它应该是新创建的列表的第一个元素呢?还是这个列表的大小呢?这肯定不是不言自明的,在这种情况下,像ArrayList.withSize(3)
这样的名称可以消除任何困惑。名称非常有用,它们解释了参数或对象创建的特征。使用名称的另外一个原因是:它解决了具有相同参数类型的构造函数之间的冲突与构造函数不同,函数可以返回其类型的任意子类型的对象。这可以用来为不同的情况提供更好的对象,尤其是我们想要将实际的对象隐藏在接口后面时,你可以参考来自标准库的
listOf
函数。它声明返回的是一个List
,这是一个接口,它真正返回的是什么呢?答案取决于我们使用的平台, 对于 Kotlin/JVM、Kotlin/JS 和 Kotlin/Native 来说都是不同的,因为它们使用不同的原生集合。这是 Kotlin 团队做的一个重要优化,他还为 Kotlin 的创建者提供了更多的自由,因为列表的实际类型可能会随时间改变而改变,但只要新对象仍然实现了接口List
并以相同的方式工作,一切都是正常的与构造函数不同,函数不需要在每次调用时创建一个新对象。它很有帮助,在我们使用函数创建对象时,我们可以包装一个缓存机制来优化对象的创建,或确保在某些情况下(单例模式)让对象复用。还可以定义一个静态工厂函数,如果无法创建对象,该函数将返回 null,例如
Connection.createOrNull()
:当由于某些原因不能创建连接时,返回 null工厂函数可以提供可能还不存在的对象,这个机制被那些注解处理器库的创建者大量使用,这样,程序员可以使用通过代理生成的对象进行操作,而无需构建项目。(例如 ServiceLoader)
当我们在对象外部定义工厂方法时,可以控制它的可见性。例如,我们可以使用顶级工厂函数,只能被同一文件或同一模块的内容访问
工厂函数可以内联(inline),因为它们的类型参数可以具体化(reified)
工厂函数可以简化那些复杂的构造函数
构造函数需要立即调用超类的构造函数或主构造函数,而使用工厂函数时,可以延迟构造函数的调用:
工厂函数的使用有一个限制:不能在子类构造函数中使用,这是因为在子类构造函数中,需要调用超类构造函数:
这通常都不是问题,因为如果我们决定要使用工厂函数创建超类,为什么要为它的子类构造函数中使用呢?我们更应该考虑为这样的类实现工厂函数:
上面的函数比前面的构造函数更长,但它有更好的特征 —— 灵活性、类的独立性以及声明可空返回类型的能力。
工厂方法的功能背后有更强大的理由支撑,你需要理解的是,它们并不与主构造函数所竞争,工厂函数仍然需要在其函数体中使用构造函数,因此构造函数必须存在。如果我们真的想强制使用工厂函数创建,它可以是私有的,但我们很少这样做(第34条:考虑一个带命名可选参数的主构造函数)。工厂函数主要与二级构造函数竞争,在 Kotlin 项目中,它们通常会胜出,二级构造函数很少使用。因为工厂函数的功能多种多样,所以对它们来说是一种竞争。让我们讨论不同的 Kotlin 工厂函数:
伴生对象工厂函数
扩展工厂函数
顶级工厂函数
伪构造函数
工厂类上的函数
伴生对象工厂函数
定义工厂函数的一个主流方法是在一个伴生对象中定义它:
Java 开发人员应该非常熟悉这种方法,因为它直接等同于静态工厂方法,而且其它语言的开发人员可能也熟悉它。在一些语言,如 C++ 中,它被称为“命名的构造函数法”,因为它的用法类似于构造函数,但是拥有了一个名称。
在 Kotlin 中,这种方法也适用于接口:
请注意:上面的函数的名称并不是具有描述性的,但是大多数开发人员应该可以理解它。原因是它来自一些 Java 的约定,多亏了这些约定,像 of
这样的简短词语就能足以让人理解参数的含义。以下是一些常见的名称及其解释:
from
—— 这是一个类型转换的函数,用于接受一个参数并返回一个相应的实例,例如:
of
—— 这是一个聚合函数,用于接受多个参数,并返回一个合并了这些参数的对应类型的实例。例如:
valueOf
—— 一个比from
和of
更加冗长的选项,例如
instance
、getInstance
—— 在单例模式中用于获取唯一的实例。当参数化时,将返回一个由入参参数化的实例。通常当实参相同时,返回的实例总是相同的,例如:
createInstance
、newInstance
—— 类似于getInstance
,但该函数保证每个调用返回一个新实例,例如:
get<Type>
、 —— 类似于getInstance
,但用于工厂函数在不同类中的情况。Type
是工厂函数返回的对象类型,例如:
缺乏经验的 Kotlin 开发人员会将伴生对象成员视为静态成员,然后将其分组在单个块中。然而,伴生对象实际上更加强大,例如,伴生对象可以实现接口和扩展类。因此,我们可以实现一般的伴生对象工厂函数,如下所示:
请注意,这种抽象的伴生对象工厂可以保存值,因此它们可以实现缓存或支持伪创建以进行测试。在 Kotlin 编程社区中,伴生对象的优点没有得到很好的利用。尽管如此,如果你查看 Kotlin 团队产品的实现,你将看到大量使用伴生对象,例如在 Kotlin Coroutines 库中,几乎每个 coroutine 上下文的伴生对象都实现了一个接口 CoroutineContext.Key
,作为用来标识此上下文的键。
扩展工厂函数
有时,我们希望创建一个工厂函数,它的作用类似于现有的伴生对象函数,而我们在既不能修改这个伴生对象,又只想在单独的文件中定义一个新的函数,这种情况下,我们可以使用伴生对象的另一个优点:为它们定义扩展函数。
假设我们不能更改接口:
然而,我们可以在它的伴生对象上定义一个扩展函数:
在调用的地方,我们可以这样写:
这提供了一种强大的可能性,它允许我们使用自己的工厂方法扩展外部库。一个问题是,要扩展伴生对象,前提是这些对象原本就要有伴生对象(甚至可以是空的):
顶级函数
创建对象的一种主流方式是使用顶级工厂函数。一些常见的例子是 listOf
、setOf
和 mapOf
。类似地,库设计人员指定用于创建对象的顶级函数。顶级工厂函数被广泛使用,例如,在 Android 中,我们有定义一个函数来创建 Intent
以启动一个 Activity 的传统, 在 Kotlin 中, getIntent
可以作为一个伴生对象函数来编写:
在 Kotlin Anko 库中,我们可以使用顶级函数 intentFor
来替代具体的类型:
这个函数也可以传递参数:
使用顶级函数创建对象对于像 List 或 Map 这样的小对象来说是一个完美的选择,因为 listOf(1,2,3)
比 List.of(1,2,3)
更简单,可读性更强。然而,公共顶级函数需要谨慎使用。公共顶级函数有一个缺点:它们无处不在。开发人员的 IDE 技巧很容易被弄乱。当顶级函数像类方法一样命名并且与它们混淆时,问题就会变得更严重。这就是为什么顶级函数应该被明智地命名。
伪构造函数
Kotlin 中的构造函数与顶级函数的使用方式相同:
它们的引用也与顶级函数相同(而构造函数引用实现了函数接口):
从使用的角度来看,大小写是构造函数和函数之间的区别。按照惯例,类以大写字母开头,函数则是小写字母。从技术上函数也可以以大写字母开头,而这个情况在不同的地方使用,例如,在 Kotlin 标准库中, List
和 MutableList
都是接口,它们不能有构造函数,但 Kotlin 开发人员希望允许以下的 List
构造方法:
这就是为什么 Kotlin stdlib 中包含了以下函数(从 Kotlin 1.1 开始):
这些顶级函数的外观和行为类似于构造函数,但它们具有工厂函数的所有优点。许多开发人员没有意识到,它们实际上是顶级函数。这就是为什么它们经常被称为“伪构造函数”。
开发者选择伪构造函数而不是真实构造函数的两个主要原因是:
为了给一个接口添加“构造函数”
拥有一个具体化(reified)类型的参数
除此之外,伪构造函数的行为应该与普通构造函数类似。它们看起来像构造函数,并且事实也应该去构造对象。如果你希望包含缓存、返回可空类型或返回类的子类,请考虑使用带名称的工厂函数,比如伴生对象工厂方法。
还有一种方法可以声明伪构造函数。使用带有 invoke
操作符的伴生对象也可以获得类似的结果,看看下面这段代码:
然而,我们很少会使用在伴生对象中实现 invoke
来生成伪构造函数,我不建议这样做。首先,因为它打破了_第12条:操作符的行为应该与其名称一致_。调用伴生对象意味着什么?要知道名称是可以替代操作符的:
调用(invoke)与对象的构造是不同的操作。以这种方式使用操作符会与其名称不一致。更重要的是,这种方法比顶级函数更为复杂,从它们的反射中可以看出这种复杂性。只需要比较反射在引用构造函数、伪构造函数和伴生对象的函数时的样子就知道了:
当你需要使用伪构造函数时,我建议使用标准的顶级函数。当我们不能在类本身中定义构造函数,或者需要添加构造函数本身所不支持的功能(比如具体化的类型参数)时,应该谨慎地使用它们,并且实现的像典型构造函数那样。
工厂类上的函数
有许多与工厂类相关的创建模式。例如,抽象工厂或原型模式。每种方式都有一些优点。
我们将看到这些方法的其中一些在 Kotlin 中使用是不太合理的,在下一条中,我们将学到重叠构造函数和建造者模式在 Kotlin 中几乎没有什么意义。
工厂类比工厂函数更有优势,因为类可以有一个状态。例如,下面一个非常简单的工厂类可以生成具有下一个 ID 数字的 Student:
工厂类可以有属性,这些属性可以用来优化对象的创建。当我们可以持有一个状态时,可以引入各式各样的优化或功能。例如,我们可以使用缓存,或者通过复制先前创建的对象来加速下一个对象的创建。
总结
如你所见,Kotlin 提供了多种指定工厂函数的方法,它们都有各自的用途。当我们设计对象的创建时,我们应该把它们记在心中。每一种方法都适用于不同的情况,其中一些应该谨慎使用:伪构造函数、顶级工厂函数和扩展工厂函数。定义工厂函数最通用的方法是使用 Companion Object
。对于大多数开发人员来说,它是安全的,而且非常直观,因为它的使用非常类似于 Java 静态工厂方法,而且 Kotlin 也继承了这种风格并实践它。
Last updated