背景
看到类似如下项目代码:
struct IA {
virtual ~IA() {}
virtual bool Init() = 0;
...
};
class A: public IA {
public:
A(const std::string& path);
bool Init() override {
FILE* fp = fopen(path_.c_str());
...
}
private:
std::string path_;
};
int main() {
A a("/etc/a.config");
if (a.Init() == false) {
return -1;
}
return 0;
}
有单独的Init方法,并且Init可能失败,开发者遵循了不在构造函数中抛出异常的原则。但是看着代码,让人想思考以前没有考虑明白或者考虑明白了又忘记的问题,比如下面的。
是否应该在Init方法中提供参数而非在构造函数中提供?
上面例子中,为了能够在Init时读取配置文件,需要使用一个构造时传入的path字符串,并保存为path_成员,对于此成员,除了在Init中使用,别的地方不再使用。path_成员仅为Init而存在,如果A的对象很多,这里可能会占用不少的内存。为什么不把path参数放到Init方法参数列表中,减少一个path_成员呢?于是我们得到:
struct IA {
virtual bool Init(const std::string& path) 0;
...
};
class A: public IA {
public:
bool Init(const std::string& path) override {
FILE* fp = fopen(path.c_str());
...
}
};
int main() {
A a;
if (a.Init("/etc/a.config") == false) {
return -1;
}
return 0;
}
除此之外,在Init中提供参数还有一个好处,即,有时我们不得不面临两个类互相依赖的情况,假如在构造函数时提供参数:
class A {
public:
A(B* b);
...
}
class B {
public:
B(A* a);
...
};
此时我们无法构造A和B的实例。改为使用Init,则可以实现:
class A {
public:
bool Init(B* b);
...
}
class B {
public:
bool Init(A* a);
...
};
int main() {
A a;
B b;
a.Init(&b);
b.Init(&a);
return 0;
}
因此,Init中提供参数,有两个好处:a) 节省内存,b) 有时能以尚算优雅的方式解决环形引用的问题。
是否应该在接口中包含Init方法?
从前面问题的讨论,我们考虑提出在Init中提供参数的编程规范,但随即发现,不同的接口实现者,Init时需要的参数不同,这就类似于不同实现类有不同的构造条件,难以抽象出相同Init接口方法。不仅如此,在使用Init方法时,往往紧随构造之后,而非在定义接口的框架中使用。既然如此,这个Init实际上是实现相关的,他是构造函数的延续,不应该在接口中定义Init方法。于是,代码变为:
struct IA {
...
};
class A: public IA {
public:
bool Init(const std::string& path) {
FILE* fp = fopen(path.c_str());
...
}
};
int main() {
A a;
if (a.Init("/etc/a.config") == false) {
return -1;
}
return 0;
}
Init方法是否有存在必要?
现在规范变为,接口中不要包含Init方法,再另见我的前面一篇文章,得出结论不管有没有接口,不要使用Init方法。代码演变为:
struct IA {
...
};
class A: public IA {
public:
A(const std::string& path, std::error_code& errc) {
FILE* fp = fopen(path.c_str());
if (fp == nullptr) {
errc = std::errc::io_error;
return;
}
...
}
};
int main() {
std::error_code errc;
A a("/etc/a.config", errc);
if (errc) {
return -1;
}
return 0;
}