谈到设计模式这个“古老”的话题,大家先别急着划走哈,虽然对它再熟悉不过,几乎是最初开始学习编程到现在伴随着我们整个编程生涯,最早 Java
、C++
语言实现的各种设计模式到现在还会经常有所接触,面试中也是必问的环节,在开发 Flutter
项目的时候,也会多少借鉴了其它语言设计模式的实现,但始终觉得dart
语言实现的设计模式理解不够系统,有的实现还缺点儿 dart
语言本身的语法特性。加上最近在看一些 Flutter
框架及常用第三方插件的源码时候,发现这些源码背后或多或少都有设计模式的影子。铺垫了这么多,还真不是我在这里故意卷Flutter
设计模式这些个话题,它对于我们日常编写高质量代码及理解 dart
语言特性、Flutter
的框架和热门第三方插件、OOP
设计模式、理解SOLID
原则及其应用、代码架构或软件工程等还是有很大的帮助的。既然这么多的好处,那还等什么呢?
Singleton Pattern
Singleton
模式在项目中再常见不过了,实现起来也很简单,它一般包括私有构的造函数、一个静态实例和提供全局访问点。该模式是用来确保一个类只有一个实例,并提供一个全局访问点。简单来说,就是限制一个类在应用程序中只能有一个实例存在。这种模式通常用于需要全局共享资源的场景,比如配置管理、日志记录器、全局状态保存等,下面来实现一个单例类。
1 | class Singleton { |
上面的单例类 Singleton
可以看出:
Singleton
类构造函数被标记为私有,用来确保该类不能从类外部去实例化。- 包含一个静态实例,该实例是对类实例本身的引用。
- 该实例只能通过静态的
get
访问,为全局提供访问点。
除了上面的写法还有没有其它的实现呢?我们可以使用 factory
构造函数特性来实现。
1 | class Singleton { |
在 Dart
中,factory
构造函数是一种特殊的构造函数,用于控制类实例的创建过程。与常规构造函数不同,factory
构造函数并不总是创建一个新的实例,它可以返回现有的实例或一个子类的实例,factory
构造函数也常被用来实现单例模式。当然除了前面两种还有如下面这种更加简单的实现:
1 | class Singleton { |
在 Flutter
开发中,基于 factory
构造函数和上面第三种实现方式会更常见,因为它们够简单直接且线程安全。那么在 Dart
中还有没有更加便捷的方式创建单例呢?当然有的。
其它的实现方式
通过依赖注入插件 injectable
添加为类 @Singleton
和 @LazySingleton
注解也能实现单例,代码也更加的简洁,也是我个人比较推荐的实现方式。
1 | abstract class AppNavigator { |
线程安全
我们知道 Dart
可以说是一种单线程编程语言,代码的执行通常发生在一个单线程上。这个单线程模型是通过事件循环来管理的。事件循环负责处理事件队列中的任务,这些任务包括 I/O 操作、定时器回调、用户输入等。
所有的 Dart 代码(除非明确使用多线程技术)都是在这个单线程上执行的,也就是一个隔离区( isolate
)中执行,因此,在 Dart
中实现单例时,只要您不自己创建一个新的独立于代码的隔离区( isolate
),根本就不必担心线程安全性。所以上面懒加载式单例的第一种实现方式基本上能满足我们的需求。
单例模式与 SOLID
原则
单例模式由于其本身的实现(确保一个类只有一个实例,并提供全局访问点)在某些方面与 SOLID
原则(面向对象设计的五个原则)是相冲突的,下面实现一个简单的日志打印的单例类来详细说明一下。
1 | class Logger { |
SOLID
原则中的单一职责原则要求每个类应该只有一个职责,即仅负责一件事。而 Logger
单例类不仅负责日志记录,还负责管理其唯一实例的生命周期,它承担了额外的职责,违背了单一职责原则。
SOLID
原则中开闭原则要求类应该对扩展开放,对修改关闭,在而单例 Logger
中如果想要拓展以支持不同的日志目标,如将日志写入文件等,不得不修改现有代码,而不是通过继承或组合进行扩展功能。
1 | class Logger { |
这不符合开闭原则,因为需要直接修改 Logger
类来添加新功能。
里氏替换原则要求子类应该可以替换父类,并且不影响其它代码的正确执行。单例模式通过私有构造函数限制实例化,所以继承和替换就很难做到了。例如,如果创建一个子类 FileLogger
继承自 Logger
。
1 | class FileLogger extends Logger { |
上面写法会直接报错,FileLogger
也没法替换 Logger
来实现文件日志记录的逻辑。
接口隔离原则要求不依赖于不需要的接口。单例模式本身与接口隔离原则没有直接冲突。然而,如果单例类实现了过多的职责,就可能导致其接口庞大,调用方很多时候不得不依赖于它们不需要的方法,这就违反接口隔离原则。
1 | class Logger { |
依赖倒置原则要求高层模块不应该依赖低层模块,二者都应该依赖于抽象。单例模式通常通过静态方法或属性提供实例,这使得高层模块依赖于具体实现,而不是抽象接口。这个原则我在之前的文章《Flutter大型项目架构:依赖管理篇》中有讲到,文章的 AppNavigator
和 AppNavigatorImpl
类,AppNavigator
是抽象类,所有用到路由的调用都是通过 AppNavigator
,而 AppNavigatorImpl
才是实现类,也可以参考上面其它实现方式的代码。
需要注意什么
虽说 Singleton
很多和 SOLID
原则相违背,但其简单直接实现方式,尤其是在需要全局共享资源的场景中去使用太方便了。但是我们在追求方便的同时也要留意过度使用 Singleton
模式可能带来的问题,尤其是大型的 Flutter
项目中。
- 确保单例适当的生命周期,避免资源的泄露,某些时候单例对象可能会持有大量资源,或者与其他部分有复杂的交互,需要在合适的时机释放这些资源。
- 单例模式应仅用于那些需要在全局范围内唯一且易于访问的对象,如
Logger
类、AppNavigator
类等。如果滥用单例会导致代码难以维护和测试。 - 确保单例对象在使用前已经正确配置和初始化。特别是在大型项目中,单例可能需要依赖多个模块的初始化顺序,确保这些依赖关系不会引发初始化错误,如在一个统一的模块(
initializer
)来处理Singleton
初始化。 - 在类中直接单例不咋容易被测试,这个时候可以使用依赖注入(
DI
)来创建和管理单例实例,在测试时可以替换单例对象。 - 确保单例对象的职责单一,不要让其承担过多的责任。通过接口分离和依赖注入,保持系统设计的灵活性和可扩展性。参考
AppNavigator
和AppNavigatorImpl
类的实现。
小结
本文介绍了单例模式实现的几种方式、单例的线程安全问题、单例模式与 SOLID
原则和在大型项目中使用单例需要注意什么,希望对你在以后的 Flutter
开发过程中有所帮助,感谢您的阅读!