Flutter 中的依赖注入
在本文中,我将尝试教你它是什么、如何操作以及为什么要这么做,并提供示例和GitHub 仓库的链接,你可以在其中查看代码并亲自尝试。现在,继续。
根据维基百科:
在软件工程中,依赖注入是一种设计模式,即一个对象或函数接收其依赖的其他对象或函数。依赖注入是一种控制反转的形式,旨在分离对象的构造和使用,从而实现松耦合的程序。该模式确保想要使用给定服务的对象或函数无需知道如何构造这些服务。相反,接收方“客户端”(对象或函数)通过外部代码(“注入器”)获取其依赖项,而它对此并不知情。
换句话说:
- 不是在类或方法内部创建对象,而是从外部“注入”对象;
- 类不需要知道如何创建它所依赖的对象,它只需要知道如何使用它们;
- 这会生成更易于测试且更易于维护的代码。
就像生活中的任何事物一样,DI 也有一些优点和缺点。
优点:
- 使您的代码更易于测试,因为您只需在类中注入模拟即可;
- 使您的代码更易于维护,因为可以对注入对象的实现进行更改,而不会影响依赖于它们的类或方法。
缺点:
- DI 会增加项目的复杂性,尤其是如果操作不当的话;
- 注入依赖项可能会带来性能开销;
- 如果依赖项没有得到正确管理或注入, DI 可能会引入运行时错误,例如空指针异常。
汽车示例
那么,让我们从一些代码开始。假设你有一个 Car 类,它有一个 Engine。
class Car {
Engine? engine;
const Car();
void start() {
engine.start(); // Null reference exception
}
}
为了使其Car
工作,您需要一个可以工作的Engine
。然而,这是另一个类,它具有许多复杂性和其他与汽车本身无关的要求。
遵循依赖注入的原则,我们可以这样做:
构造函数注入
依赖项通过其构造函数传递给类。
这种模式明确了类需要哪些依赖项才能运行,并确保依赖项在类创建后即可使用。
如果我们在Car
类中实现构造函数注入:
class Car {
final Engine engine;
const Car(this.engine);
void start() {
engine.start(); // engine is not null!
}
}
由于Car.engine
是final
并且也是构造中必需的,我们确保它永远不会为空。
void main() {
final engine = Engine();
final car = Car(engine);
car.start();
}
添加更多部件
现在,假设你是一家汽车制造商,正在生产汽车零部件。由于汽车不仅仅由发动机组成,因此现在有这样的类结构:
请注意,我不是汽车制造商,而且这并不是汽车所需的全部零件。
class Car {
final Engine engine;
final List<Wheel> wheels;
final List<Door> doors;
final List<Window> windows;
Car(this.engine, this.wheels, this.doors, this.windows);
void start() {
engine.start();
}
void rollDownAllWindows() {
for (var w in windows) {
w.rollDown();
}
}
void openAllDors() {
for (var d in doors) {
d.open();
}
}
// ...
}
由于引擎final
必须通过构造函数传递,所以除非你提供一个可以运行的引擎,否则该类无法编译。如果你的引擎没有运行,你的门就无法工作,这毫无道理。
Car
使用构造注入方法,只有在完成所有部分后才能拥有一个实例,并且不能拥有“不完整”的实例Car
。
Setter 注入
通过 setter 方法在类上设置依赖关系。
这种模式允许更大的灵活性,因为可以在创建类之后设置或更改依赖关系。
只要你有一个 实例Car
,就可以使用它setEngine
来为汽车设置引擎。这解决了之前的问题,我们现在可以拥有 ,Car
然后再为其设置引擎。
class Car {
Engine? engine;
List<Wheel> wheels;
List<Door> doors;
List<Window> windows;
Car(this.wheels, this.doors, this.windows, {this.engine});
void setEngine(Engine newEngine) {
engine = newEngine;
}
void start() {
engine?.start();
}
// ...
}
现在,您只需在setEngine
引擎准备好安装到汽车上时调用即可。您还必须添加一些验证,以确保代码中不会出现运行时错误。有关如何正确预防这些问题的更多信息,请参阅Dart 中的空值安全。
其他类型的依赖注入
本例中不会涉及这些其他类型,因此这些只是介绍。
接口注入
该类实现了一个接口,该接口定义了注入依赖项的方法。
这种模式允许代码更加抽象和解耦,因为类不必依赖于接口的特定实现。
环境背景
您可能熟悉提供程序 pub 包
共享上下文用于向需要它们的类提供依赖关系。
当多个类需要访问相同依赖项时,此模式非常有用。
服务定位器
您可能熟悉get_it pub 包。
中央注册表用于管理并向需要它们的类提供依赖关系。
这种模式可以更容易地管理大型应用程序中的依赖关系,但它也可能使代码更加复杂且难以测试。
好的,但是为什么呢?
在我的一个项目中,我需要创建一个身份验证层,以便我的用户可以创建帐户并进行自我身份验证。
由于我仍在决定使用哪一个 - 因为它需要免费且易于扩展 - 我创建了一个依赖注入结构,以便我可以在任何时候轻松地交换以测试另一个身份验证服务。
这是我得到的结构:
class AuthenticationRepository {
final AuthenticationProvider provider;
AuthenticationRepository(this.provider);
Future<UserSession?> signIn(String email, String password) {
return provider.signIn(email, password).then((session) {
if (session != null) {
return session;
}
throw 'Failed to authenticate';
}).catchError((error) {
throw error;
});
}
// ...
}
此类有一个方法signIn
,它接收用户的email
和password
,然后将其传递给相应的提供程序。它还返回一个UserSession
类,负责存储当前用户的数据和身份验证令牌。
class UserSession {
final String username;
final String email;
UserSession({
required this.username,
required this.email,
});
String get sessionToken => "";
}
注意AuthenticationRepository.provider
。它是 类的一个实例AuthenticationProvider
。配置如下:
abstract class AuthenticationProvider {
Future<UserSession?> signIn(String email, String password);
}
由于此类是抽象的,为了创建一个真正起作用的存储库,您需要给它一个实现。
因此我创建了两个类:FirebaseProvider
和CognitoProvider
。这两个类分别负责使用 Firebase 和 Cognito 的 API 管理用户身份验证。
有一个用于 Firebase 集成的pub 包,还有一个用于 Cognito 集成的pub 包。
然而,这些包并不能无缝地融入AuthenticationProvider
本例中展示的抽象类中。
现在,为了进行身份验证,我们只需要决定使用哪一个。假设你把你的AuthenticationRepository
信息存储在一个服务定位器中,比如GetIt:
// setting up
GetIt.instance.registerSingleton<AuthenticationRepository>(AuthenticationRepository(CognitoProvider());
// authenticating an user
final auth = GetIt.instance<AuthenticationRepository>();
auth.signIn(email, password);
测试示例
为了展示如何使用 DI 轻松地进行更好的测试和模拟类,这里有一个MockAuthenticationProvider
可以在 上进行测试的示例AuthenticationRepository
。
您可以先创建模拟提供程序:
class MockAuthenticationProvider implements AuthenticationProvider {
static String successPassword = "123";
UserSession? userSession;
MockAuthenticationProvider({this.userSession});
@override
Future<UserSession?> signIn(String email, String password) {
if (password == successPassword) {
return Future.value(userSession);
} else {
return Future.value(null);
}
}
}
请注意,上面的类有一个静态successPassword
属性。这是为了让我们能够实现成功和失败的方法,但这并非必需。您可以随意实现任何您想要的逻辑。
现在您可以创建模拟工厂:
AuthenticationRepository mockRepository() {
final mockUserSession = UserSession(
username: "mock",
email: "mock@mail.com",
sessionToken: "token",
);
final mockProvider = MockAuthenticationProvider(userSession: mockUserSession);
return AuthenticationRepository(mockProvider);
}
通过使用它AuthenticationRepository
,我们可以轻松测试其方法,而无需与 Cognito 或 Firebase 集成。以下是一个成功的单元测试示例:
test('Should return a valid UserSession', () async {
final repo = mockRepository();
final result = await repo.signIn(
"email", MockAuthenticationProvider.successPassword);
assert(result.sessionToken != null);
});
请注意,我们正在尝试使用"email"
and进行登录MockAuthenticationProvider.successPassword
,这是一种强制提供商返回 的方法UserSession
。
现在,测试故障:
test('Should throw if UserSession comes null from provider', () async {
final repo = mockRepository();
try {
await repo
.signIn("email", "incorrect password")
.then((userSession) {
fail("Should throw an exception");
});
} catch (error) {
assert(error.toString() == "Failed to authenticate");
}
});
结束
就是这样!
感谢您读完本文。这是我在 dev.to 上的第一篇文章,欢迎您留下任何反馈。
再次附上源代码。欢迎在下方提出问题或评论。
再见!
文章来源:https://dev.to/alvbarros/dependency-injection-in-flutter-598k