Qinject

一个快速、灵活的 IoC 库,用于 Dart 和 Flutter

Qinject 帮助您轻松地使用 DI(依赖注入)和 Service Locator(服务定位器)基于 IoC(控制反转)的方法来开发应用程序,或者经常是两者的结合。

Makefile CI

主要特点

  • 支持 DI(依赖注入)
  • 支持 Service Locator(服务定位器)
  • 隐式依赖链解析;无需定义和维护已注册依赖项之间的“依赖于”关系
  • 简单但极其灵活的依赖注册和解析机制
  • 将任何类型注册为依赖项,从 Flutter 小部件和函数到简单的类
  • 简单但功能强大的单元测试工具

安装

Qinject 包可在 pub.dev/packages/qinject 上找到

可以使用以下方式安装

  dart pub add qinject

或者

  flutter pub add qinject

快速入门

以下示例展示了一个使用 DIService Locator 模式以及 SingletonResolver 依赖项注册的基础应用程序。这些主题将在本文档后面更详细地介绍。

注意: 在 `./example` 目录中提供了一个功能齐全的演示应用程序。可以通过在仓库根目录执行 make example 来运行它。

main() {
  // Use the Qinject instance to support DI by passing it as a constructor
  // argument to classes defining dependencies
  final qinjector = Qinject.instance();

  Qinject.registerSingleton(() => stdout);
  Qinject.register((_) =>
      DateTimeReader()); // Note the type argument is ignored as the resolver does not use it and the type arguments required by `register` are omitted due to dart's type inference
  Qinject.register((_) => Greeter(qinjector));

  Qinject.use<void, Greeter>().greet(); // Resolve a Greeter instance and invoke its greet method
}

class Greeter {
  // Below the service locator style of resolution is used. This may be
  // preferable for some trivial dependencies even when DI is being used elsewhere
  final Stdout _stdout = Qinject.use<Greeter, Stdout>();

  // Here the DI style of dependency resolution is used. This is typically
  // more flexible when writing unit tests
  final DateTimeReader _dateTimeReader;

  Greeter(Qinjector qinjector)
      : _dateTimeReader = qinjector.use<Greeter, DateTimeReader>();

  void greet() => _stdout.writeln("Right now it's ${_dateTimeReader.now()}");
}

class DateTimeReader {
  DateTime now() => DateTime.now();
}

依赖注册

有两种高级别的依赖注册类型;Singleton(单例)和 Resolver(解析器)。Resolver 在专门的 Resolvers 部分进行了详细介绍。Singleton 在首次解析时进行一次评估,然后为应用程序的整个生命周期返回相同的实例。

单例注册如下所示

  Qinject.registerSingleton(() => MyClass());

当第一次调用 use<MyClass>() 时,将返回 MyClass 的一个新实例。当后续调用 use<MyClass>() 时,将返回相同的、原始实例;在应用程序的整个生命周期内。

依赖项使用

任何类型的依赖项都使用相同的方法解析:use<TConsumer, TDependency>。可以从 Qinject 服务定位器或注入到类中的 Qinjector 实例调用此方法。这两种方法都可以互换使用,因为任何已注册的依赖项都可以通过任一途径访问。

使用服务定位器

Service Locator(服务定位器)是一种解耦依赖项的简单模式。它的使用基于一个全局可访问的 Service Locator 实例;在 Qinject 的情况下,它是 Qinject 类型本身,它公开了一个静态的 use<TConsumer, TDependency> 方法。

可以从任何可以访问 Qinject 类型的任何地方以以下方式解析依赖项

    final DependencyType _dependency = Qinject.use<ConsumerType, DependencyType>();

某些复杂的应用程序在使用 Service Locator 解耦依赖项时可能会变得难以测试。特别是当涉及到并发执行的逻辑时。这是因为单一的全局实例需要配置适合给定测试运行中所有测试的测试替身,或者至少需要重复配置和清理。对于此类应用程序,DI 可能是更好的选择,并且 Service Locator 可能不被使用,或者仅保留用于微不足道的依赖项,例如 快速入门 示例中所示的 stdout

然而,对于许多应用程序而言,Service Locator 是一种经过时间考验的、简单而有效的选择。

使用 DI(依赖注入)

采用 DI 时,类将它们的依赖项声明为变量,通常是某种抽象类型,并在其构造函数中(通常)公开这些变量的 setter。然后,由一个外部实体来设置这些变量,该实体负责选择、创建和管理这些抽象依赖项类型的具体实现。

如果 naively(朴素地)执行此操作,很快就会变成一个非常复杂的依赖关系图来管理,因此大多数采用此方法的项目都会使用 IoC 库或框架来管理这种复杂性。这就是 QinjectDartFlutter 项目提供的。

然而,Qinject 在这方面比其他语言中的可比框架采取了略微不同的方法,它通过将一个抽象的 Service Locator 实例注入构造函数来实现。然后,构造函数立即使用该实例来获取任何依赖项。

下面的示例说明了如何在构造函数中声明依赖项,并通过注入的 Qinjector 实例进行填充。

main() {
    // Access the Qinjector instance
    final qinjector = Qinject.instance();

    Qinject.register((_) => ConsumerClass(qinjector)); // Note the ConsumerClass can be registered before its dependencies
    Qinject.register((_) => DependencyA(qinjector)); // Note that DependencyA requires a Qinjector also. We hide this from ConsumerClass by providing it in the Resolver closure; this is purely for brevity and clarity however
    Qinject.register((_) => DependencyB());
    Qinject.register((_) => DependencyC(qinjector));
    
    final consumerClass = Qinject.use<void, ConsumerClass>(); // Resolve an instance of ConsumerClass

    // do something with consumerClass
}


class ConsumerClass {
  final DependencyA _dependencyA;
  final DependencyB _dependencyB;
  final DependencyC _dependencyC;

  ConsumerClass(Qinjector qinjector)
      : _dependencyA = qinjector.use<ConsumerClass, DependencyA>(),
        _dependencyB = qinjector.use<ConsumerClass, DependencyB>(),
        _dependencyC = qinjector.use<ConsumerClass, DependencyC>();

  // Do something with dependencies
}

当使用 DI 解耦依赖项时,复杂的应用程序可以更容易地进行测试。在 Qinject 中,这是因为表示依赖项解析机制的接口被传递到每个消费类中。在测试场景中,这可以被一个专门为测试创建的 Qinjector 实现所替换,并且该实现仅限于测试范围。这可以防止一个测试的配置泄露到另一个测试中,并防止依赖关系围绕测试执行的顺序而变得脆弱。这些是细微的好处,但在大型项目中它们通常会带来回报。

使用 Qinjector 进行单元测试

可以使用 TestQinjector 实例来注册 Test Doubles(测试替身)作为依赖项,以辅助单元测试。然后,可以将 TestQinjector 实例传递给依赖项消费者,而不是从 Qinject.instance() 返回的默认 Qinjector 实例。

例如,如果测试上面的示例 Qinjector 应用程序 如上所示,则可能定义以下内容

main() {
    test('ConsumerClass behaves as expected', () {
        // Create a TestQinjector instance that implements the Qinjector interface
        final qinjector = TestQinjector();

        // Register stubs or mocks as required against the TestQinjector instance
        qinjector.registerTestDouble<DependencyA>((_) => DependencyAStub()); // Note the TDependency type argument is set explictly here to DependencyA otherwise Dart's type inference would cause the registration to be assigned to the type DependencyAStub and then dependency resolution would fail in ConsumerClass
        qinjector.registerTestDouble<DependencyB>((_) => DependencyBMock()); 
        qinjector.registerTestDouble<DependencyC>((_) => DependencyCStub());
        
        final consumerClass = ConsumerClass(qinjector); // Create instance of ConsumerClass using the TestQInjector

        // do some assertions against consumerClass
    }
}

解析器

依赖项通过将 Resolver(解析器)委托分配给一个类型来注册,该类型标记为 TDependencyResolver 委托是任何接受单个 Type 参数并返回 TDependency 实例的函数。

这采取以下形式

    Qinject.register<TDependency>(TDependency Function (Type consumer));

TDependency 的类型可以是类型系统识别的任何 Type;不仅仅是类。例如,函数经常被注册为 Factory Resolvers(工厂解析器)。

Resolver 委托不在注册时调用。因此,TDependency 所需的任何依赖项不必在 Qinject 中注册。成功解析的唯一要求是,给定 TDependency 的完整依赖关系图在调用 use<TConsumer, TDependency>() 来解析该特定 TDependency 之前,以任何顺序进行注册。

在依赖项解析过程中,Type 参数通常可以被忽略;它仅用于允许根据需要将同一接口的不同实现返回给不同的使用者。传递给 Type 的参数是在调用 use<TConsumer, TDependency> 时作为 TConsumer 传递的类型,该调用导致 Resolver 委托被调用。这通常设置为使用者本身的 Type,但是它可以是任何 type

在没有有意义的包含 Type 的情况,或者它不相关的情况下,可以传递 void

重要的是要记住,只有一种基本类型的 Resolver;任何满足 Resolver 签名的函数。这意味着您的依赖项解析过程和生命周期管理可以像您的需求一样简单或复杂。但是,以下部分描述了一些常见的 Resolver 形式。

瞬时解析器

Transient Resolver(瞬时解析器)是最简单的 Resolver 形式。它如下所示

    Qinject.register((_) => MyClass()); // note the Type argument is ignored with _ as it is not used in this example (though it could be required if the resolution process)

每当调用 use<MyClass>() 时,都会返回 MyClass 的一个新实例。

类型敏感解析器

Type Sensitive Resolver(类型敏感解析器)根据作为 consumer 参数传递的类型返回接口的不同实现。

      Qinject.register((Type consumer) => consumer.runtimeType == TypeA
      ? ImplementationA()
      : ImplementationB());

每当调用 use<TConsumer, TypeA>() 时,都会返回 ImplementationA 的一个新实例。当调用 use<TConsumer, NotTypeA>() 时,其中 NotTypeA 如其名称所示,是除 TypeA 以外的任何类型,将返回 ImplementationB 的一个新实例。

请注意,ImplementationAImplementationB 都必须实现相同的接口。

工厂解析器

Factory Resolver(工厂解析器)返回一个工厂函数而不是依赖项实例。这通常用于具有运行时可变构造函数参数的类,例如 Flutter Widgets

    Qinject.register((_) => (String variableArg) => MyClass(variableArg));

每当调用 use<TConsumer, MyClass Function(String)>() 时,就会返回一个接受 String 参数并返回 MyClass 实例的函数。该函数通常会被分配给使用者中的一个变量,并使用不同的参数反复调用;就像可以在父 Widget 中多次调用 Flutter Widget 构造函数一样。

例如

Qinject.register((_) => (String msg) => MessageWidget(msg)); // Register the MessageWidget Factory Resolver

class ConsumerWidget extends StatelessWidget {
  final MessageWidget Function(String) _messageWidget; // The ConsumerWidget has a dependency on the MessageWidget expressed as a Factory Function

  ConsumerWidget(Qinjector qinjector, {Key? key})
      : _messageWidget =
            // Resolve the MessageWidget dependency using Qinjector
            qinjector.use<ConsumerWidget, MessageWidget Function(String)>(), // 
        super(key: key);

  @override
  Widget build(BuildContext context) => {
     // Use the _messageWidget factory function as many times as required, in place of the MessageWidget constructor
     _messageWidget("Hello you!"); 
     _messageWidget("Hello World!");
     _messageWidget("Hello Universe!");
}

class MessageWidget extends StatelessWidget {
  final String _message;

  const MessageWidget(this._message, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => Text(_message);
}

缓存解析器

Cached Resolver(缓存解析器)在定义的时间段内返回 TDependency 的相同实例,然后在刷新它并使用新实例用于下一个定义的时间段,无限循环。这种形式的 ResolverQinject 依赖项解析方法简单、灵活性质的一个很好的例子。

  // In registration section of app
  var cachedDataTimeStamp = DateTime.now();
  var cachedData = populateCachedData();

  Qinject.register((_) {
    if (DateTime.now().difference(cachedDataTimeStamp).inSeconds > 60) {
      var cachedDataTimeStamp = DateTime.now();
      var cachedData = populateCachedData();
    }

    return cachedData;
  });

  // Elsewhere in main app codebase
  CachedData populateCachedData() {
    // omitted for brevity; maybe fetched from a network service or local db
    CachedData();
  }

  class CachedData {
    // omitted for brevity; would house various data attributes
  }

日志记录与诊断

默认情况下,Qinject 将可能相关活动的记录记录到 console。在某些情况下,这可能需要被覆盖以重定向日志或对它们应用某种预处理。

可以通过将 Quinject.log 字段设置为自定义日志记录委托来实现这一点。

以下示例将日志路由到 Flutter 的 debugPrint

  Qinject.log = (message) => debugPrint(message);

贡献

欢迎提交拉取请求,特别是对于任何 bug 修复或效率改进。

API 更改将得到考虑,但是,虽然可以轻松想象许多快捷方式或辅助方法,但目标是保持 Qinject 轻便、简单且灵活。因此,此类更改可能会被拒绝,但不会带有与贡献相关的任何负面判断。

GitHub

查看 Github