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

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

Singleton Pattern

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

dart 复制代码
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 构造函数特性来实现。

dart 复制代码
class Singleton {
  static final Singleton _instance = Singleton._internal();

  factory Singleton() {
    return _instance;
  }

  Singleton._internal();
}

// 调用
// Singleton();

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

dart 复制代码
class Singleton {
  Singleton._();

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

// 调用
// Singleton.instance;

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

其它的实现方式

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

dart 复制代码
abstract class AppNavigator {
  const AppNavigator();
  void push();
}

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

线程安全

我们知道 Dart 可以说是一种单线程编程语言,代码的执行通常发生在一个单线程上。这个单线程模型是通过事件循环来管理的。事件循环负责处理事件队列中的任务,这些任务包括 I/O 操作、定时器回调、用户输入等。

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

单例模式与 SOLID 原则

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

dart 复制代码
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 中如果想要拓展以支持不同的日志目标,如将日志写入文件等,不得不修改现有代码,而不是通过继承或组合进行扩展功能。

dart 复制代码
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

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

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

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

dart 复制代码
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 开发过程中有所帮助,感谢您的阅读!

相关推荐
BG9 小时前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng13 小时前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
数据智能老司机13 小时前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
恋猫de小郭14 小时前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯14 小时前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
数据智能老司机14 小时前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
w_y_fan17 小时前
Flutter 滚动组件总结
前端·flutter
烛阴17 小时前
【TS 设计模式完全指南】懒加载、缓存与权限控制:代理模式在 TypeScript 中的三大妙用
javascript·设计模式·typescript
醉过才知酒浓18 小时前
Flutter Getx 的页面传参
flutter
李广坤18 小时前
工厂模式
设计模式