简单工厂的模式,它的核心作用是通过一个工厂类隐藏对象实例的创建逻辑,而不需要对外暴露。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。
假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑:
interface Computer {
val cpu: String
}
class PC(override val cpu: String = "Core") : Computer
class Server(override val cpu: String = "Xeon") : Computer
enum class ComputerType {
PC, Server
}
class ComputerFactory {
fun produce(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
fun main() {
val pc = ComputerFactory().produce(ComputerType.PC)
println(pc.cpu) // Core
}
以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑和客户端之间实现解耦。 这是用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。 当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。
我们已经知道Kotlin支持用object来实现Java中的单例模式。所以可以实现一个ComputerFactory单例,而不是一个工厂类:
object ComputerFactory {
fun produce(type: ComputerType) : Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
fun main() {
// 这样我们就不用再每次都创建对象了
val pc = ComputerFactory.produce(ComputerType.PC)
println(pc.cpu)
}
由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。我们可以用运算符重载来通过operator操作符 重载invoke方法来代替produce,从而进一步简化表达:
object ComputerFactory {
operator fun invoke(type: ComputerType) : Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
fun main() {
// 这样就会非常简洁
val pc = ComputerFactory(ComputerType.PC)
println(pc.cpu)
}
上面的工厂模式实现已经足够优雅,然而依旧不够完美: 我们是否可以直接通过Computer()而不是ComputerFactory()来创建一个实例呢?
我们可以通过在Computer接口中定义一个伴生对象,这样就能实现以上的需求:
interface Computer {
val cpu: String
companion object {
operator fun invoke(type: ComputerType) : Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
}
fun main() {
val pc = Computer(ComputerType.PC)
println(pc.cpu)
}
我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用 Factory来命名Computer的伴生对象,如下:
interface Computer {
val cpu: String
companion object Factory {
operator fun invoke(type: ComputerType) : Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
}
fun main() {
val pc = Computer.Factory(ComputerType.PC)
println(pc.cpu)
}
依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。 假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好的隐藏。 那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好的实现这一需求:
比如我们希望给Computer增加一种功能,通过CPU型号来判断电脑类型,那么可以如下实现:
fun Computer.Factory.fromCPU(cpu: String) : ComputerType? = when(cpu) {
"Core" -> ComputerType.PC
"Xeon" -> ComputerType.Server
else -> null
}
fun main() {
val pc = Computer.Factory.fromCPU("Core")
println(pc)
}
Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,可以改进一种更复杂的工厂模式,称为抽象工厂。 上面的例子中已经用工厂模式很好的处理了一个产品等级结构的问题。但是如果现在引入了品牌商的概念,我们有好几个不同的电脑品牌,比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护, 所以这时候我们就需要引入抽象工厂模式。
为创建一组相关或相互依赖的对象提供一个接口,而且无须指定他们的具体类。
interface Computer
class Dell: Computer
class Asus: Computer
class Acer: Computer
class DellFactory: AbstractFactory() {
override fun produce() = Dell()
}
class AsusFactory: AbstractFactory() {
override fun produce() = Asus()
}
class AcerFactory: AbstractFactory() {
override fun produce() = Acer()
}
abstract class AbstractFactory {
abstract fun produce(): Computer
companion object {
operator fun invoke(factory: AbstractFactory): AbstractFactory {
return factory
}
}
}
fun main(args: Array<String>) {
val dellFactory = AbstractFactory(DellFactory())
val dell = dellFactory.produce()
println(dell)
}
可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的 AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。
由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的 工厂类对象作为参数进行构造,这个在语法上显然不够优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。 我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。
abstract class AbstractFactory {
abstract fun produce(): Computer
companion object {
// 增加reified关键字
inline operator fun <reified T : Computer> invoke(): AbstractFactory =
when (T::class) {
Dell::class -> DellFactory()
Asus::class -> AsusFactory()
Acer::class -> AcerFactory()
else -> throw IllegalArgumentException()
}
}
}
这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。 我们来分析下这段代码:
- 通过将invoke方法用inline定义为内联函数,我们就可以引入reified关键字,使用具体化参数类型的语法特性。
- 要具体化的参数类型为Computer,在invoke方法中我们通过判断它的具体类型,来返回对应的工厂类对象。
再来看看通过上面内联函数改善后的工厂类的创建语法表达:
fun main(args: Array<String>) {
val dellFactory = AbstractFactory<Dell>()
val dell = dellFactory.produce()
println(dell)
}
现在终于可以用类似创建一个泛型类对象的方式,来构建一个抽象工厂具体对象了。
构造者模式与单例模式一样,它主要做的事情就是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性: 代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。
一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器模式, 即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少, 但同样存在明显的缺点,因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。
构建者模式可以避免以上问题,我们用Kotlin来实现Java中的构建者模式:
class Robot private constructor (
val code: String,
val battery: String?,
val height: Int?,
val weight: Int?) {
class Builder(val code: String) {
private var battery: String? = null
private var height: Int? = null
private var weight: Int? = null
fun setBattery(battery: String?): Builder {
this.battery = battery
return this
}
fun setHeight(height: Int): Builder {
this.height = height
return this
}
fun setWeight(weight: Int): Builder {
this.weight = weight
return this
}
fun build(): Robot {
return Robot(code, battery, height, weight)
}
}
}
}
var robot = Robot.Builder("007")
.setBattery("R6")
.setHeight(100)
.setWeight(80)
.build()
为了避免代码太长,上面的例子中只选择了4个属性,其中code是必须属性,battery、height、weight为可选属性。我们来分析一下它的具体思路:
- Robot类内部定义了一个嵌套类Builder,由它负责创建Robot对象
- Robot类的构造函数用private进行修饰,这样可以确保使用者无法直接通过Robot声明实例
- 通过在Builder类中定义set方法来对可选的属性进行设置
- 最终调用Builder类中的build方法来返回一个Robot对象
这种链式调用的设计看起来确实优雅了很多,同时对于可选参数的设置也显得比较语义化。此外,构建者模式另外一个好处就是解决了 多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。
然而,构建者模式也存在一些不足:
- 如果业务需求的参数很多,代码依然会显得比较长
- 你可能会在使用Builder的时候忘记在最后调用build方法
- 由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。
事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时, 是这样子描述它的:本质上builder模式模拟了具名的可选参数。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。
Kotlin中的函数和构造器都支持这一特性,它主要表现为两点:
- 在具体化一个参数的取值时,可以通过带上它的参数名,而不是它在所有参数中的位置决定。
- 由于参数可以设置默认值,这允许我们只给出部分参数的取值,而不必是所有的参数。
因此,我们可以直接使用Kotlin中原生的语法特性来实现构建者模式的效果。现在重新设计以上的Robot例子:
class Robot(
val code: String,
val battery: String? = null,
val height: Int? = null,
val weight: Int? = null
)
private fun main() {
val robot1 = Robot(code = "007")
val robot2 = Robot(code = "007", battery = "R6")
val robot3 = Robot(code = "007", height = 100, weight = 80)
println(robot1)
}
可以发现,相比构建者模式,通过具名的可选参数构造类具有很多优点:
- 代码变得十分简单,这不仅表现在Robot类的结构体代码量,我们在声明Robot对象时的语法也要更加简洁
- 声明对象时,每个参数名都可以是显式的,并且无须按照顺序书写,非常方便灵活
- 由于Robot类的每个对象都是val声明的,相较构建者模式中的var的方案更加安全,这在要求多线程并发安全的业务场景中会显得更有优势。
此外,如果你的类的功能足够简单,更好的思路是用data class直接声明一个数据类。数据类同样支持以上的所有特性。
我们再来看看构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据 电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在重新修改一下上面build方法的实现:
fun build(): Robot {
if (weight != null && battery == null) {
throw IllegalArgumentException("Battery should be determined when setting weight.")
} else {
return Robot(code, battery, height, weight)
}
}
这种在build方法中对参数进行约束的手段,可以让业务变得更加安全。那么,通过具名的可选参数来构造类的方案该如何实现呢?
显然,我们同样可以在Robot类的init方法中增加以上的校检代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字 进行参数限制,本质上它是一个内联的方法,有点类似于Java的assert。
class Robot(
val code: String,
val battery: String? = null,
val height: Int? = null,
val weight: Int? = null
) {
init {
require(weight == null || battery != null) {
"Battery should be determined when setting weight."
}
}
}
可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式, 因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。
观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时, 需要通知相应的观察者,使这些观察者对象能够自动更新。
简单来说,观察者模式无非做两件事情:
- 订阅者(observer)添加或删除对发布者(publisher)的状态监听。
- 发布者状态改变时,将事件通知给监听它的所有观察者,然后观察者执行响应逻辑。
Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式,接下来我们就采用 它们来实现一个动态更新股价的例子。
class StockUpdate: Observable() {
val observers = mutableSetOf<Observer>()
fun setStockChanged(price: Int) {
this.observers.forEach { it.update(this, price)}
}
}
class StockDisplay: Observer {
override fun update(o: Observable, price: Any) {
if (o is StockUpdate) {
println("The latest stock price is ${price}")
}
}
}
fun main(args: Array<String>) {
val su = StockUpdate()
val sd = StockDisplay()
su.observers.add(sd)
su.setStockChanged(100)
}
上面是通过Kotlin使用Java标准库中的类和方法来实现了观察者模式。事实上,Kotlin的标准库额外引入了可被观察的委托属性, 也可以利用它来实现同样的场景。我们可以先用这一委托属性来改造以上的程序:
import kotlin.properties.Delegates
interface StockUpdateListener {
fun onRise(price: Int)
fun onFall(price: Int)
}
class StockDisplay: StockUpdateListener {
override fun onRise(price: Int) {
println("The latest stock price has risen to ${price}")
}
override fun onFall(price: Int) {
println("The latest stock price has fell to ${price}")
}
}
class StockUpdate {
var listeners = mutableSetOf<StockUpdateListener>()
var price: Int by Delegates.observable(0) {_, old, new ->
listeners.forEach {
if (new > old) it.onRise(price) else it.onFall(price)
}
}
}
fun main(args: Array<String>) {
val su = StockUpdate()
val sd = StockDisplay()
su.listeners.add(sd)
su.price = 100
su.price = 98
}
// 执行结果
The latest stock price has risen to 100
The latest stock price has fell to 98
如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应, 我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得冗余。换个角度,如果我们把发布者的事件推送看成一个第三方服务, 那么它提供的API接口只有一个,API调用者必须承担更多的职责。
显然,使用Delegates.observable()的方案更加灵活。它提供了三个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。 通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中 实现该接口的onRise和onFall方法,实现了解耦。
- 邮箱 :charon.chui@gmail.com
- Good Luck!