耦合通常与内聚相对照。低耦合往往与高内聚相关,反之亦然。低耦合通常是结构良好的计算机系统和良好设计的标志,当它与高内聚相结合时,有助于实现高可读性和高可维护性的总体目标。本案例研究旨在展示低耦合和高内聚的好处,以及如何在 C++ 中实现它们。案例研究的内容是设计一个从文件读取数据、对其进行处理、并将结果写入输出文件的应用程序。
一个设计不佳的方案
在第一个设计中,只使用了一个名为CDataProcessor的类来完成:
- 从文件获取数据。
- 处理数据。
- 打印结果。
而 main 方法则调用这个类的方法。
该方案的缺点
- 低内聚:
CDataProcessor类承担了太多职责,所以我们无法轻易地将算法重用到其他应用程序中。 - 高耦合:处理逻辑与控制台和数据提供方紧密耦合。
设计重构
高内聚
为了提高内聚性,每个职责应该分配给不同的类,所以我们需要三个类:
CFileProvider:从文件获取数据。CDataProcessing:处理数据。这个类可以使用其他类来执行处理,但为了让设计保持简单,在我们的示例中认为它一个类就足够了。CResultReporting:将结果写入文件。
现在每个类都只有一个职责。这种设计的优点是:
- 类更容易理解。
- 代码更容易维护。
- 类更容易在其他应用程序中重用。
低耦合
如果数据存储在数据库而不是文件中,会发生什么?在我们之前的设计中,应用程序与文件提供方紧密耦合。
为了解决这个问题,我们需要一个接口,提供从任何来源检索数据的方法。对于基于文件的数据,我们再需要一个实现该接口的类。
为此,使用NVI(非虚接口)模式是一个不错的解决方案。这个模式比只使用抽象类更有用,因为它可以定义前置条件和后置条件。这是一种实用的面向对象编程技术,在开发过程中尤其有用。前置条件和后置条件确保类层次结构(以及一般而言的抽象)的不变量在程序执行的指定位置不被违反。
在我们的案例中,我们可以添加IDataProvider接口。

并让CFileProvider继承自IDataProvider来实现GetDataFromImpl;同样的设计也可用于CDataProcessing和CReportResult以外的类也可以。
下面是重构后类之间的新协作关系:
类工厂
在修改后的设计中,IDataProvider、IDataProcessing和IReportResult的具体实例由 main 方法创建。更好的做法是把这一职责交给一个工厂类,从而将实例化所需对象族所需的逻辑隔离开来。
控制器
所有类之间的编排是在 main 方法中实现的。最好把这一职责交给一个控制器(Controller)类,这样它就可以在其他应用程序中重用。
控制器需要与三个类交互,所以问题是:我们如何把这些实例绑定到控制器?
我们可以用以下两种方法绑定这些实例:
- 添加一个名为
BindInstances(IDataProviderPtr,IDataProcesingPtr,IReportResultPtr)以外的类也可以。 - 将控制器定义为模板,它将像这样实例化:
CController<CFileProvider,CDataProcessor,CConsoleReport>两种解决方案的区别在于:对于第一种,每个数据提供方都必须继承自IDataProvider;而对于第二种,CFileProvider只需要拥有方法GetData(Data&),即使它继承自IDataProvider以外的类也可以。
关于该用 OOP 还是模板,C++ 专家们有许多讨论。这里有一篇不错的文章讨论了这两种方法之间的矛盾。
下面是重构后类之间的新协作关系:
重构的好处
重构之后,应用程序变得更加灵活,可以用于不同的场景:
- 它可以从文件、数据库、XML 文件、CSV 文件等获取数据……
- 它可以使用许多不同的类来处理数据,而不仅限于一个。
- 它可以将结果输出到控制台、文件等……
