6.3. 让函数定义它们所需的行为

假设我需要编写一个将 Document 结构保存到磁盘的函数的任务。

  1. // Save writes the contents of doc to the file f.
  2. func Save(f *os.File, doc *Document) error

我可以指定这个函数 Save,它将 *os.File 作为写入 Document 的目标。但这样做会有一些问题

Save 的签名排除了将数据写入网络位置的选项。假设网络存储可能在以后成为需求,则此功能的签名必须改变,从而影响其所有调用者。

Save 测试起来也很麻烦,因为它直接操作磁盘上的文件。因此,为了验证其操作,测试时必须在写入文件后再读取该文件的内容。

而且我必须确保 f 被写入临时位置并且随后要将其删除。

*os.File 还定义了许多与 Save 无关的方法,比如读取目录并检查路径是否是符号链接。 如果 Save 函数的签名只用 *os.File 的相关内容,那将会很有用。

我们能做什么 ?

  1. // Save writes the contents of doc to the supplied
  2. // ReadWriterCloser.
  3. func Save(rwc io.ReadWriteCloser, doc *Document) error

使用 io.ReadWriteCloser,我们可以应用接口隔离原则来重新定义 Save 以获取更通用文件形式。

通过此更改,任何实现 io.ReadWriteCloser 接口的类型都可以替换以前的 *os.File

这使 Save 在其应用程序中更广泛,并向 Save 的调用者阐明 *os.File 类型的哪些方法与其操作有关。

而且,Save 的作者也不可以在 *os.File 上调用那些不相关的方法,因为它隐藏在 io.ReadWriteCloser 接口后面。

但我们可以进一步采用接口隔离原则

首先,如果 Save 遵循单一功能原则,它不可能读取它刚刚写入的文件来验证其内容 - 这应该是另一段代码的功能。

  1. // Save writes the contents of doc to the supplied
  2. // WriteCloser.
  3. func Save(wc io.WriteCloser, doc *Document) error

因此,我们可以将我们传递给 Save 的接口的规范缩小到只写和关闭。

其次,通过向 Save 提供一个关闭其流的机制,使其看起来仍然像一个文件,这就提出了在什么情况下关闭 wc 的问题。

可能 Save 会无条件地调用 Close,或者在成功的情况下调用 Close

这给 Save 的调用者带来了问题,因为它可能希望在写入文档后将其他数据写入流。

  1. // Save writes the contents of doc to the supplied
  2. // Writer.
  3. func Save(w io.Writer, doc *Document) error

一个更好的解决方案是重新定义 Save 仅使用 io.Writer,它只负责将数据写入流。

接口隔离原则应用于我们的 Save 功能,同时, 就需求而言, 得出了最具体的一个函数 - 它只需要一个可写的东西 - 并且它的功能最通用,现在我们可以使用 Save 将我们的数据保存到实现 io.Writer 的任何事物中。

[译注: 不理解设计原则部分的同学可以阅读 Dave 大神的另一篇《Go 语言 SOLID 设计》]