About components and interfaces

Introduction

Recently, I often encounter a situation where I have to deal with a lack of understanding by developers of the need to split systems into components. And if this understanding exists, then a misunderstanding of hiding the implementation of the component is revealed. And there’s no need to talk about the convenience of interfaces.

Hereinafter, we mean components in the sense of parts of one process. Components that are separate processes or services that work through RPC or something else are not considered here. Although to a large extent all of the following applies to them.

Example One:
I need a component long developed and tested in another department. I go to the developer. A dialogue ensues:
- Here I need this thing. How do I use it in my project?
- Yes, here it is necessary to fill in the project from CVS here on this tag. Compile. You get a lib, and now you need to link with it.
- OK thanks.
Deflate the project. Compile. A bunch of errors crawls out, there are not enough any includes. You start to find out. It turns out that to build a project you need to pump out a bunch of projects from CVS, and collect them too. Some are built as standard by the studio, some with a tambourine, like autoconf, make, and others like them. All. Gathered. Linking problems begin. One, second, third are not linked. Third-party libraries are not enough. As a result - a lot of lost time on unproductive labor and understanding of used libraries, third-party components and technologies.


Second example:
- Here I need this thing. How do I use it in my project?
“You see, this thing is not separate.” Here you need to download the project and take this file from it, this class, this is clueless, take these projects, put it like that ...
- Ahhh !

Third example:
- Here I need this thing. How do I use it in my project?
- Yes, here is a liba, and here it is necessary to link with it.
- OK thanks.
You connect the lib, but it does not link, there are not enough characters. Once again, the search and assembly of the components on which it depends begins.

Problems that arise, offhand:
1. I collected not what was tested. And in general, it doesn’t matter whether the tested component or not, after rebuilding it still needs to be tested.
2. Assembled with the wrong versions of libraries.
3. Assembled with the wrong versions of third-party components.
4. Assembled with the wrong options.
5. Just collected not what I wanted.
And I needed only one method of one class ... I would have written faster.

Where does evil come from

Component - it is also a component that fully implements a complete model of an object or behavior. Do not interfere with everything in a bunch. Usually, if a component has been fully discussed, designed and developed, then for its testing it is not necessary to include the component in a working system. The component has an interface with which you can do an automatic test, unit test, load test, and much more. Indeed, it’s easier to understand the system, and it’s more convenient to work with it.

It turns out that all the evil is in not understanding the need to separate the interface and implementation. And not misunderstanding the need to hide the implementation. Well, I don’t want to see and study how something works and works there. I need to use the functionality and finish my project.

Let's look at the correct, from my point of view, component design.

Examples will be in C ++.

What the component should look like

In my humble opinion, the component should be a dynamic library with a header file in the kit describing the interfaces implemented by the component (anticipating that the stones will fly, I will inform you that I know about dll-hell, but it will not exist in a properly designed and installed system). You can add .def and .lib files here, depending on the situation, but .dll (.so) and .h are enough in a properly designed component. It is also desirable that our dynamic library be statically linked to runtime libraries. This will save us the trouble of various Redistributable Packages under Windows.

But static libraries cannot be considered components at all. In static libraries, it is better to add different common parts of the implementation for different components, strongly tied to third-party containers, such as STL or boost.
As a result, the finished component should, as it were, fix the implemented and tested functionality, providing a convenient interface for its use.

Interfaces

Consider examples and solutions.
We will not consider static libraries and interfaces, we will immediately pass to dynamic ones.
Bad interface option:

#include 
#ifdef EXPORTS
#define API __declspec( dllexport )
#else
#define API __declspec( dllimport )
#endif
class API Component
{
public:
    const std::string& GetString() const;
private:
    std::string m_sString;
};


What is wrong here? Well, firstly, the implementation is not hidden. We see a member of the class, we see a third-party container, in this case std :: string. Those. we see part of the implementation, and this is bad. Someone will be indignant and say, what kind of third-party is he if this is a standard container? And it’s third-party because the component can use the Microsoft STL implementation, and we want STLPort. And then we will never be able to use such a component directly. Secondly: the interface is not cross-platform. __Declspec instructions are by no means in all compilers. Third: using an explicit link for a component with such an interface is, to put it mildly, difficult.

To solve the first problem, the PIMPL idiom and rejection of external containers with replacing them with built-in types are suitable. To solve the second, we extend the define directives.

#ifdef WIN32
#ifdef EXPORTS
#define API __declspec( dllexport )
#else
#define API __declspec( dllimport )
#endif
#else
#define API
#endif
class ComponentImpl;
class API Component
{
public:
    const char* GetString() const;
private:
    ComponentImpl* m_pImpl;
};


Implementation was hidden, cross-platform added. In fact, standard containers can not be abandoned if the use of the STL implementation in development is strictly regulated. However, using mixed systems may cause problems.

What to do with the simplicity of explicit linking?

To do this, use abstract classes and a factory function.

#ifdef WIN32
#ifdef EXPORTS
#define API __declspec( dllexport )
#else
#define API __declspec( dllimport )
#endif
#else
#define API
#endif
class Component;
extern "C" API Container* GetComponent();
class Component
{
public:
    virtual ~Component() {}
    virtual const char* GetString() const = 0;
};


In this case, we have one single function, whose name is known and does not change. Downloading and linking such a component is very simple. After loading the library, it is enough to get by name and call the GetComponent function. Next, we will have access to all the many methods of the Component interface.

You can go further. If the function returns the interface of the factory class, then we have the opportunity to expand the interface indefinitely, while the procedure for loading the library component will not change.

#ifdef WIN32
#ifdef EXPORTS
#define API __declspec( dllexport )
#else
#define API __declspec( dllimport )
#endif
#else
#define API
#endif
class Factory;
extern "C" API Factory* GetFactory();
class Component;
class Component1;
class Factory
{
public:
    virtual ~Factory() {}
    virtual Component* GetComponent() = 0;
    virtual Component1* GetComponent1() = 0;
};
class Component
{
public:
    virtual ~Component() {}
    virtual const char* GetString() const = 0;
};
class Component1
{
public:
    virtual ~Component1() {}
    virtual const char* GetString() const = 0;
};


Well, as an aerobatics, you can charge something from the COM ideology, which will allow you to expand the functionality of the component unlimitedly, while maintaining full backward compatibility with systems already working with it.

#ifdef WIN32
#ifdef EXPORTS
#define API __declspec( dllexport )
#else
#define API __declspec( dllimport )
#endif
#else
#define API
#endif
class Factory;
extern "C" API Factory* GetFactory();
class Base
{
public:
    virtual ~Base() {}
    virtual void QueryInterface( const char* id, void** ppInterface ) = 0;
    virtual void Release() = 0;
};
class ConnectionPoint
    : public Base
{
public:
    virtual void Bind( const char* id, void* pEvents ) = 0;
    virtual void Unbind( const char* id, void* pEvents ) = 0;
};
class Factory
    : public Base
{
};
static const char* COMPONENT_ID = "Component";
class Component
    : public Base
{
public:
    virtual const char* GetString() const = 0;
};
static const char* COMPONENT1_ID = "Component1";
class Component1
    : public Base
{
public:
    virtual const char* GetString() const = 0;
};


In this case, we can add interfaces to the component, while maintaining backward compatibility. We can extend the interfaces themselves by inheritance, or use them as factories. We can implement ConnectionPoints in the interfaces and expand the possibilities for using event handlers unlimitedly. The memory management in the example is greatly simplified, but it is possible, by analogy with COM, to use reference counting and smart pointers.

COM ideology is often difficult to understand, especially for beginners, but developing interfaces with it allows you to flexibly change interfaces and implement various requirements without violating the integrity of already working projects. The COM approach is completely cross-platform, and it should not confuse developers.

Of course, using a pure COM approach is almost always redundant; it is better to combine it with simple work with abstract classes, as in the previous example.

Often, an abstract factory and a factory function are enough at all, when it is clear that expanding the capabilities of the component in the future is not required.

When there is a clear regulation of compilers, third-party libraries, as well as the understanding that backward compatibility with working systems is not required, the PIMPL idiom is also great for simplicity of the interface.

Finally

A properly designed interface and hiding the implementation greatly helps in the work when reusing the same components. In this case, the component is assembled and tested once, which significantly saves resources for testing and development. It is available as a dynamically loaded library and is always ready to use, without requiring the developer to understand the intricacies of its implementation and compilation. I took the library with the header file and use it, enjoy life.

PS

Properly designed components and interfaces are necessary in Java. But more about that next time.

Also popular now: