Maxpm
Maxpm

Reputation: 25571

Class Interfaces: Basic or Complex?

I'm writing a container class for fun and education. Previously when writing container classes I had limited myself to just a few very basic methods: GetValue, SetValue, GetSize and Resize. I did this to avoid "code spaghetti" so my class would be easier to debug.

However, it occurred to me that users of the class might want to do more than just simple substitution. So I added a few more methods:

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);

In general, should classes provide only the most basic interface and let the users of the class make their own functions to do the complex stuff? Or should the complex stuff be built-in at the cost of maintainability?

Upvotes: 3

Views: 292

Answers (5)

Philipp
Philipp

Reputation: 11814

You should try to keep your interface lean, especially if you might want to implement different container types, e.g. array-based and linked-list. If you provide some basic methods in all your containers, you can create external algorithms which perform certain tasks but which can work on all containers:

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

could become

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

outside the classes. If you don't do this, you have to write all those methods In all your containers.

Another possibility is to use the template method pattern (not related to C++ templates) and write all these methods in a base class (which defines the basic methods as pure virtual and calls them from the implemented "convenience" methods). This leads to possibly many virtual function calls which might not be desired in a container class for performance reasons.

Upvotes: 2

Alexandre C.
Alexandre C.

Reputation: 56956

The problem is that as soon as you write another container class (there are plenty of them in the wild, you may need different kinds), you will find that your design squares in O(N * M), where N is the number of container classes and M the number of algorithms.

The solution is to decouple the containers from the algorithms, and this is why iterators were introduced in the STL.

There are alternatives to iterators, using eg. polymorphism. You can factor out the traversing interface in an abstract common base class, and implement the algorithms in term of it.

In short, keep the most logic out of your container classes.

Upvotes: 2

ltjax
ltjax

Reputation: 15997

Classes should only provide a basic/minimal interface of member-functions (and preferably no data!). You can then add convenience methods as non-friend non-member functions. According to the interface-principle however, these functions are still part of your classes interface.

You already named the prime reason for this: it makes the class a lot easier to maintain. Also, implementing your "convienence" method part will serve as a nice test to see if you interface is good enough.

Note that the member function part of a container should usually be very generic and powerful, and care not about much more than maintaining class invariants.

This is most modern opinion on the subject, as far as I know. It is prominently advocated in Scott Meyer's "Effective C++" (In the most recent 3rd edition) and in Sutter's and Alexandrescu's "C++ Coding Standards".

Upvotes: 2

umlcat
umlcat

Reputation: 4143

I had a similar case like these. My suggestion is, that you have 2 "base classes" or "super classes".

The first class, very general class, represents the "conceptual root" for all containers classes, almost not methods, similar to an interface, and should be like:


containers.hpp

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

   size_t GetSize();

   void Resize(size_t);
};

The second class, starts to be a little less conceptual and more "real world":


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);

}

And, finally, some classes that are concrete:


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();
}

AS @Chris mentioned, there are several libraries for doing this, but there is always an exception to the rule, and you may want to "reinvent the wheel" if you need it.

I had an application, with its set of libraries, that included some containers / collections. It was made in another progr. langr. and need it to migrate it to C++. Altought, I also check the c++ standard libraries, I ended migrating my libraries to C++, because I had several libraries calling my container libraries, and need it to do it fast.

When using "base classes", you may want to "protect" its members, and "take it to the public", in child classes. I usually don't make "private" fields or methods, unless necessarily.

Summary: Some very common complex stuff (like memory allocation or storage) may be done in your base classes, but, most of those complexity should be leave it to child classes.

Upvotes: 1

Bernhard
Bernhard

Reputation: 8831

If this container is used only by you in your code and the methods of your interface are sufficient for a specific purpose it is fine doing it this way.

However, as soon as someone else is going to use the container, or you plan to use it in other areas I'd recommend adding interface methods working with iterator types, then your container is much more open to be used with the stdlib containers and algorithms. Use the interfaces of the stdlib containers as example.

Upvotes: 0

Related Questions