解决依赖与多种模式的应用

《秘封活动记录·祝》到了,Gogo很开心的去吃土了。

读了不少关于设计模式的书,一直不知道如何应用,最近构思 ForeverNight 时,才真正尝试用模式去解决问题,接下来是我关于一些模式、方法,在解决依赖方面的感悟。

什么是依赖?

我对依赖的理解比较简单,若 A类需要用到 B类实例的一些方法,那么 A就依赖 B,本文是一只萌新的感悟,在定义并不严谨,请路过的大佬包涵。

单例模式

用单例模式来解决依赖是比较容易的。

但我并不喜欢单例,因为它有许多缺点。单例很像全局变量,大量单例会让程序更加复杂,并且带来耦合问题。而且许多情况下,单例并不能很好的解决问题,比如无法动态提供一个子类的示例来代替。

/**
 * 经典的单例模式,使用了内部类加载的特性
 */
class AudioPlayer private constructor() {

    private object InstanceHolder {
        val instance: AudioPlayer = AudioPlayer()
    }

    companion object {
        fun getInstance(): AudioPlayer = InstanceHolder.instance
    }

    fun playMusic() {
        // ...
    }
}

/**
 * 使用者直接使用单例
 */
class A {
    fun func() {
        AudioPlayer.getInstance().playMusic()
    }
}

服务定位模式

服务定位模式是专门创建一个类,用于提供服务。服务定位模式会有静态的方法或静态的实例来提供服务。

它与单例模式有许多相似之处,都是通过静态获取依赖,只不过服务定位模式提供的是其他类的实例,而单例模式提供的是它本身的实例。

相比单例模式,服务定位模式有一些优势,它将提供服务的职责与服务的实现分离,更符合单一职责原则;它能动态地提供服务。对于一些全局的服务使用服务定位模式而非单单例模式更合适。

/**
 * 利用服务定位模式,动态提供 AudioPlayer
 * 在 Debug状态下使用 NO_SOUNDS_PLAYER 代替
 */
class ServiceLocator {

    companion object {
        fun getAudioPlayer(): AudioPlayer = when (state) {
            DEBUG -> NO_SOUNDS_PLAYER
            else -> DEFAULT_PLAYER
        }

        fun getOtherService() {
            // ...
        }
    }

}

/**
 * 使用者通过定位器获取服务
 */
class B {
    fun func() {
        ServiceLocator.getAudioPlayer().playMusic()
    }
}

然而对于非全局、不唯一的对象,服务定位模式并非那么有用。并且由于服务定位模式也是依赖于静态,大量使量会造成类似单例的代码混乱。

依赖注入

相对于服务定位模式中,使用者主动获取依赖,另一种常见的获取依赖的方式,是由外部把依赖传递过来,这就是依赖注入。

依赖注入,最常见的就是通过构造函数注入依赖,比如下面这个例子。

/**
 * 通过构造函数注入依赖
 */
class C(val audioPlayer: AudioPlayer) {
    fun func() {
        audioPlayer.playMusic()
    }
}

另外一种是通过Setter函数注入依赖,这就不示范了。

依赖注入,可以很好地解决前面几个模式的一些问题,由于依赖的创建、获取都是外界负责的,这就把获取依赖的职责分离。

当然这也会造成麻烦,比如上面这个例子,创建对象的过程变得繁琐,需要先准备好依赖才能创建。为了将这个复杂的过程封装起来,还得使用工厂模式,我想为何Java中工厂模式这么常见,其中一大原因就是依赖注入导致对象的创建过程变得复杂吧。

IoC容器

虽然依赖注入可以解决依赖的问题,但遇到一些复杂的依赖仍是比较崩溃。
比如 A依赖于 B和 C, B和 C又有各自的依赖。即使把整个过程放入工厂中,也是令人十分头疼。

这时使用IoC容器便是一个选择,IoC容器在解决这类复杂的依赖时极为有用。可以把IoC容器理解为一张注册表,只要向这之中注册依赖,以后就可以获取。

比如上述的例子,只要先把 B和 C所需的依赖注册,那么创建 B和 C时,就可以通过容器获取所需的依赖。创建后再把 B和 C也注册到容器中,这样创建 A时就可以直接从 IoC容器中获取 B、C。若还有别的类依赖于 A呢?那就把 A也放入IoC中。

如此一来,工厂也会变得十分简单,只要从容器中获取依赖,再注入到 A之中即可。

若不是用容器,工厂会变得怎么样呢?若 B、C可以在工厂中创建,那么还好,创建即可。若 B、C是已经创建好的服务,不应该在 A的工厂中产生新的实例,那工厂又应该如何获取 B、C呢。

你会发现依赖注入仅仅是把 A的依赖转移到 A的工厂中,对工厂如何获取依赖,并没有什么贡献,还得用先前的模式解决。

有了 IoC容器情况就会大不相同,工厂只需要依赖容器就可以获取所需的所有依赖,那样大可以通过构造函数把容器注入到工厂中,就可以彻底摆脱前几种方式对静态的依赖。

/**
 * 初始化容器时设定一些基本依赖
 */
fun initContainer(container: Container) {
    container.registerAsSingleton<AudioPlayer>(audioPlayer) // 作为单例的依赖
    container.registerAsFactory<ID> { generateID() } // 作为工厂的依赖
}

/**
 * 工厂中直接通过容器注入依赖
 */
class Factory(val container: Container) {
    fun create(): DemoClass {
        val audioPlayer = container.inject<AudioPlayer>()
        val id = container.inject<ID>()
        return DemoClass(audioPlayer, id)
    }
}

class DemoClass(val audioPlayer: AudioPlayer, val id: ID) {
    fun func() {
        audioPlayer.playMusic()
    }
}

本文根据 CC BY-NC-SA 4.0 License 协议授权
本文链接:https://blog.gogo.moe/20170531_%E8%A7%A3%E5%86%B3%E4%BE%9D%E8%B5%96%E4%B8%8E%E5%A4%9A%E7%A7%8D%E6%A8%A1%E5%BC%8F%E7%9A%84%E5%BA%94%E7%94%A8/