Pure Interfaces

In my opinion, one disadvantage of c++ is that there exists no explicit interface concept or language feature. Of course, you can implement interfaces in c++ but you must use an abstract. The downside of this concept is the fact that an abstract class can already contain implementations and definitions or implementations of private or protected members or functions. Therefore, it allows to have elements which should not be part of an interface.

But before we start to analyze this in detail we want step back and define some terms. In most type-safe object-oriented programming languages with will find the concept of interfaces, abstract classes and concrete classes. An interface is a syntactical contract only. It defines the methods a class must contain. An abstract class also contains contract definitions and additional it already contains implementations. As an abstract class contains contract definitions for methods which must be implemented, it cannot be instantiated. Instead it is thought as base class for concrete classes. If a concreate class is based on an abstract class, it can use the implementations of the abstract class and it must implement the methods defined as contract but not implemented in the abstract class. A concrete class contains implementations only and therefore it can be instantiated. It can be derived from an interface and/or an abstract class and of course it can be implemented without using a parent element.

In c++ we don’t have an implicit language feature to implement interfaces. But we have abstract and concrete classes. An abstract class can be implemented by adding a contract for a method which must be implemented by a concreate class. In c++ we can implement such a contract by using a pure virtual method. If we want to implement an interface, we can do this by implementing an abstract class with pure virtual functions only. Therefore, you can implement interfaces in c++ but you must use an abstract class. As described at the beginning, abstract classes and interfaces are two independent elements in object oriented concepts. As c++ does not distinguish these two concepts, the compiler cannot prevent according implementation errors. For example, if you want to implement an interface, you can add elements (e.g. implemented methods) which will make the interface to an abstract class. But c++ cannot prevent this smelly software design. So, as a developer, you are responsible to write interfaces which are according object-oriented concepts. At next we will look at an example and think about a possible code design.

Let’s start with an example. We want to implement an application which is used to show and edit documents of different types. Therefore, we need some kind of document class which offers methods to manage a document. Within the example we want to use a base class “document” and two derived classes “TextFile” and “HtmlFile”. We will start with the load and save features of the document. In this article, we will think about the document data management only and offer an according interface which can be used by the client application. The following source code shows a possible implementation.

class Document
{
public:   // interface for clients of Document
  virtual void Load(std::string fileName);
  virtual void Save();

protected:  // common functions for implementers of Document
  std::string Serialize();
  void DeSerialize(std::string data);
  std::string CalculateHash();

protected:  // common data for implementers of Document
  std::string mFileName;
};

class TextFile : public Document
{
public:
  void Load(std::string fileName);
  void Save();

protected:
  std::string mEncoding;
};

class HtmlFile : public Document
{
public:
  void Load(std::string fileName);
  void Save();
};

 

The base class “document” contains public implementations for the load and the save methods. These public methods are our interface for the client application. Furthermore, it contains protected methods to serialize and de-serialize data, a function to calculate a hash code which is needed for some security features and it contains internal data about the loaded document. These protected methods and protected data members are an implementation help for the derived classes which can use these methods and data members. The derived classes can use the already implemented load and save methods or if needed they can overwrite them. Furthermore, they can add new elements like the “mEncoding” member in the “TextFile” class.

This implementation seems to solve our needs. And of course, you will find implementations of this kind very often in existing code. But based on the thoughts we had have at the start of the article we must ask: is this a good software design? What do you think?

 

Single Responsibility

In my opinion, there is one major issue regarding the above implementation: the class “Document” has four responsibilities. It provides the interface for client, it contains document specific functions used by derived classes, it contains generic functions which could be needed by other classes and not only derived ones and it manages the document data.

According the “single responsibility principle” such a design has many disadvantages. Due to the unnecessary dependencies, the code will be bad to maintain, changes will result in higher effort and compiling takes longer. So, we should try to split the document class into four separate classes, each responsible for one topic.

The following source code shows a possible implementation.

class DocumentInterface
{
public:   // interface for clients of Document
  virtual void Load(std::string fileName) = 0;
  virtual void Save() = 0;
};

class DocumentTools
{
protected:  // common functions for implementers of Document
  std::string CalculateHash();
};

class DocumentData
{
protected:  // common data for implementers of Document
  std::string mFileName;
};

class Serializer
{
public:  // common functions 
  std::string Serialize();
  void DeSerialize(std::string data);
};

 

We have three document specific classes, one for the interface, one for common helper functions used to implement derived classes and one for the data management. Furthermore, we have created a generic serializer class as it offers common serialization features which may be used in other uses cases too. So, this implementation can be reused in other scenarios and is no longer limited to documents.

 

Composition vs. Inheritance

The nice separation of concerns principle forces us to implement four independent classes. To implement our document specific features, we will use and connect these classes. This connection can be created by using two main concepts: composition and inheritance.

We know our classes “TextFile” and “HtmlFile” are both documents which must implement the document interface. Therefore, we found a first need for inheritance. But most often it isn’t that easy and so it isn’t in this case too. We could think about two main designs: implement the interface directly or implement a base class. “TextFile” as well as “HtmlFile” are both documents. Maybe we have some advantages if we use a base class “document” which implements the interface and we derive from this class. The following source code shows both possibilities.

// inheritance without base class
class DocumentInterface {};
class TextFile : public DocumentInterface {};
class HtmlFile : public DocumentInterface {};

// inheritance with base class
class DocumentInterface {};
class DocumentBase : public DocumentInterface {};
class TextFile : public DocumentBase {};
class HtmlFile : public DocumentBase {};

 

Beside the decision about the kind of inheritance we want to use, we should decide whether we want to use inheritance at all. This will lead us to the well-known “composition vs. inheritance” topic. Let’s assume we implement a document base class. This base class can derive from the tool and data classes or it can use the tool and data classes. The following code shows these possibilities.

// inheritance for additional classes
class DocumentInterface {};
class DocumentTools {};
class DocumentData {};
class Serializer {};
class DocumentBase : public DocumentInterface, public DocumentTools,
  public DocumentData, public Serializer {};

// composition for additional classes
class DocumentInterface {};
class DocumentTools {};
class DocumentData {};
class Serializer {};
class DocumentBase : public DocumentInterface
{
private:
  DocumentTools mTools;
  DocumentData mData;
  Serializer mSerializer;
};

 

As we can see we must make some fundamental design decisions before we start to implement the document feature. At first, we should decide whether we want to implement a base document class. Such a base class makes sense if we have some common functionality which is needed in several sub classes. For example, if we can implement the “Load” and “Save” functions in a generic way, we should implement them only once. In this case these functions should be implemented in a base class and can be used by all derived classes.

At next we should think about the composition vs. inheritance topic. If you have two classes and you want to choose the type of dependency, you could ask: “Is x also a y? Or does x only use y?”. For example, think about the dependency between “TextFile” and “Document”. Is the “TextFile” a “Document”? I think: Yes, it is. Therefore, we have a inheritance connection between them. What is with “HtmlFile” and “Serializer”? Is the “HtmlFile” a “Serializer”? I don’t think so. But the “HtmlFile” may use the “Serializer”. Therefore, we should use a composition in this case.

 

Possible class design

Based on the Thoughts so far, I want to implement a suitable class design. As a general rule, I would recommend avoiding dependencies. Each functionality should be an own feature and implemented independent from the other parts of the software. This will increase reusability, maintainability, the software will be easier to understand, unit-testing will be much easier and so the software gets a higher quality.

Of course, dependencies are needed sometimes. If a complex feature should be done, it needs the combine the different functionalities of the independent classes and combine them to solve a more complex task. That’s the point where we want to create dependencies. Depending on the use case, we will select the corresponding single classes needed to solve the use case and combine them to a more complex system.

The following source code shows an implementation according this rule. There are several independent classes. As we want to use the features of this classes to manage documents, we will combine the single tasks (classes) by using them in one complex task (class). So, we create a document base class which creates the dependency between the document specific data class, the document specific tool class, the independent tools and the client interface. This base class will then be starting point for concrete document classes. These will be derived from the base class and may use their functionality or implement their own features.

// interface for clients of Document
class DocumentInterface
{
public:   
  virtual void Load(std::string fileName) = 0;
  virtual void Save() = 0;
};

// common functions for implementers of Document
class DocumentTools
{
protected:  
  std::string CalculateHash();
};

// common data for implementers of Document
class DocumentData
{
protected:  
  std::string mFileName;
};

// service class which is independent from document features 
// an may be used used in other application units too
class Serializer
{
public:  
  std::string Serialize();
  void DeSerialize(std::string data);
};

// base class for all documents with
// base implementation of the document interface
class DocumentBase
{
public:   
  virtual void Load(std::string fileName);
  virtual void Save();

protected:
  DocumentTools mTools;
  DocumentData mData;

private:
  Serializer mSerializer;
};

// document class, derived from document base
// may use the implementations of the base class
// may add additional functions
class TextFile : public DocumentBase
{
protected:
  std::string mEncoding;
};

// document class, derived from document base
// may use the implementations of the base class
// may add additional functions
class HtmlFile : public DocumentBase
{
};

 

Pure interfaces

Based on the fundamental consideration about interfaces in object oriented languages, we have analyzed a messy implementation example and have thought about the interface concept in c++. This first concept, of separation between interfaces, abstract classes and concreate classes was the base idea of the further design decisions und design guidelines to implement classes without dependencies to each other and create a connection between them only at higher level in use case specific scenarios.

In summary, the basis of the design was a clear separation between interfaces, base classes and concrete classes. As we don’t have an explicit language concept for interfaces in c++, we should create an implicit rule: “Make pure Interfaces”. As you want to implement an interface for a client, you should use an abstract class, but it must contain public pure virtual functions only.

Werbung
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit deinem WordPress.com-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s