Flutter 中的 SOLID 原则实用指南:I — 接口隔离原则(ISP)

107 阅读3分钟

在 SOLID 五大设计原则中,"I" 代表 接口隔离原则(Interface Segregation Principle,ISP),它的核心理念是:

不应该强迫客户端依赖它不使用的方法。

换句话说:

一个接口不应该包含太多职责,应该根据实际需要将其拆分为更小、更专一的接口。

这看似简单,却是很多 Flutter 项目后期变复杂、测试困难的根本原因。

🧠 一个贴合 Flutter 项目的例子

✅ 场景设定:

假设你正在开发一个笔记类 App(比如 Notion、MPost),你的数据存储模块中有一个统一的接口,用于支持不同来源的存储方式(本地 SQLite、远程 Firebase、Mock 等)。

你可能最初这样设计:

abstract class NoteRepository {
  Future<void> createNote(String content);
  Future<void> updateNote(int id, String content);
  Future<void> deleteNote(int id);
  Future<String> getNote(int id);
  Future<List<String>> listNotes();
  Future<void> syncWithCloud();
  Future<void> exportToMarkdown();
}

一切看起来还算整洁,但随着功能扩展 —— 问题就来了。

❌ 违反 ISP 的错误实践

比如现在你想写一个只用于“导出 Markdown”的工具类,它只需要 exportToMarkdown(),但是你被迫实现全部方法:

class MarkdownExporter implements NoteRepository {
  @override
  Future<void> exportToMarkdown() async {
    print('已导出 Markdown');
  }

  @override
  Future<void> createNote(String content) {
    throw UnimplementedError();
  }

  @override
  Future<void> updateNote(int id, String content) {
    throw UnimplementedError();
  }

  @override
  Future<void> deleteNote(int id) {
    throw UnimplementedError();
  }

  @override
  Future<String> getNote(int id) {
    throw UnimplementedError();
  }

  @override
  Future<List<String>> listNotes() {
    throw UnimplementedError();
  }

  @override
  Future<void> syncWithCloud() {
    throw UnimplementedError();
  }
}

这种写法明显违背了接口隔离原则。你被迫使依赖并实现了许多根本用不到的方法,不仅多余,还埋下了未来的维护隐患。

✅ 正确做法:接口拆分

我们可以根据功能将接口拆分成更细颗粒的职责单元:

abstract class NoteReadable {
  Future<String> getNote(int id);
  Future<List<String>> listNotes();
}

abstract class NoteWritable {
  Future<void> createNote(String content);
  Future<void> updateNote(int id, String content);
  Future<void> deleteNote(int id);
}

abstract class CloudSyncable {
  Future<void> syncWithCloud();
}

abstract class MarkdownExportable {
  Future<void> exportToMarkdown();
}

这样每个类只需实现自己真正关心的接口

class NoteDatabaseRepository implements NoteReadable, NoteWritable {
  // 实现本地数据库读写逻辑
}

class FirebaseSyncService implements CloudSyncable {
  @override
  Future<void> syncWithCloud() async {
    // 上传数据至 Firebase
  }
}

class MarkdownExporter implements MarkdownExportable {
  @override
  Future<void> exportToMarkdown() async {
    // 生成并导出 Markdown 文件
  }
}

🧩 搭配依赖注入更加清晰

比如你使用 Riverpod,可以清晰地注入具体职责:

final noteReaderProvider = Provider<NoteReadable>((ref) {
  return NoteDatabaseRepository();
});

final markdownExporterProvider = Provider<MarkdownExportable>((ref) {
  return MarkdownExporter();
});

调用方只依赖所需的接口即可:

final exporter = ref.read(markdownExporterProvider);
await exporter.exportToMarkdown();

这种方式下,模块之间的依赖关系变得非常清晰,测试替换也变得轻松自然。

✅ 总结:ISP 的核心思维

做法是否符合 ISP说明
大接口包含多个职责❌ 不符合客户端被迫依赖自己不使用的功能,增加维护成本
拆分接口按功能划分职责✅ 符合各模块只依赖自己需要的功能,代码更清晰、解耦性更强
配合依赖注入系统解耦调用层✅ 符合调用方不关心具体实现,只关注接口职责,提升可测试性

接口隔离原则真正落地的关键,是敢于将接口拆小、将职责分清,这样你的代码才会变得更加模块化、可测试、可维护。

📌 最后总结一句话:

与其设计一个“大而全”的胖接口,不如设计多个“小而美”的专属接口。

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