4

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

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

4

6 回答 6

7

不,事实上我相信你的设计是好的。您可能稍后需要为您的类添加一个模拟或测试实现,而您的设计使这更容易。

于 2013-01-14T16:58:32.323 回答
4

答案取决于您对该接口有多个行为的可能性有多大。

文件系统的读写操作现在可能非常有意义。如果你决定写入远程的东西,比如数据库怎么办?在这种情况下,新的实现仍然可以完美运行,而不会影响客户端。

我会说这是一个很好的例子来说明如何做一个界面。

你不应该让析构函数成为纯虚拟的吗?如果我没记错的话,这是 Scott Myers 推荐的用于创建 C++ 接口的习惯用法。

于 2013-01-14T17:00:33.027 回答
1

是的,这是可以接受的,即使您的接口只有 1 个实现,但它在运行时(稍微)可能比单个类慢。(virtual调度的成本大约是跟随 1-2 个函数指针)

这可以用作防止客户端依赖于实现细节的一种方式。例如,您的接口的客户端不需要仅仅因为您的实现在上述模式下获得一个新的数据字段而重新编译。

您还可以查看pImpl模式,这是一种在不使用继承的情况下隐藏实现细节的方法。

于 2013-01-14T17:01:47.137 回答
0

不。如果不存在多态性,则没有继承的理由,您应该使用重构规则将两个类合二为一。“更喜欢组合而不是继承”。

编辑:正如@crush 评论的那样,“更喜欢组合而不是继承”可能不是这里的充分引用。所以让我们说:如果你认为你需要使用继承,三思而后行。如果您真的确定需要使用它,请再考虑一下。

于 2013-01-14T17:02:04.683 回答
0

您的模型适用于工厂模型,您可以在其中使用大量共享指针并调用一些工厂方法来“获取”指向抽象接口的共享指针。

使用 pImpl 的缺点是管理指针本身。然而,对于 C++11,pImpl 可以很好地移动,因此更可行。但是目前,如果您想从“工厂”函数返回类的实例,则它的内部指针存在复制语义问题。

这导致实现者要么返回一个指向外部类的共享指针,该指针是不可复制的。这意味着您有一个指向一个类的共享指针,该类持有一个指向内部类的指针,因此函数调用会通过额外的间接级别,并且每个构造都会获得两个“新”。如果您只有少数这些不是主要问题的对象,但它可能有点笨拙。

C++11 的优势在于具有支持其底层和移动语义的前向声明的 unique_ptr。因此 pImpl 将变得更加可行,因为您确实知道您将只有一个实现。

顺便说一句,我会摆脱那些CStrings 并将它们替换为std::string,而不是将 C 作为每个类的前缀。我还将实现的数据成员设为私有,而不是受保护。

于 2013-01-14T17:21:36.127 回答
0

由 Stephane Rolland 引用的Composition over InheritanceSingle Responsibility Principle定义的替代模型实现了以下模型。

首先,您需要三个不同的类:

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

于 2013-01-14T17:42:58.783 回答