类接口:基本还是复杂?

3

我正在为了娱乐和学习编写一个容器类。以前在编写容器类时,我会限制自己只使用一些非常基本的方法:GetValueSetValueGetSizeResize。这样做是为了避免代码混乱,使我的类更易于调试。

然而,我意识到类的用户可能希望做更多的事情,而不仅仅是简单的替换操作。因此,我又添加了一些方法:

void Replace(const std::size_t Start, const std::size_t End, const T Value);
void Replace(const std::size_t Start, const std::size_t End, const MyClass Other);
void Insert(const std::size_t Index, const T Value);
void Insert(const std::size_t Index, const MyClass Other);
void Delete(const std::size_t Index);
void Delete(const std::size_t Start, const std::size_t End);

一般来说,类应该只提供最基本的接口,让类的用户自己编写函数来完成复杂的任务吗?还是应该内置复杂的功能,但这样会牺牲可维护性?


1
首先,你需要这些功能来实现你的目的吗?其次,如果你让它遵循标准库的模式,你的生活会更轻松。 - Chris Pitman
1
@Chris 我的目的是教育和娱乐,所以可能也可能不。 - Maxpm
两种不同的插入和替换方法是什么意思?T vs MyClass. - Matt Wonlaw
@mlaw 插入(Insert)会腾出空间给新元素,使其他元素腾挪位置。而替换(Replace)则仅覆盖现有元素。容器类是模板化的,因此T代表类所存储的内容,而MyClass则代表类本身。 - Maxpm
啊,明白了。所以Insert(Index,MyClass Other)就像是一个“addAll”操作。 - Matt Wonlaw
5个回答

2
类应该只提供基本/最小的成员函数接口(最好不要包含数据!)。然后,您可以将方便方法作为非友元非成员函数添加。但是根据接口原则,这些函数仍然是类接口的一部分。
您已经提到了主要原因:它使类更容易维护。此外,实现“方便”方法部分将作为测试,以查看您的接口是否足够好。
请注意,容器的成员函数部分通常应该非常通用和强大,并且不需要太多关注维护类不变量。
据我所知,这是关于此问题的最现代观点。它在Scott Meyer的“Effective C ++”(最近的第3版)和Sutter和Alexandrescu的“C ++编码标准”中得到了广泛倡导。

2
问题在于,一旦您编写另一个容器类(在野外有很多这样的类,您可能需要不同类型),您会发现您的设计方块在O(N * M)中,其中N是容器类的数量,M是算法的数量。
解决方案是将容器与算法解耦,这就是为什么在STL中引入了迭代器的原因。
还有其他替代迭代器的方法,例如使用多态性。您可以从抽象公共基类中提取出遍历接口,并根据它来实现算法。
简而言之,要让您的容器类保持尽可能少的逻辑。

2

您应该尽量保持界面简洁,特别是如果您可能想要实现不同的容器类型,例如基于数组和链表的容器。如果在所有容器中提供一些基本方法,则可以创建外部算法执行某些任务,但可以在所有容器上工作:

 void Replace(const std::size_t Start, const std::size_t End, const T Value);

可能会变成

 template<class ContainerType>
 void ReplaceAllElementsInContainer(ContainerType& Container, const std::size_t Start, const std::size_t End, const T Value);

在类外编写这些方法。如果您不这样做,您必须在所有容器中编写所有这些方法。

另一个可能性是使用模板方法模式(与C++模板无关),并在基类中编写所有这些方法(将基本方法定义为纯虚拟方法,并从实现的“便利”方法中调用它们)。这可能会导致许多虚拟函数调用,这对于容器类来说可能并不理想。


1
我曾经遇到过类似的情况。我的建议是,你需要有两个“基类”或“超类”。
第一个类是非常通用的类,代表了所有容器类的“概念根源”,几乎没有方法,类似于接口,应该像这样:

containers.hpp

class Container
{
protected:
   int GetValue();
   void SetValue(int newValue);

   size_t GetSize();

   void Resize(size_t);
};

第二个课程开始变得更加实际,概念性较少。

mcontainers.hpp

#include "containers.hpp";

class MethodContainer: public Container
{
protected:
  void Replace(const std::size_t Start, const std::size_t End, const T Value);
  void Replace(const std::size_t Start, const std::size_t End, const MyClass Other);
  void Insert(const std::size_t Index, const T Value);
  void Insert(const std::size_t Index, const MyClass Other);
  void Delete(const std::size_t Index);
  void Delete(const std::size_t Start, const std::size_t End);

}

最后,一些具体的类:


stacks.hpp

#include "containers.hpp";
#include "mcontainers.hpp";

#define pointer void*

class Stack: public MethodContainer
{
public:
  // these methods use "mcontainer::Insert", "mcontainer::Replace", etc
  void Push(pointer Item);
  void Pop();
  pointer Extract();
}

正如@Chris提到的,有几个库可以完成这项工作,但也总有例外情况,如果您需要,您可能希望“重新发明轮子”。

我有一个应用程序,它包含一些容器/集合的库。 它是用另一种编程语言制作的,需要将其迁移到C ++。 虽然我也检查了c++标准库,但最终还是将我的库迁移到了C ++,因为我有几个调用我的容器库的库,并且需要快速完成迁移。

当使用“基类”时,您可能希望“保护”其成员,并在子类中将其公开。 除非必要,我通常不会设置“私有”字段或方法。

总结:某些非常常见的复杂内容(例如内存分配或存储)可以在基类中完成,但大多数复杂性都应该留给子类处理。


0

如果这个容器只在你的代码中被你使用,并且你的接口方法足以满足特定的目的,那么这样做是可以的。

然而,一旦有其他人要使用这个容器,或者你计划在其他领域使用它,我建议添加与迭代器类型一起工作的接口方法,这样你的容器就更容易与stdlib容器和算法一起使用。使用stdlib容器的接口作为示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接