Introduction
Recently, I very often come across a situation when one has to deal with a lack of understanding by developers of the need to split systems into components. And if this understanding is, then there is a lack of understanding of hiding the implementation of the component. And we can't talk about the convenience of interfaces.
Hereinafter, we mean components in the sense of parts of a single process. Components that are separate processes or services that are handled through RPC or something else are not considered here. Although to a large extent all of the below applies to them.
Example one:
I need a component that has long been developed and tested in another department. I went to the developer. Dialogue begins:
“I need this thing.” How can I use it in my project?
- Yes, you need to fill in the project from CVS for this tag. Compile It turns out, and here with it it is necessary to link.
- OK thanks.
Siphon project. Compile. A lot of errors come up, there are not enough inclusions. You start to find out. It turns out to build a project you need to deflate a bunch of projects from CVS, to build them too. Some are built as standard by the studio, some with a tambourine, like autoconf, make and their ilk. Everything. Gathered. Begin problems with linking. Do not link one, second, third. There are not enough third-party libraries. The result - a lot of wasted time on unproductive work and understanding of the used libraries, third-party components and technologies.
')
Example two:
“I need this thing.” How can I use it in my project?
- You see, this thing is not separately. Here you need to download the project and take this file from it, this class, download this project, take these projects, put it like this ...
- Aaaa!
Example three:
“I need this thing.” How can I use it in my project?
- Yes, here is, and here you need to link with it.
- OK thanks.
You connect, and it does not link, there are not enough characters. Again begins the search and assembly of the components on which it depends.
Arising problems, offhand:
1. Collected not what they tested. Yes, and in general, no matter whether the component is tested or not, after rebuilding it should still be tested.
2. Collected not with those versions of libraries.
3. Collected not with those versions of third-party components.
4. Assembled with the wrong options.
5. Just collected not what I wanted.
But only one method of one class was needed ... I would have written faster myself.
Where does evil come from
A component is a component and it fully implements the complete model of an object or behavior. Do not interfere with everything in the pile. Usually, if a component has been fully discussed, designed and developed, then to test it, it is not necessary to include the component in a working system. The component has an interface with which you can make an automatic test, a unit test, a load test, and much more. After all, it is easier to understand the system and it is more convenient to work with it.
It turns out that all evil is in not understanding the need to separate the interface and implementation. And not a misunderstanding of the need to hide the implementation. Well, I do not want to see and study how something works there and works. I need to use the functionality and finish my project.
Let's consider the correct, from my point of view, the design of the components.
Examples will be in C ++.
What should the component 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’ll inform you that I know about the dll-hell, but it will not be in the correct and installed system). Depending on the situation, you can add .def and .lib files here, but in a properly designed component, there is enough .dll (.so) and .h. It is also desirable that our dynamic library be statically linked with runtime libraries. This will save us from the problem of various Redistributable Packages under Windows.
But static libraries in general can not be considered components. 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 turn to dynamic ones.
Bad interface option:
#include <string> #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, first, the implementation is not hidden. We see a member of the class, we see a third-party container, in this case, std :: string. Those. see part of the implementation, and this is bad. Someone will be indignant and say, what kind of third-party is it, if it is a standard container? And third-party, because the component can use the Microsoft STL implementation, and we want STLPort. And never then can we use such a component directly. Secondly: the interface is not cross-platform. The __declspec instructions are by no means all compilers. Third: the use of explicit linking for a component with such an interface is, to put it mildly, difficult.
To solve the first problem, the PIMPL idiom and the abandonment of external containers, replacing them with built-in types, will work. To solve the second, let's 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; };
Realization hid, cross-platform added. It is actually possible not to refuse standard containers if the use of the STL implementation in development is strictly regulated. However, when using mixed systems, problems are possible.
How to deal 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. Download and link this component is very simple. After loading the library, it suffices to get by name and call the GetComponent function. Next, we get 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, and 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 aerobatics, you can charge something from the COM ideology, which will allow you to expand the functionality of the component indefinitely, while maintaining full backward compatibility with the systems that are 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 ConnectionPoint in interfaces and unlimitedly expand the possibilities for using event handlers. 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 using it allows you to flexibly change interfaces and implement various requirements without disturbing the integrity of already working projects. The COM approach is fully cross-platform, and it should not embarrass developers.
Of course, the use of a pure COM-approach is almost always unnecessary; it is better to combine it with simple work with abstract classes, as in the previous example.
Often, it is enough to have an abstract factory and a factory function, when it is clear that the component will not need to be expanded in the future.
When there is a clear regulation of compilers, third-party libraries, and the understanding that backward compatibility with running systems is not required, PIMPL is an idiom for simplicity of the interface.
Finally
A properly designed interface and hiding the implementation greatly helps in working with repeated use of the same components. In this case, the component is assembled and tested once, which significantly saves resources for testing and development. It is available in the form of a dynamically loaded library and is always ready for use, without obliging the developer to understand the intricacies of its implementation and compilation. Took the library with the header file and use, enjoy life.
PS
Properly formatting components and interfaces is also necessary in Java. But more about that next time.