基类只有一个派生类可以吗?
Posted
技术标签:
【中文标题】基类只有一个派生类可以吗?【英文标题】:Is it okay when a base class has only one derived class? 【发布时间】:2013-01-14 16:56:21 【问题描述】:我正在使用 OOD 和设计模式创建密码模块。该模块将保留可记录事件的日志并读取/写入文件。我在基类中创建了接口,在派生类中创建了实现。现在我想知道如果基类只有一个派生类,这是否是一种难闻的气味。这种类层次结构是不必要的吗?现在为了消除类层次结构,我当然可以在一个类中做所有事情而根本不派生,这是我的代码。
class CLogFile
public:
CLogFile(void);
virtual ~CLogFile(void);
virtual void Read(CString strLog) = 0;
virtual void Write(CString strNewMsg) = 0;
;
派生类是:
class CLogFileImpl :
public CLogFile
public:
CLogFileImpl(CString strLogFileName, CString & strLog);
virtual ~CLogFileImpl(void);
virtual void Read(CString strLog);
virtual void Write(CString strNewMsg);
protected:
CString & m_strLog; // the log file data
CString m_strLogFileName; // file name
;
现在在代码中
CLogFile * m_LogFile = new CLogFileImpl( m_strLogPath, m_strLog );
m_LogFile->Write("Log file created");
我的问题是,一方面我遵循 OOD 原则并首先创建接口并在派生类中实现。另一方面,它是不是有点矫枉过正,是否会使事情复杂化?我的代码很简单,不使用任何设计模式,但它确实从中获得了通过派生类进行一般数据封装方面的线索。
上面的类层次最终是好的还是应该在一个类中完成?
【问题讨论】:
如果您以后可以看到自己或其他人扩展该课程,那么不,这并不过分。不过,您应该知道,为您的类生成的 vtable 确实会为类的每个后代消耗内存。 【参考方案1】:不,事实上我相信你的设计是好的。您稍后可能需要为您的类添加一个模拟或测试实现,您的设计使这更容易。
【讨论】:
【参考方案2】:答案取决于您对该界面有多个行为的可能性。
文件系统的读写操作现在可能非常有意义。如果你决定写入远程的东西,比如数据库怎么办?在这种情况下,新的实现仍然可以完美运行而不会影响客户端。
我想说这是一个很好的例子来说明如何做一个界面。
你不应该把析构函数变成纯虚拟的吗?如果我没记错的话,这是 Scott Myers 推荐的用于创建 C++ 接口的习惯用法。
【讨论】:
这更好地解释了为什么在这种情况下继承是正确的选择。 析构虚拟,不是纯虚拟 是的,纯虚拟。请参阅 Scott Meyers “有效的 C++”:books.google.com/… 我不得不承认,在可预见的未来,在这种情况下我只看到了一个接口,但是分离接口和实现的想法首先吸引了我。这可以为将来的其他事情铺平道路。在一个单独的模块中,我在接口中有更多的方法,我很惊喜地发现我可以轻松地使用模板设计模式,因为我在那里也有这种类层次结构。所以我认为这是一个很好的原则,可能与不同的行为无关。 如果我错了,请纠正我,但我觉得任何“工人阶级”都应该有一个“接口”,即使它现在只需要一个派生类,而且我们没有立即看到需要不同的行为。【参考方案3】:是的,这是可以接受的,即使你的接口只有 1 个实现,但它在运行时可能会比单个类慢(稍微)。 (virtual
dispatch 的开销大概是跟随 1-2 个函数指针)
这可以用作防止客户端依赖于实现细节的一种方式。例如,您的接口的客户端不需要仅仅因为您的实现在上述模式下获得一个新的数据字段而重新编译。
您还可以查看pImpl
模式,这是一种隐藏实现细节而不使用继承的方法。
【讨论】:
【参考方案4】:您的模型与使用大量共享指针并调用一些工厂方法“获取”指向抽象接口的共享指针的工厂模型配合得很好。
使用 pImpl 的缺点是管理指针本身。然而,对于 C++11,pImpl 可以很好地移动,因此更可行。但目前,如果您想从“工厂”函数返回类的实例,则其内部指针存在复制语义问题。
这导致实现者要么返回一个指向外部类的共享指针,该指针不可复制。这意味着您有一个指向一个类的共享指针,该类持有一个指向内部类的指针,因此函数调用会通过额外的间接级别,并且每个构造都会获得两个“新”。如果您只有少量这些对象,这不是主要问题,但它可能有点笨拙。
C++11 的优势在于具有 unique_ptr 支持其底层和移动语义的前向声明。因此 pImpl 将变得更加可行,因为您确实知道您将只有一个实现。
顺便说一句,我会摆脱那些CString
s 并用std::string
替换它们,而不是将C 作为每个类的前缀。我还将实现的数据成员设为私有,而不是受保护。
【讨论】:
他为什么要换成std::string?这和依赖一样多,不是吗?除了它是一个优秀的字符串封装器之外,我的意思是。【参考方案5】:Composition over Inheritance 和 Single Responsibility Principle 定义的替代模型(均由 Stephane Rolland 引用)实现了以下模型。
首先,您需要三个不同的类:
class CLog
CLogReader* m_Reader;
CLogWriter* m_Writer;
public:
void Read(CString& strLog)
m_Reader->Read(strLog);
void Write(const CString& strNewMsg)
m_Writer->Write(strNewMsg);
void setReader(CLogReader* reader)
m_Reader = reader;
void setWriter(CLogWriter* writer)
m_Writer = writer;
;
CLogReader 处理读取日志的单一职责:
class CLogReader
public:
virtual void Read(CString& strLog)
//read to the string.
;
CLogWriter 处理写入日志的单一职责:
class CLogWriter
public:
virtual void Write(const CString& strNewMsg)
//Write the string;
;
然后,如果您希望您的 CLog 写入套接字,您将派生 CLogWriter:
class CLogSocketWriter : public CLogWriter
public:
void Write(const CString& strNewMsg)
//Write to socket?
;
然后将你的 CLog 实例的 Writer 设置为 CLogSocketWriter 的实例:
CLog* log = new CLog();
log->setWriter(new CLogSocketWriter());
log->Write("Write something to a socket");
优点 这种方法的优点是你遵循单一职责原则,因为每个类都有一个单一的目的。它使您能够扩展单一用途,而无需拖拽您无论如何都不会修改的代码。它还允许您根据需要更换组件,而无需为此创建一个全新的 CLog 类。例如,您可以有一个写入套接字的 Writer,但有一个读取本地文件的阅读器。等等。
缺点 内存管理在这里成为一个巨大的问题。您必须跟踪何时删除指针。在这种情况下,您需要在销毁 CLog 以及设置不同的 Writer 或 Reader 时删除它们。这样做,如果引用存储在其他地方,可能会导致悬空指针。这将是了解强引用和弱引用的好机会,它们是引用计数器容器,当所有对它的引用都丢失时,它们会自动删除它们的指针。
【讨论】:
对我来说,这看起来更复杂,因为它被过度分割。读/写也可以是文件或其他任何模块的一个模块。未来的扩张看起来也更加复杂。 未来的扩展其实没那么复杂。另外,如果您想从套接字读取,但要写入文件(正如我所提到的)。使用组件允许您根据需要混合和匹配,而无需创建类来处理每个变化。它还可以使用工厂。事实上,大多数框架 .NET、Java、Qt for C++ 等都使用这种风格。【参考方案6】:没有。如果不存在多态性,则没有继承的理由,您应该使用重构规则将两个类合二为一。 “优先组合而不是继承”。
编辑:正如@crush 评论的那样,“更喜欢组合而不是继承”在这里可能不是足够的引用。所以让我们说:如果你认为你需要使用继承,三思而后行。如果您真的确定需要使用它,请再考虑一下。
【讨论】:
组合不是指将其他继承类的对象添加为成员吗? 当未来的开发人员出现并需要创建新的 CLogFile 实现时会发生什么?把它分成两类而不是一类的任务会落到他们头上吗? 如果基类中有内容是的...但是这种情况下根本没有内容,甚至可以完全删除基类。 事实上,我确实相信迷恋。那是上课的目的,不是吗?哎呀,我什至可以为我的过程 C 库定义一个接口。虽然它没有像我可以将它与类分组一样好。 @StephaneRolland 不,您没有为每个类定义一个接口。您为相关的内容定义它们。这个实例是继承的合适时机,而class Object
可能使用 CLogFile 作为其本身的组合。 en.wikipedia.org/wiki/Composition_over_inheritance可能想先了解一下校长再讲。以上是关于基类只有一个派生类可以吗?的主要内容,如果未能解决你的问题,请参考以下文章