更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)
面向对象编程 (OOP) 的三个关键原则之一是 封装 :对象包含一种状态,当从外部观察时,该状态始终在内部保持一致。为了说明这一点,假设有一个表示矩形的类,并且该类跟踪矩形的尺寸及其面积:

Rectangle上面的类有两个字段来表示它的维度(width和height)和一个派生字段来表示它的area,将它们存储在对象本身中(有时候是因为它的计算成本)。
通过上面显示的公共接口(在非多线程环境中)使用这个类,可以保证 area始终与width和height保持同步。对象第一次构造时,所有字段都处于一致状态,如果resize()在后面调用,所有字段在方法调用前后也是一致的。
但这个实现中 并非 如此:在构造函数和 resize()方法中存在一个时间点,其中 的预计算值area是陈旧的。
添加错误处理
假设更新 Rectangle类实现,以确保width和height是正数而不是零。

从封装的角度来看,这里一切都很好。如果尝试用无效值实例化 Rectangle并使用它,如下所示:

...构造函数将抛出异常,该对象将永远不存在,并且同一范围内该行之后的任何代码都不会运行。不可能从 r访问无效值。
禁止异常
事实证明,有许多代码库禁止在 C++ 中使用异常(比如谷歌C++规范,https://google.github.io/styleguide/cppguide.html#Exceptions)。如果没有异常,就不可能从构造函数返回错误。
这就引出了一个问题:如何在构造过程中检测错误并防止创建无效对象?常见的答案似乎是添加一个单独的初始化方法(例如 init),如下所示:

这意味着现在必须这样使用该类:

调用者现在必须通过两个单独的步骤来创建对象:一个是 Rectangle r,它调用构造函数,另一个是调用r.init()。至关重要的是,对象的内部状态现在在这两个步骤之间无效,并且可以通过公共接口观察到这种不一致的状态。
这违反了封装原则。程序现在可以拥有 Rectangle无效的 a 实例(类似于空值),并且代码可以很好地引用它们。
一个可能的解决方案
有一些方法可以缓解但不能完全解决这个问题。首选方法是使用静态工厂方法来执行对象构造和初始化以及所需的错误处理。

在这个版本中,恢复了原来不进行验证的构造函数。对象现在总是以完整状态构造,构造函数的调用者可能提供无效值。但是构造函数现在是私有的,所以从封装的角度来看这是合理的;只需要在公共接口边界上保持一致性和有效性。
但是使用私有构造函数,无法创建类的实例!为了解决这个问题,引入一个静态工厂方法, Rectangle如果参数有效则返回一个新的,或者如果输入无效则返回一个空指针。通过这种方式,Rectangle现在再次正确封装了的公共接口:create()保证返回的所有对象都是有效的,如果参数无效,则根本不创建任何对象。
这段代码并不等同于使用异常的版本。工厂方法返回一个堆分配的对象,并完全控制对象的创建。这有两个问题:第一,放弃了使用栈分配对象,因为仅仅在堆栈上声明一个对象会导致其构造函数被调用;其次,如果其他类想要扩展,则很难处理继承 Rectangle。
总结
以下是从这篇文章中的要点:
- 要提高程序的可靠性,可以做的最好的事情就是使无效状态无法表示。
- 让构造函数保持简单:他们应该做的就是分配字段。无论是否使用异常。
- init尽可能避免类似的方法。如果在对象构造期间有实际的逻辑,则将该代码放入静态工厂方法中(并且有更好命名的“构造函数”),或使用依赖注入将该逻辑推送给调用者。
- 作为上述的 init 方法不返回void是无用的。不要将构造与初始化分开。将所有初始化保留在构造函数中。
- 如果 必须 提供 init方法来处理错误,请在其中做尽可能少的工作。任何可以在构造函数中初始化的字段都应该在那里初始化(因为这允许创建const数据)。init为少数需要进行错误检查的字段保留该方法。
- 考虑向类添加一个布尔值is_initialized,并在所有方法中断言它为真(除了在 中init,断言它为假)。这会增加代码和运行时的开销,但它会检测最终使用部分初始化对象的情况。
最后一个想法是:Rust 迫使你关心这些正确性属性(通过 Result<T,E>)。如果你不了解 Rust 但经常使用 C++ 编写代码,强烈建议学习 Rust:你将改变对构建数据类型和算法的思考方式,并且将轻松发现 C++ 中的问题模式.