Flutter 中的 SOLID 原则实用指南:D — 依赖倒置原则(DIP)

187 阅读3分钟

在 SOLID 原则中,“D” 指的是 依赖倒置原则(Dependency Inversion Principle,DIP),它倡导:

高层模块不应依赖低层模块,二者都应该依赖抽象。抽象不应依赖细节,细节应依赖抽象。

换句话说,我们不应该在高层逻辑中直接依赖具体实现,而应依赖接口或抽象类。这可以大幅提升代码的可扩展性、可测试性和模块解耦能力。

🚫 常见反例:直接依赖具体实现

我们以一个“文章服务”为例,展示 Flutter 中常见的错误做法:

class ArticleService {
  Future<List<String>> fetchArticles() async {
    // 模拟从网络获取文章
    await Future.delayed(Duration(seconds: 1));
    return ['文章 A', '文章 B'];
  }
}

class HomePage extends StatelessWidget {
  final service = ArticleService(); // 👈 错误:UI 层直接依赖具体类

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: service.fetchArticles(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }
        return ListView(
          children: snapshot.data!.map((e) => Text(e)).toList(),
        );
      },
    );
  }
}

😵 问题点分析:

  • UI 直接依赖具体类 ArticleService,导致 耦合严重,难以测试或替换
  • 如果将来需要改为从本地缓存读取文章或接入 GraphQL,需要改动 UI。
  • 违背了依赖倒置原则,造成模块层级混乱。

✅ 正确做法:依赖抽象接口

1. 抽象定义(domain/article_repository.dart)

abstract class ArticleRepository {
  Future<List<String>> fetchArticles();
}

2. 实现类(infrastructure/remote_article_repository.dart)

class RemoteArticleRepository implements ArticleRepository {
  @override
  Future<List<String>> fetchArticles() async {
    await Future.delayed(Duration(seconds: 1));
    return ['文章 A', '文章 B'];
  }
}

3. UI 依赖抽象(application/home_controller.dart)

class HomeController {
  final ArticleRepository repository;

  HomeController(this.repository);

  Future<List<String>> getArticles() => repository.fetchArticles();
}

🚀 结合 Riverpod 实现依赖倒置

我们通过 Riverpod 注入 抽象接口绑定的实现类,完成解耦与依赖管理:

1. Provider 注入(infrastructure/article_provider.dart)

final articleRepositoryProvider = Provider<ArticleRepository>((ref) {
  return RemoteArticleRepository(); // 可轻松替换为 Mock 或其他实现
});

final homeControllerProvider = Provider<HomeController>((ref) {
  final repository = ref.read(articleRepositoryProvider);
  return HomeController(repository);
});

2. UI 使用(presentation/home_page.dart)

class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final controller = ref.read(homeControllerProvider);

    return FutureBuilder<List<String>>(
      future: controller.getArticles(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) return CircularProgressIndicator();
        return ListView(
          children: snapshot.data!.map((e) => Text(e)).toList(),
        );
      },
    );
  }
}

✅ 优势:

  • UI 完全解耦,只关注 HomeController
  • 将来的实现可以轻松替换为缓存、本地数据库、Fake 模拟等。
  • 可单元测试、集成测试,依赖由外部注入。

🧪 轻松测试你的业务逻辑

class FakeArticleRepository implements ArticleRepository {
  @override
  Future<List<String>> fetchArticles() async => ['测试文章 A', '测试文章 B'];
}

void main() {
  test('HomeController fetches articles', () async {
    final controller = HomeController(FakeArticleRepository());
    final articles = await controller.getArticles();
    expect(articles.length, 2);
  });
}

📌 总结:依赖倒置的意义与实践

依赖倒置原则的目标是降低模块之间的耦合,让高层逻辑更稳定、底层实现更灵活。在实际开发中,我们应牢记以下几点:

🎯 实战重点

场景DIP 应用方式
需要替换网络服务实现依赖抽象 ArticleRepository,替换不动上层逻辑
测试时避免请求真实网络使用 FakeRepository 注入控制结果
组件间解耦通过 Provider 注入抽象而非直接创建类实例
多平台(Web / App)支持差异逻辑抽象接口 + 不同平台实现类,运行时自由切换

💬 一句话总结:

DIP 不是“强行抽象”,而是“依赖更稳定的抽象,释放更灵活的实现”

用抽象隔离变动,用 Provider 管理依赖。写出更优雅、测试友好、可扩展的 Flutter 代码,从依赖倒置原则开始。

如果你觉得这篇文章对你有帮助,欢迎点个 “在看”,或转发给身边的 Flutter 同学,一起写出更优雅、可维护的代码!