我正在使用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主体并首先创建接口并在派生类中实现。另一方面,这是一种矫枉过正,它会使事情变得复杂吗?我的代码很简单,不使用任何设计模式,但它通过派生类在通用数据封装方面确实从中获得了线索。
最终上面的类层次结构是好还是应该在一个类中完成?
答案 0 :(得分:7)
不,实际上我相信你的设计很好。您以后可能需要为您的类添加模拟或测试实现,您的设计使这更容易。
答案 1 :(得分:4)
答案取决于您为该界面提供多种行为的可能性。
文件系统的读写操作现在可能非常有意义。如果你决定写一些远程的东西,比如数据库怎么办?在这种情况下,新的实现仍然可以完美运行而不会影响客户端。
我想说这是一个如何做界面的很好的例子。
你不应该让析构函数纯虚拟吗?如果我没记错的话,这是根据Scott Myers创建C ++接口的推荐习惯用法。
答案 2 :(得分:1)
是的,这是可以接受的,即使只有一个接口的实现,但它在运行时(稍微)可能比单个类慢。 (virtual
dispatch大概花费了1-2个函数指针的成本)
这可以用作防止客户端依赖于实现细节的方法。例如,您的界面客户端不需要重新编译,因为您的实现在上述模式下获得了新的数据字段。
您还可以查看pImpl
模式,这是一种在不使用继承的情况下隐藏实现细节的方法。
答案 3 :(得分:0)
没有。如果在操作中没有多态,则没有理由进行继承,您应该使用重构规则将这两个类合并为一个。 “首选组合而非继承”。
编辑:正如@crush所评论的那样,“首选组合优于继承”可能不适合这里的引用。所以,让我们说:如果你认为你需要使用继承,请三思而后行。如果您真的确定需要使用它,请再次考虑它。
答案 4 :(得分:0)
您的模型适用于您使用大量共享指针的工厂模型,并且您调用一些工厂方法来“获取”指向抽象接口的共享指针。
使用pImpl的缺点是管理指针本身。但是使用C ++ 11,pImpl可以很好地移动,因此更加可行。目前,如果你想从“工厂”函数返回你的类的实例,它就会用它的内部指针复制语义问题。
这导致实现者返回一个指向外部类的共享指针,该指针是不可复制的。这意味着你有一个指向一个类的共享指针,该类包含一个指向内部类的指针,因此函数调用会通过额外的间接级别,每个构造得到两个“新”。如果你只有少数这些对象不是主要问题,但它可能有点笨拙。
C ++ 11的优点是都具有unique_ptr,它支持其底层和移动语义的前向声明。因此,当您确实知道自己只有一个实现时,pImpl将变得更加可行。
顺便说一句,我会删除那些CString
并用std::string
替换它们,而不是把C作为每个类的前缀。我还会将实现的数据成员设为私有,而不是受保护。
答案 5 :(得分:0)
由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时删除它们。这样做,如果引用存储在别处,可能会导致悬空指针。这将是一个很好的机会来了解强引用和弱引用,它们是引用计数器容器,当所有对它的引用都丢失时会自动删除它们的指针。