文/ 杨加康,CFUG 社区成员,《Flutter 开发之旅从南到北》作者,小米工程师

单例设计模式(Singleton Design Pattern)理解起来非常简单。

一个类只允许创建一个实例,那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

作为最简单的一种设计模式之一,对于单例本身的概念,大家一看就能明白,但在某些情况下也很容易使用不恰当。相比其他语言,Dart 和 Flutter 中的单例模式也不尽相同,本篇文章我们就一起探究看看它在 Dart 和 Flutter 中的应用。

Flutter(able) 的单例模式

一般来说,要在代码中使用单例模式,结构上会有下面这些约定俗成的要求:

  • 单例类(Singleton)中包含一个引用自身类的静态属性实例(instance),且能自行创建这个实例。
  • 该实例只能通过静态方法 getInstance() 访问。
  • 类构造函数通常没有参数,且被标记为私有,确保不能从类外部实例化该类。

遵循以上这些要求,我们就不难能用 Dart 写出一个普通的单例模式:

class Singleton {
static Singleton _instance; // 私有的命名构造函数
Singleton._internal(); static Singleton getInstance() {
if (_instance == null) {
_instance = Singleton._internal();
} return _instance;
}
}

同时,在实现单例模式时,也需要考虑如下几点,以防在使用过程中出现问题:

  • 是否需要懒加载,即类实例只在第一次需要时创建。
  • 是否线程安全,在 Java、C++ 等多线程语言中需要考虑到多线程的并发问题。由于 Dart 是单线程模型的语言,所有的代码通常都运行在同一个 isolate 中,因此不需要考虑线程安全的问题。
  • 在某些情况下,单例模式会被认为是一种 反模式,因为它违反了 SOLID 原则中的单一责任原则,单例类自己控制了自己的创建和生命周期,且单例模式一般没有接口,扩展困难。
  • 单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。

在实际编码过程中,单例模式常见应用有:

  • 全局日志的 Logger 类、应用全局的配置数据对象类,单业务管理类。
  • 创建实例时占用资源较多,或实例化耗时较长的类。
  • 等等...

Dart 化

如上文所说的,Dart 语言作为单线程模型的语言,实现单例模式时,我们本身已经可以不用再去考虑 线程安全 的问题了。Dart 的很多其他特性也依然可以帮助到我们实现更加 Dart 化的单例。

使用 getter 操作符,可以打破单例模式中既定的,一定要写一个 getInstance() 静态方法的规则,简化我们必须要写的模版化代码,如下的 get instance:

class Singleton {
static Singleton _instance;
static get instance {
if (_instance == null) {
_instance = Singleton._internal();
} return _instance;
} Singleton._internal();
}

Dart 的 getter 的使用方式与普通方法大致相同,只是调用者不再需要使用括号,这样,我们在使用时就可以直接使用如下方式拿到这个单例对象:

final singleton = Singleton.instance;

而 Dart 中特有的 工厂构造函数(factory constructor)也原生具备了 不必每次都去创建新的类实例 的特性,将这个特性利用起来,我们就可以写出更优雅的 Dart(able) 单例模式了,如下:

class Singleton {
static Singleton _instance; Singleton._internal(); // 工厂构造函数
factory Singleton() {
if (_instance == null) {
_instance = Singleton._internal();
} return _instance;
}
}

这里我们不再使用 getter 操作符额外提供一个函数,而是将单例对象的生成交给工厂构造函数,此时,工厂构造函数仅在第一次需要时创建 _instance,并之后每次返回相同的实例。这时,我们就可以像下面这样使用普通构造函数的方式获取到单例了:

final singleton = Singleton();

如果你还掌握了 Dart 空安全及箭头函数等特性,那么还可以使用另一种方式进一步精简代码,写出像下面这样 Dart 风味十足的代码:

class Singleton {
static Singleton _instance; Singleton._internal() {
_instance = this;
} factory Singleton() => _instance ?? Singleton._internal();
}

这里,使用 ?? 作为 _instance 实例的判空操作符,如果为空则调用构造函数实例化否则直接返回,也可以达到单例的效果。

以上,Dart 单例中懒加载的无不是使用判空来实现的(if (_instance == null)??),但是在 Dart 空安全特性里还有一个非常重要的操作符 late ,它在语言层面就实现了实例的懒加载,如下面这个例子:

class Singleton {
Singleton._internal(); factory Singleton() => _instance; static late final Singleton _instance = Singleton._internal();
}

被标记为 late 的变量 _instance 的初始化操作将会延迟到字段首次被访问时执行,而不是在类加载时就初始化。这样,Dart 语言特有的单例模式的实现方式就这么产生了。

Flutter 化

说到工厂构造函数/空安全操作符等 Dart 语法上的特性,Flutter 应用中的例子已经屡见不鲜了, 但光看单例模式的定义,我们还必须联想到 Flutter 中另一个非常重要的 widget,那就是 InheritedWidget。

如果你已经是一个 Flutter 小能手,或者已经看过《Flutter 开发之旅从南到北》和之前的文章的话,一定已经对他的作用有了清晰的认识了。

InheritedWidget 状态可遗传的特性可以帮助我们很方便的实现父子组件之间的数据传递,同时,它也可以作为状态管理中的 数据仓库,作为整个应用的数据状态统一保存的地方。

上面代码中,我们通过继承 InheritedWidget 就实现了自己的可遗传组件 _InheritedStateContainer,其中的 data 变量表示全局状态数据,在这里就可以被认为是整个应用的一个单例对象

_InheritedStateContainer 还接受 child 参数作为它的子组件,child 表示的所以子组件们就都能够以某种方式得到 data 这个单一的全局数据了。

约定俗成地,Flutter 源码经常会提供一些 of 方法(类比 getInstance())作为帮助我们拿到全局数据的辅助函数。

以 Flutter 中典型的 Theme 对象为例。我们通常会在应用的根组件 MaterialApp 中创建 ThemeData 对象作为应用统一的主题样式对象:

MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);

在其他任意的组件中,我们可以使用 Theme.of(context) 拿到该对象了,且这个对象全局唯一。如下所示,我们可以将该 ThemeData 对象中的 primaryColor 应用在 Text 中:

// 使用全局文本样式
Text(
'Flutter',
style: TextStyle(color: Theme.of(context).primaryColor),
)

这个角度来看,InheritedWidget 完全可以被我们看作是最原生、最 Flutter 的单例应用了。

本文小结

本篇文章,我们经历了从实现普通单例到应用 getter 操作符 的 Dart 单例,到使用 工厂构造函数 Dart 单例,再到使用了 工厂构造函数 + 空安全语法 + 箭头函数 的 Dart 单例,最后结合对 InheritedWidget 概念的理解,看到了 Flutter 中特有的单例模式,算是每一步都走了一遍。但学习设计模式的重点还是在于实际应用,希望大家今后在实际工程中能将这些概念用起来,如果你想更进一步理解 Dart 中的单例模式,可以参阅「拓展阅读」学习更多,希望对你有帮助。

拓展阅读

关于本系列文章

Flutter / Dart 设计模式从南到北(简称 Flutter 设计模式)系列内容预计两周发布一篇,着重向开发者介绍 Flutter 应用开发中常见的设计模式以及开发方式,旨在推进 Flutter / Dart 语言特性的普及,以及帮助开发者更高效地开发出高质量、可维护的 Flutter 应用。

Flutter(able) 的单例模式的更多相关文章

  1. Flutter日常笔记

    factory修饰的构造方法 表示不是每次返回的都是新创建出来的对象, 可以取内存中已有的, 比如单例模式的书写 每次返回的都是一个实例, 这时要使用factory修饰构造方法 flutter不要求显 ...

  2. flutter dio网络请求封装实现

    flutter dio网络请求封装实现 文章友情链接:   https://juejin.im/post/6844904098643312648 在Flutter项目中使用网络请求的方式大致可分为两种 ...

  3. 【Flutter 实战】大量复杂数据持久化

    老孟导读:上一篇文章讲解了 Android 和 iOS 的文件目录系统,此篇文章讲解如何使用 SQLite 保存数据. 欢迎大家投稿:http://laomengit.com/plan/Contrib ...

  4. Flutter 设计模式|工厂模式家族

    文/ 杨加康,CFUG 社区成员,<Flutter 开发之旅从南到北>作者,小米工程师 在围绕设计模式的话题中,工厂这个词频繁出现,从 简单工厂 模式到 工厂方法 模式,再到 抽象工厂 模 ...

  5. C++实现线程安全的单例模式

    在某些应用环境下面,一个类只允许有一个实例,这就是著名的单例模式.单例模式分为懒汉模式,跟饿汉模式两种. 首先给出饿汉模式的实现 template <class T> class sing ...

  6. 23种设计模式--单例模式-Singleton

    一.单例模式的介绍 单例模式简单说就是掌握系统的至高点,在程序中只实例化一次,这样就是单例模式,在系统比如说你是该系统的登录的第多少人,还有数据库的连接池等地方会使用,单例模式是最简单,最常用的模式之 ...

  7. angular2系列教程(十)两种启动方法、两个路由服务、引用类型和单例模式的妙用

    今天我们要讲的是ng2的路由系统. 例子

  8. java设计模式之--单例模式

    前言:最近看完<java多线程编程核心技术>一书后,对第六章的单例模式和多线程这章颇有兴趣,我知道我看完书还是记不住多少的,写篇博客记录自己所学的只是还是很有必要的,学习贵在坚持. 单例模 ...

  9. 设计模式C#合集--单例模式

    单例模式 代码: 第一种: private static Singleton singleton = null; private Singleton() { } public static Singl ...

  10. 设计模式之单例模式(Singleton)

    设计模式之单例模式(Singleton) 设计模式是前辈的一些经验总结之后的精髓,学习设计模式可以针对不同的问题给出更加优雅的解答 单例模式可分为俩种:懒汉模式和饿汉模式.俩种模式分别有不同的优势和缺 ...

随机推荐

  1. oeasy教您玩转vim - 80 - # 宏macro

    ​ 宏 macro 回忆 这次我们了解了编码格式 屏幕显示的encoding 文件保存的fileencoding 不能搞乱了 一般用什么编的就用什么解 解铃还须系铃人 打开不正确的话,就要切到正确的上 ...

  2. [rCore学习笔记 010]基于 SBI 服务完成输出和关机

    RustSBI的两个职责 它会在计算机启动时进行它所负责的环境初始化工作,并将计算机控制权移交给内核 在内核运行时响应内核的请求为内核提供服务 这里用不太确切的话表述一下,RustSBI作为介于内核和 ...

  3. IPFS 解决国内 docker mirror 封锁

    IPFS 解决国内 docker mirror 封锁 内容仅用于研究,帮助开发者学习技术知识,以建设祖国 IPFS 技术是当前 Web3 的主要基建设施,提供去中心化存储,以及 libp2p 的去中心 ...

  4. python Selenium 不要混合隐式和显式等待

    警告:不要混合隐式和显式等待.这样做可能会导致不可预测的等待时间.例如,设置10秒的隐式等待和15秒的显式等待可能会导致20秒后发生超时 Warning: Do not mix implicit an ...

  5. Jmeter调试取样器

    调试取样器(Debug Sampler),生成一个包含JMeter变量或属性值的样本,并且这些值可以在组件[查看结果树]的响应窗格中看到 组件路径:线程组->右键添加->取样器->D ...

  6. Python和RPA网页自动化-让非标准下拉框选择指定文本的方法

    以下方"节点审批"下拉框为例 该下拉框没有<select>标签,而是<div><ul><li>标签.分别使用Python和RPA网页 ...

  7. Jmeter循环指定接口并接收不同参数值

    背景:在做接口自动化流程中我们会遇到需要提取上个接口的返回值作为下个接口的传参,在提取值数量不固定的情况下,如何在一个线程让指定接收参数的接口自动循环完所有的传参呢? 解决:添加[循环控制器]指定循环 ...

  8. 【SpringMVC】 Controller接收深度复杂对象封装不到的问题

    首先来看数据结构的定义: 一个Form对象,然后里面有一个排版日期对象的List集合 排班集合的每个元素中又有一个String集合 在前端的Post请求中可以看到这个String集合是传递了的 但是D ...

  9. 【SQL】 牛客网SQL训练Part1 简单难度

    地址位置: https://www.nowcoder.com/exam/oj?difficulty=2 查找入职员工时间排名倒数第三的员工所有信息 -- 准备脚本 drop table if exis ...

  10. 【Vue】14 UI库

    PC端: 第一梯队:基于JQuery实现的Dom操作,和一些简单CSS样式组成 Layui Bootstrap EasyUI 第二梯队:基于Vue2.0开发的UI库,组件化开发 ElementUI A ...