我有一个代码,其中派生类实现的功能是基类中相同的扩展。在下面的示例中,我希望函数do_work
执行一系列任务。在派生类Derived
中,do_work
函数包含基类Base
do_work
函数的所有工作,包括一个额外任务。
从软件设计的角度来看,最优雅的方法是什么?使用OPTION 1
或使用OPTION 2
?
#include <iostream>
class Base
{
public:
virtual void do_work()
{
do_base_task1();
do_base_task2();
}
protected:
void do_base_task1() { std::cout << "Doing base task1" << std::endl; }
void do_base_task2() { std::cout << "Doing base task2" << std::endl; }
};
class Derived : public Base
{
public:
void do_work()
{
// OPTION 1
do_base_task1();
do_base_task2();
// END OF OPTION 1
// OPTION 2
Base::do_work();
// END OF OPTION 2
do_extra_task();
}
protected:
void do_extra_task() { std::cout << "Doing derived task" << std::endl; }
};
int main()
{
Base base;
base.do_work();
Derived derived;
derived.do_work();
return 0;
}
答案 0 :(得分:2)
在我看来,最有效的版本是:
void do_work() {
Base::do_work();
do_extra_task();
}
因为这样改变Base::do_work
实现,所以你也不必在派生类成员函数Derived::do_work
中传递更改。
答案 1 :(得分:1)
除了101010的答案,还要做
void do_work() {
Base::do_work();
do_extra_task();
}
允许您创建do_base_task1()
和do_base_task2()
私有成员函数而不是受保护,这改进了结构和封装。 Derived
类没有业务调用这两种方法,因此将它们设为私有,并提供一种公共接口方法来调用这些方法(即Base::do_work()
)是更好的设计。
答案 2 :(得分:1)
选项3:
class Base
{
public:
void do_work() // not virtual
{
do_base_task1();
do_base_task2();
do_extra_task();
}
//... base tasks as before
protected:
virtual void do_extra_task(){}
};
class Derived : public base
{
//no do_work in here now
protected:
void do_extra_task() { std::cout << "Doing derived task" << std::endl; }
}
以与示例中相同的方式在main中调用
通过这种方式,您知道所有派生类都会调用基类执行(您不能在其中一个派生类中忘记它),如果某个特定派生类没有额外的工作要做,那么它就不需要了定义do_work或do_extra_tasks。减少重复次数,因为您没有重复定义do_work。
这是非虚拟接口模式。
答案 3 :(得分:1)
又一个选择:
#include<iostream>
struct B {
virtual void doWork() = 0;
};
template<class D>
struct T: B {
void doWork() override {
taskT();
static_cast<D*>(this)->taskD();
}
private:
void taskT() { std::cout << "taskT" << std::endl; }
};
struct S: T<S> {
void taskD() { std::cout << "taskD" << std::endl; }
};
int main() {
B *b = new S;
b->doWork();
}
这是基于CRTP习语
如果您正在设计一个小型图书馆,这个选项在完成调用时会完全负责调用正确的方法。
您的库的用户甚至不知道基类中的方法是否存在,但是人们知道派生类的方法将被契约调用。
更重要的是,taskD
不是基类接口的一部分,因此您无法从对B
的引用中执行额外任务(除非您调用doWork
,它实际上执行所有正确的顺序任务)。