(原文链接:https://abseil.io/tips/42 译者:clangpp@gmail.com)
每周贴士 #42: 工厂函数优于初始化方法
- 最初发布于2013-05-10
- 作者:Geoffrey Romer (gromer@google.com)
- 校订于2017-12-21
“The man who builds a factory builds a temple; the man who works there worships there, and to each is due, not scorn and blame, but reverence and praise.” – Calvin Coolidge
(译者注:译者文学水平太洼,起兴部分保留原文。欢迎在评论区提出翻译建议)
在禁用异常的环境里(比如Google),C++的构造函数事实上必须成功,因为它们没有办法向调用者报告失败。当然,你可以用abort()
,但那会让整个程序崩溃,在产品代码中一般不可接受。
如果你的类初始化逻辑不能避免失败的可能,一个常见的办法是在类中定义一个初始化方法(也被称作"init method")。它执行任何可能失败的初始化操作,并以返回值来表示失败(或成功,译者注)。这里有个假定:用户会在对象被构造后立即调用初始化方法;如果初始化失败,用户会立即销毁对象。然而,这个假定常常不会被写入文档,更别提遵守了。用户很容易在初始化之前或初始化失败后调用别的方法。有些类甚至会鼓励这种行为。例如,在初始化之前提供方法配置对象,或者在初始化失败后提供方法从对象中读取错误信息。
这种设计要求一个类维护至少两种(常常是三种)用户可见的状态:已初始化(初始化成功,译者注),未初始化,和初始化失败。做这样的设计需要遵守很多规程:类的每个方法都要指明在哪些状态下可以被调用,而且用户必须遵守这些规则。如果(设计类时,译者注)不遵守这个规程,那么客户端开发者很可能会写出碰巧能工作的代码,不管你本来期望支持何种调用范式。当这种情况开始出现后,(你的类,译者注)可维护性会骤然下降,因为一旦客户端(在调用初始化方法前)以某种顺序调用了一些方法,客户端事实上就已经依赖于这个调用顺序了,类的实现只能对其一直保持兼容。结果是,你的实现成为了你的接口。(参阅海勒姆法则。)
幸运的是,有个简单的备选方案没有这些坑:提供一个工厂函数,创建和初始化对象,以指针或absl::optional
返回对象(参阅TotW #123),返回空值(null)表示失败。这里有个使用unique_ptr<>
的小例子:
// foo.h
class Foo {
public:
// 工厂方法:创建并返回一个Foo。
// 失败时返回null
static std::unique_ptr<Foo> Create();
// Foo不支持复制
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
private:
// 客户端不能直接调用构造函数。
Foo();
};
// foo.c
std::unique_ptr<Foo> Foo::Create() {
// 注:因为Foo的构造函数是私有的(private),我们只能用new。
return absl::WrapUnique(new Foo());
}
在很多情况下,这种模式两全其美:工厂函数Foo::Create()
只暴露出完全初始化的对象(与构造函数相似),但它还能指示失败(与初始化方法相似)。工厂函数的另一个优势是,它们可以生成返回值类型子类的对象(返回值类型为absl::optional
时不适用)。这可以让你在不改变用户代码的前提下更换实现方式,甚至根据用户输入动态选择实现类型。
这种方式主要的缺点是,它会返回指针指向堆上的对象,因此不适用于期望其在栈上工作的“值”类型(如Point、Timestamp、Duration等,译者注)。不过话又说回来了,这样的类型通常一开始就不需要复杂的初始化操作。当派生类构造函数需要初始化其基类时,工厂函数也不适用。因此有时候初始化方法作为基类的保护(protected)成员也还是必须的。当然了,公有(public)API仍然可以使用工厂函数。