Flutter设计模式全面解析:单例模式

在这里插入图片描述
谈到设计模式这个“古老”的话题,大家先别急着划走哈,虽然对它再熟悉不过,几乎是最初开始学习编程到现在伴随着我们整个编程生涯,最早 JavaC++ 语言实现的各种设计模式到现在还会经常有所接触,面试中也是必问的环节,在开发 Flutter 项目的时候,也会多少借鉴了其它语言设计模式的实现,但始终觉得dart 语言实现的设计模式理解不够系统,有的实现还缺点儿 dart 语言本身的语法特性。加上最近在看一些 Flutter 框架及常用第三方插件的源码时候,发现这些源码背后或多或少都有设计模式的影子。铺垫了这么多,还真不是我在这里故意卷Flutter 设计模式这些个话题,它对于我们日常编写高质量代码及理解 dart 语言特性、Flutter 的框架和热门第三方插件、OOP 设计模式、理解 SOLID 原则及其应用、代码架构或软件工程等还是有很大的帮助的。既然这么多的好处,那还等什么呢?

Singleton Pattern

Singleton 模式在项目中再常见不过了,实现起来也很简单,它一般包括私有构的造函数、一个静态实例和提供全局访问点。该模式是用来确保一个类只有一个实例,并提供一个全局访问点。简单来说,就是限制一个类在应用程序中只能有一个实例存在。这种模式通常用于需要全局共享资源的场景,比如配置管理、日志记录器、全局状态保存等,下面来实现一个单例类。

class Singleton {
  // 1. 私有构造函数
  Singleton._privateConstructor();

  // 2. 静态实例
  static final Singleton _instance = Singleton._privateConstructor();

  // 3. 提供全局访问点
  static Singleton get instance => _instance;
}

上面的单例类 Singleton 可以看出:

  1. Singleton 类构造函数被标记为私有,用来确保该类不能从类外部去实例化。
  2. 包含一个静态实例,该实例是对类实例本身的引用。
  3. 该实例只能通过静态的 get 访问,为全局提供访问点。

除了上面的写法还有没有其它的实现呢?我们可以使用 factory 构造函数特性来实现。

class Singleton {
  static final Singleton _instance = Singleton._internal();

  factory Singleton() {
    return _instance;
  }

  Singleton._internal();
}

// 调用
// Singleton();

Dart 中,factory 构造函数是一种特殊的构造函数,用于控制类实例的创建过程。与常规构造函数不同,factory 构造函数并不总是创建一个新的实例,它可以返回现有的实例或一个子类的实例,factory 构造函数也常被用来实现单例模式。当然除了前面两种还有如下面这种更加简单的实现:

class Singleton {
  Singleton._();

  static final Singleton instance = Singleton._();
}

// 调用
// Singleton.instance;

Flutter 开发中,基于 factory 构造函数和上面第三种实现方式会更常见,因为它们够简单直接且线程安全。那么在 Dart 中还有没有更加便捷的方式创建单例呢?当然有的。

其它的实现方式

通过依赖注入插件 injectable 添加为类 @Singleton@LazySingleton 注解也能实现单例,代码也更加的简洁,也是我个人比较推荐的实现方式。

abstract class AppNavigator {
  const AppNavigator();
  void push();
}

(as: AppNavigator)
class AppNavigatorImpl {
	
    void push(){
    	//......
    }
}

线程安全

Flutter 是否真的有必要像上面例子中通过加锁来保证线程安全呢?我们知道 Dart 可以说是一种单线程编程语言,代码的执行通常发生在一个单线程上。这个单线程模型是通过事件循环来管理的。事件循环负责处理事件队列中的任务,这些任务包括 I/O 操作、定时器回调、用户输入等。

所有的 Dart 代码(除非明确使用多线程技术)都是在这个单线程上执行的,也就是一个隔离区( isolate )中执行,因此,在 Dart 中实现单例时,只要您不自己创建一个新的独立于代码的隔离区( isolate ),根本就不必担心线程安全性。所以上面懒加载式单例的第一种实现方式基本上能满足我们的需求。

单例模式与 SOLID 原则

单例模式由于其本身的实现(确保一个类只有一个实例,并提供全局访问点)在某些方面与 SOLID 原则(面向对象设计的五个原则)是相冲突的,下面实现一个简单的日志打印的单例类来详细说明一下。

class Logger {
  static final Logger _instance = Logger._internal();
  
  // 私有构造函数,防止外部实例化
  Logger._internal();
  
  static Logger get instance => _instance;

  void log(String message) {
    print("Log: $message");
  }
}

SOLID 原则中的单一职责原则要求每个类应该只有一个职责,即仅负责一件事。而 Logger 单例类不仅负责日志记录,还负责管理其唯一实例的生命周期,它承担了额外的职责,违背了单一职责原则。

SOLID 原则中开闭原则要求类应该对扩展开放,对修改关闭,在而单例 Logger 中如果想要拓展以支持不同的日志目标,如将日志写入文件等,不得不修改现有代码,而不是通过继承或组合进行扩展功能。

class Logger {
  static final Logger _instance = Logger._internal();

  Logger._internal();

  static Logger get instance => _instance;

  void log(String message) {
    print("Log: $message");
  }
  
  // 添加新功能
  void saveLogToFile(String message) {
    // 将日志写入文件的代码
  }
}

这不符合开闭原则,因为需要直接修改 Logger 类来添加新功能。

里氏替换原则要求子类应该可以替换父类,并且不影响其它代码的正确执行。单例模式通过私有构造函数限制实例化,所以继承和替换就很难做到了。例如,如果创建一个子类 FileLogger 继承自 Logger

class FileLogger extends Logger {
  void log(String message) {
    // 自定义文件日志记录逻辑
  }
}

上面写法会直接报错,FileLogger 也没法替换 Logger 来实现文件日志记录的逻辑。

接口隔离原则要求不依赖于不需要的接口。单例模式本身与接口隔离原则没有直接冲突。然而,如果单例类实现了过多的职责,就可能导致其接口庞大,调用方很多时候不得不依赖于它们不需要的方法,这就违反接口隔离原则。

class Logger {
  static final Logger _instance = Logger._internal();

  Logger._internal();

  static Logger get instance => _instance;

  void log(String message) {
    print("Log: $message");
  }
  
  void logToFile(String message) {
    // 将日志写入文件的代码
  }
  
  void logToNetwork(String message) {
    // 将日志发送到网络服务器的代码
  }
}

依赖倒置原则要求高层模块不应该依赖低层模块,二者都应该依赖于抽象。单例模式通常通过静态方法或属性提供实例,这使得高层模块依赖于具体实现,而不是抽象接口。这个原则我在之前的文章《Flutter大型项目架构:依赖管理篇》中有讲到,文章的 AppNavigatorAppNavigatorImpl 类,AppNavigator 是抽象类,所有用到路由的调用都是通过 AppNavigator,而 AppNavigatorImpl 才是实现类,也可以参考上面其它实现方式的代码。

需要注意什么

虽说 Singleton 很多和 SOLID 原则相违背,但其简单直接实现方式,尤其是在需要全局共享资源的场景中去使用太方便了。但是我们在追求方便的同时也要留意过度使用 Singleton 模式可能带来的问题,尤其是大型的 Flutter 项目中。

  1. 确保单例适当的生命周期,避免资源的泄露,某些时候单例对象可能会持有大量资源,或者与其他部分有复杂的交互,需要在合适的时机释放这些资源。
  2. 单例模式应仅用于那些需要在全局范围内唯一且易于访问的对象,如 Logger 类、AppNavigator 类等。如果滥用单例会导致代码难以维护和测试。
  3. 确保单例对象在使用前已经正确配置和初始化。特别是在大型项目中,单例可能需要依赖多个模块的初始化顺序,确保这些依赖关系不会引发初始化错误,如在一个统一的模块(initializer)来处理 Singleton 初始化。
  4. 在类中直接单例不咋容易被测试,这个时候可以使用依赖注入(DI)来创建和管理单例实例,在测试时可以替换单例对象。
  5. 确保单例对象的职责单一,不要让其承担过多的责任。通过接口分离和依赖注入,保持系统设计的灵活性和可扩展性。参考 AppNavigatorAppNavigatorImpl 类的实现。

小结

本文介绍了单例模式实现的几种方式、单例的线程安全问题、单例模式与 SOLID 原则和在大型项目中使用单例需要注意什么,希望对你在以后的 Flutter 开发过程中有所帮助,感谢您的阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/640195.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IntelliJ IDEA集成Baidu Comate,商城系统支付交易功能开发实战

文章目录 Baidu Comate介绍安装配置体验安装插件配置体验注释生成代码技术问答 实战设计表生成代码导入数据 总结 Baidu Comate介绍 在科技互联网飞速发展的今天,百度凭借其深厚的技术积累和创新能力,推出了一款名为Baidu Comate智能代码助手的产品。该…

Linxu 系统中 修改 docker 镜像存放目录 修改docker默认路径。亲测有效。

1、关闭docker 服务 systemctl stop docker 2、创建新的存放路径(-p 父级目录不存在一起创建) mkdir /home/service/docker -p 3、移动默认路径中的镜像文件到新目录 mv /var/lib/docker/* /home/service/docker/ 4、修改docker.service 将新的路…

【C++】继承(二)深入理解继承:派生类默认成员函数与友元、静态成员的奥秘

目录 派生类的默认成员函数①派生类的构造函数②派生类的拷贝构造函数③派生类的赋值构造④派生类的析构函数 继承与友元继承与静态成员 前言 我们在上一章讲解了: 继承三部曲,本篇基于上次的基础继续深入了解继承的相关知识,欢迎大家和我一起学习继承 派…

微信小程序报错:notifyBLECharacteristicValueChange:fail:nodescriptor的解决办法

文章目录 一、发现问题二、分析问题二、解决问题 一、发现问题 微信小程序报错:notifyBLECharacteristicValueChange:fail:nodescriptor 二、分析问题 这个提示有点问题,应该是该Characteristic的Descriptor有问题,而不能说nodescriptor。 …

docker-file 网络

docker挂载 1.绑定挂载(Bind Mounts):绑定挂载是将主机上的文件或目录挂载到容器中。 docker run -v /host/path:/container/path image_name 2.卷挂载(Volume Mounts):卷挂载将 Docker 数据卷挂载到容器中…

Java开发大厂面试第23讲:说一下 JVM 的内存布局和运行原理?

JVM(Java Virtual Machine,Java 虚拟机)顾名思义就是用来执行 Java 程序的“虚拟主机”,实际的工作是将编译的 class 代码(字节码)翻译成底层操作系统可以运行的机器码并且进行调用执行,这也是 …

使用delphi11编写一个基于xls作为数据库的照片展示程序

1、创建xls文档可以参考前一篇博客,并使用wps将文档保存为2003格式xls后缀。 2、在form上面放置adoconnection、adotable、datasource、spinedit、timer、checkbox、image、4个button组件。 image的设置: Image1.Align : alClient; Image1.Center : Tr…

三台泵恒压供水站电控系统及PLC程序设计实例

本文由艺捷自动化编写,其旗下产品有艺捷自动化网站和易为二维码说明书小程序(微信) 本文以一个具体的项目案例,来讲述一个恒压供水站的电控柜设计过程。包括用户需求,材料选型,图纸设计,柜内布…

Manjaro linux install RedisGUI (RedisInsight)亲测2024-5-25

Arch 用户仓库(Arch User Repository)(AUR) 是用户选择 基于 Arch Linux 的系统 的一个主要理由。你可以在 AUR 中访问到大量的附加软件。 (LCTT 译注:AUR 中的 PKGBUILD 均为用户上传且未经审核,使用者需要自负责任,在构建软件包前请注意检…

ubuntu 源码安装 cloudcompare

1.系统环境: ubuntu18 cmake:3.10.2 官方安装指导:https://github.com/CloudCompare/CloudCompare/blob/master/BUILD.md (注:查看cmake版本: cmake --version) 2.安装依赖 sudo apt-get update sudo apt-get insta…

【Numpy】深入解析numpy中的ravel方法

NumPy中的ravel方法:一维化数组的艺术 🌈 欢迎莅临我的个人主页👈这里是我深耕Python编程、机器学习和自然语言处理(NLP)领域,并乐于分享知识与经验的小天地!🎇 🎓 博主简…

Linux修炼之路之自动化构建工具,进度条,gdb调试器

目录 一:自动化构建工具make/makefile 生成内容: 清理内容: 对于多过程的: 对于多次make: 特殊符号: 二:小程序之进度条 三:git的简单介绍 四:Linux调试器gdb 接…

Centos7静态路由和动态路由

路由,即路由选择(Routing),是指在计算机网络中选择数据传输路径的过程。路由器(Router)是执行路由选择功能的网络设备。路由的主要目的是在复杂的网络结构中,选择最佳路径将数据包从源节点传递到…

kubectl

陈述式资源管理方法 kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用apiserver的接口 kubectl 是官方的CLI命令行工具,用于与apiserver进行通信,将用户在命令行输入的命令,组织转换成apiserver能识别的信息,进而实现…

在某云服务器上搭建公网kali linux2.0

前提: 可用的 CVM 实例 挂载一个系统盘之外的盘,安装完成后可卸载! 创建实例,安装centos7系统! 然后执行fdisk -l看磁盘的情况 在这里我将把镜像写入vdb这块数据盘 非 root 的情况下记得sudo执行以下命令 注意&…

【综合类型第 39 篇】《我的创作纪念日》成为创作者的第2048天

这是【综合类型第 39 篇】,如果觉得有用的话,欢迎关注专栏。 前言 无意间看了一眼CSDN的私信,提示我 Allen Su ,不知不觉今天已经是你成为创作者的 第2048天 啦,为了纪念这一天,我们为您准备了一份专属小…

51-53 DriveWorld:通过自动驾驶世界模型进行 4D 预训练场景理解 (含模型数据流梳理)

24年5月,北京大学、国防创新研究院无人系统技术研究中心、中国电信人工智能研究院联合发布了DriveWorld: 4D Pre-trained Scene Understanding via World Models for Autonomous Driving。 DriveWorld在UniAD的基础上又有所成长,提升了自动驾驶目标检测…

Java方法的基本用法

Java方法的基本用法 前言一、什么是方法方法存在的意义示例 二、方法定义语法基本语法代码示例注意事项 三、方法调用的执行过程基本规则代码示例计算两个整数相加计算 1! 2! 3! 4! 5! 四、实参和形参的关系代码示例交换两个整型变量原因分析解决办法 五、没有返回值的方法…

如果有多个文件夹,怎么快速获得文件夹的名字呢

上一篇写到怎么批量建立文件夹,那么怎么获取批量文件夹的名字呢? 一、啊这,这真是一个好问题二、这个得用Python(文本末尾有打包程序,点击链接运行就可以了)(1)首先建立一个py文件&a…

类的组合、作用域与可见性、类的静态成员、单例模式、

类的组合 一个类内嵌其他类的对象作为成员的情况 has - a组合 初始化列表的另一用途:为了调用数据成员的带参构造函数 能够层层递进 class Line { public:Line(int x1 0, int y1 0, int x2 0, int y2 0);Line(const Line &other);~Line();Line(const Po…