基类只有一个派生类时可以吗?

时间:2013-01-14 16:56:21

标签: c++ oop design-patterns

我正在使用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主体并首先创建接口并在派生类中实现。另一方面,这是一种矫枉过正,它会使事情变得复杂吗?我的代码很简单,不使用任何设计模式,但它通过派生类在通用数据封装方面确实从中获得了线索。

最终上面的类层次结构是好还是应该在一个类中完成?

6 个答案:

答案 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 InheritanceSingle 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时删除它们。这样做,如果引用存储在别处,可能会导致悬空指针。这将是一个很好的机会来了解强引用和弱引用,它们是引用计数器容器,当所有对它的引用都丢失时会自动删除它们的指针。