📜 ⬆️ ⬇️

Reduced component connectivity of C ++ code


We get rid of the drawbacks of the classic OOP and write in C ++ in a modular style.

By the will of fate, I had to maintain and develop a project of average complexity written in C ++. The project is written in the classic OOP style and well structured by modules and classes. I must say that before this I spent a lot of time developing a project in Java and Apache Tapestry 5. In particular, I understood the ideology of its IOC container very well. Therefore, some of the ideas copied from there.
So the project is structured, but any minor change in almost any header file will recompile half of the project. I do not have much attention to syntactic details when writing code (forgetting the inclusion of headers, namespaces, etc. is normal for me), so it happens that you have to correct errors and recompile again 2-3 times and it takes a lot of time. Therefore, I decided to introduce into the project a number of practices to reduce the component code connectivity of what I want to share. Just want to make a warning. The project requires compatibility with C ++ 98, so everything that goes beyond its framework is implemented using Boost.

Variable lifetime


One of the basic principles of OOP is encapsulation. This includes the rule that a variable should only be available where it is used. Availability is almost equivalent to the lifetime of automatic variables. Therefore, if a variable of type MyStack is a private member of class A, then all users of the class must also import the header MyStack.h. If this variable is used by only one function and does not contain a state, then it must be made generally static. In addition, you should not forget that automatic variables live to the end of the block and use this to destroy more unnecessary variables by adding brackets to the block of code.

PImpl


The problem of hiding the implementation of the private part of the class is partially solved by the implementation pointer (Pimpl). I would not like to re-retell in detail what Pimpl is, as there are enough articles on this topic. Here, for example, at the coat of arms of Sutter:

I will only make my comments and give my implementation of the idiom.
  1. Idiom does not hide public constructors that take parameters for implementation. This problem can be solved by combining interfaces and object factories.
  2. Do not forget to move all unnecessary for the public part of the include in the module with the implementation.
  3. To hide the extra code from my eyes, I implemented the PImpl module compatible with C ++ 98
    Code
     #ifndef PIMPL_H #define PIMPL_H ///idea from GotW #101: Compilation Firewalls, Part 2s http://herbsutter.com/gotw/_101/ #include <boost/scoped_ptr.hpp> template<typename T> class PImpl { private: boost::scoped_ptr<T> m; public: PImpl() : m(new T) { } template<typename A1> PImpl(A1& a1) : m(new T(a1)) { } //    2  9  …. template<typename A1, typename A2, typename A3, typename A4, typename A5, typename A6 , typename A7, typename A8, typename A9, typename A10> PImpl(A1& a1, A2& a2, A3& a3 , A4& a4, A5& a5, A6& a6, A7& a7, A8& a8, A9& a9, A10& a10) : m(new T(a1, a2, a3, a4, a5 , a6, a7, a8, a9, a10)) { } PImpl(const PImpl& orig) : m(new T(*orig)) { } T* operator->() const { return m.get(); } T& operator*() const { return *m.get(); } PImpl& operator=(const PImpl& orig) { m.reset(new T(*orig)); return *this; } }; #endif /* PIMPL_H */ 


  4. In all classes, the implementation declaration looks like
     class Impl; PImpl<Impl> me; 

    me borrowed from VBA
  5. If a pointer to the public part is required (to call public methods), then the public this is passed to the Impl constructor Impl first parameter and is stored in the ppub field
  6. An implementation with a full declaration is always declared as a struct since it has scope only in the current module.
  7. An implementation usually has to have constructors and overloaded operators that completely repeat public ones. For copy constructors and operator= don't forget to set me and ppub .
  8. Declarations of Impl functions in Java style. As you know, functions declared and defined immediately in a class are inline functions. We should not forget that inline is only a tip to the compiler, which it may not take into account, therefore, most likely, large functions will not be inline, but there will be less boilerplate for declarations and definitions of functions.
  9. About unit testing. As it is known, in unit testing, it is often required to have stubs instead of the implementations on which the module under test depends. If the object on which our code depends is implemented with PImpl , then we can very simply replace the current implementation with a stub using the linker. Testing the same hidden implementation is possible by including the implementation code in the test module using the #include directive.
    A comprehensive example of the above
     ------- Hasher.h ------ #include <PImpl.h> class Hasher { class Impl; //   class  struct    PImpl<Impl> me; //   public: Hasher(); void execute(); int getResults(); }; ------- Hasher.cpp ------ #include “Hasher.h” #include <HashLib.h> #include “SecTokens.h” // . struct      struct Hasher::Impl { Hasher* ppub; //    HashContext cnt; int hash; Impl(Hasher* ppub): ppub(ppub) { } void prepare() { HashLib::createContext(cnt); hash = 0; } void update(int val) { HashLib::updateHash(cnt, hash, val); } void finalize() { HashLib::releaseContext(cnt); } }; Hasher::Hasher(): me(this) { //     } void Hasher::execute() { me->prepare(); me->update(SecTokens::one); me->update(SecTokens::two); me->finalize(); } int Hasher::getResults(){ return me->hash; } ------- Cryptor.h ------ #include <string> #include <PImpl.h> class Cryptor { class Impl; PImpl<Impl> me; public: Cryptor(std::string salt); std::string crypt(std::string plain); }; ------- Cryptor.cpp ------ #include <CryptoLib.h> #include “Cryptor.h” struct Cryptor::Impl { std::string salt; CryptoContext cnt; Impl(std::string salt): me(salt) { } void prepare() { CryptoLib::createContext(cnt); } void update(std::string plain) { CryptoLib::updateHash(cnt, plain); } std::string finalize() { return CryptoLib::releaseContext(cnt); } }; Cryptor::Cryptor(std::string salt): me(salt) { } std::string Cryptor::crypt(std::string plain) { me->prepare(); me->update(plain); return me->finalize(); } ------- MockHasher.cpp ------ #include “Hasher.h” struct Hasher::Impl { }; void Hasher::execute() { } int Hasher::getResults(){ return 4; } ------- TestCryptor.cpp ------ #include “Cryptor.cpp” int main(int argc, char** argv) { Cryptor::Impl impl(“salt”); impl.prepare(); //   impl  prepare impl.update(“text”); //   impl  update std::string crypto=impl.finalize(); //    crypto } 


    So there is a class Cryptor (a wrapper for a certain CryptoLib ) for which you need to write a test and a class Hasher (a wrapper for a certain HashLib ) on which Cryptor depends. but Cryptor still depends on the HashLib and SecTokens , and this is absolutely not necessary for us to test Cryptor . Instead, prepare MockHasher.cpp.
    The Cryptor.cpp code is included in TestCryptor.cpp, so to build the test, we compile and compile only TestCryptor.cpp and MockHasher.cpp. I do not cite examples based on unit testing libraries as this is not the case for this article.

')

Revise the inclusion of header files


It's simple. The header should be included as late as possible during the parsing of the code, but preferably at the beginning of the file. Those. if only the class implementation uses a third-party header, then transfer it to the class implementation module from the class header.

Callbacks and functors instead of public functions


There is a module in the project in which I carry out all platform-specific functions. It is called Platform . It turns out the module with functions unrelated to each other, which I simply declared in the same namespace platform . In the future, I am going to replace the module with implementation depending on the platforms. But the trouble is. One of the functions should fill in the <key, value> class pairs (this is std::map , but with a specific comparator) declared generally in the private part of another public class Settings .
You can bring a private class into public visibility and break the Platform header into several headers. Then the fill function will not be included in classes unrelated to this fill and they will not become dependent on this std::map . I am not in favor of producing header files, except that changing the scope of the template comparator from a private to a more general one will result in an increase in component connectivity. Any change in it will recompile everything that depends on the platform-specific placeholder.
Another way is to use the boost::bind and callback functions. The placeholder function will take a pointer to the function.
 void fillDefaults(boost::function<void(std::string, std::string) > setDefault); 

instead
 void fillDefaults(std::map<std::string, std::string, ci_less>& defaults); 

Create a callback in the private part of Settings :
  void setDefault(std::string key, std::string value) { defaults[key] = value; } void fillDefaults() { platform::fillDefaults(boost::bind(&SettingsManager::Impl::setDefault, this, _1, _2)); } 

instead
  void fillDefaults() { platform::fillDefaults(defaults); } 

Using pimpl, it is sometimes more convenient to make a public function in the form of a wrapper for the same private. Using the example above function
 void Hasher::execute() { me->prepare(); me->update(SecTokens::one); me->update(SecTokens::two); me->finalize(); } 

can be represented as
 void Hasher::Impl::execute() { prepare(); update(SecTokens::one); update(SecTokens::two); finalize(); } void Hasher::execute() { me->execute(); } 

but you can do it with the bind functor
 ------- Hasher.h ------ #include <boost/functions.hpp> #include <PImpl.h> class Hasher { class Impl; //   class  struct    PImpl<Impl> me; //   public: Hasher(); boost::function<void()> execute; int getResults(); }; ------- Hasher.cpp ------ //……... Hasher::Hasher(): me(this), execute(boost::bind(&Hasher::Impl::execute, &*me)) { } int Hasher::getResults(){ return me->hash; } 

We got rid of the function definition
Now execute can be called as before.
 void f(Hasher& h) { h.execute(); } 

and, for example, sent to a performance in a separate performer
 void f(Hasher& h, boost::asio::io_service& executor) { executor.post(h.execute); } 

instead
 void f(Hasher& h, boost::asio::io_service& executor) { executor.post(boost::bind(&Hasher::execute, &h)); } 

The boilerplate of the wrapper function declaration transformed into the boilerplate of the ad boost functor and remained only in the constructor.
It should be noted that there is a downside. execute now a public class field and a new value can be randomly assigned to it during execution, which cannot happen to the function. Also, the usual redefinition of the virtual method is no longer available, although this problem is solved simply.
Thus we get the pleasures of higher-order functions as in JavaScript.
A few words about functors beyond the main theme. Suppose we have created a functor and want to make another functor based on it with a smaller number of arguments.
 void myFunction(int, int); int main(int argc, char** argv) { boost::function<void(int, int)> functor1(boost::bind(myFunction, _1, _2)); boost::function<void(int)> functor2(boost::bind(functor1, 4, _1)); } 

Here this call boost :: bind (functor1, 4, _1) hurts the eye. Why not combine function pointer and bind, because they are rarely used separately. Then the code above will look like:
 int main(int argc, char** argv) { Bindable<void(int, int)> functor1(boost::bind(myFunction, _1, _2)); Bindable<void(int)> functor2(functor1.bind(4, _1)); } 

Bindable code
 #ifndef BINDABLE_H #define BINDABLE_H #include <boost/bind.hpp> #include <boost/function.hpp> template<typename Signature> struct Bindable : public boost::function<Signature> { Bindable() { } template<typename T> Bindable(const T& fn) : boost::function<Signature>(fn) { } template<typename NewSignature, typename A1> Bindable<NewSignature> bind(const A1& a1) { return boost::bind(this, a1); } //    2  9  template<typename NewSignature, typename A1, typename A2, typename A3, typename A4, typename A5, typename A6, typename A7, typename A8, typename A9, typename A10> Bindable<NewSignature> bind(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5, const A6& a6, const A7& a7, const A8& a8, const A9& a9, const A10& a10) { return boost::bind(*this, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); } }; #endif /* BINDABLE_H */ 


Hiding Designer Parameters


First you need to decide what types of constructor parameters can be:
  1. configuration parameters for a particular instance of the instance. Usually these are parameters of a simple type - flags, strings, metrics. However, there is no possibility to hide these parameters;
  2. objects derived from the global scope for the work implementation. Here we will hide them.

A question may arise: “why transfer the globally accessible objects to the constructor, if you can refer to them at any time?”. Yes it is. But there are several reasons why it is better not to do this:
  1. retrieving a global object can be a resource-intensive operation, then it’s better to cache it in the class field
  2. retrieving a global object can have complex syntax, for example globalStorage.getProfiles().getProfile(“Default”) . In order not to repeat such an expression, an object or a link to it is also best stored in the class field.
  3. You may need to modify a copy of the global object. Then the copy must also be in the class field.
  4. it may be necessary to replace the object used for debugging purposes. Then only one extraction and assignment call to the class field is changed.


Inheritance. Factories and interfaces

Using absolutely abstract classes as interfaces (a header file is enough) and creating a descendant with the necessary constructor parameters, you can avoid publishing parameters. In this case, a factory is used to create an instance. This may be a factory method, declared in the interface and defined in the implementation module, or it may be an independent class whose object returns a new object or a pointer to a new object.
I have long been inclined to think that if I can use inheritance or composition, I choose composition. Additionally, I was convinced of the correctness of this approach by getting the error Pure Virtual Function Called

Composition

If the idiom pimpl is embedded in the class, then when creating a private implementation, you can pass to the constructor not the parameters of the public part constructor, but objects from the global scope. those. In a public constructor, there are no global value parameters, only flags, etc. the parameters that you really need to know and set in the area of ​​the code that creates the instance.

Structuring files, modularity and lazy initialization


The project contains about 50 “.cpp” files plus header files. Files are logically distributed in directories - subsystems. The code contains a number of global variables of simple types and an object to access shared objects of user types. Accessing objects may look like this.
 globalStorage.getHoster()->invoke(); 

or so:
 Profile pr=globalStorage.getProfiles()->getProfile(“Default”); 

Similar to the Platform discussed above, all who use globalStorage forced to know that the GlobalStorage interface is GlobalStorage with all external types. But GlobalStorage should really return an object of a given type (or implementing a specified interface) and there is no way to solve the problem as in Platform .

So, the next goal is to convert the subsystems into something similar to Apache Tapestry 5 IOC modules, simplify access to global objects (hereinafter services are analogous to Tapestry services) and transfer the configuration of services into a separate file in the IOC module. As a result, we get the most real components (see Component-oriented programming )
Just want to say that we are not talking about a full-fledged IOC container. The described example is only a generalization of the Singleton service template and the Factory. Using this approach, you can also implement Shadow services (the field of service is presented as an independent service) and other sources of services.

Configuration services module IOC

Create
header IOC.h
 #include "InjectPtr.h" ///Helper interface class. Only for visual marking of needed methods. ///We can't do virtual template members namespace ioc { ///methods like http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceBuilderMethods ///Like public @InjectService or @Inject annotation ///ServiceId Case http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceIds template<typename T, size_t ID> InjectPtr<T> resolve(); ///Singleton or factory case template<typename T> InjectPtr<T> resolve(); }; 


now instead
 boost::shared_ptr<Hoster> hoster = globalStorage.getHoster(); 

the call will look like
 InjectPtr<Hoster> hoster = ioc::resolve<Hoster>(); 

As you can see, this design does not import anything extra. If you need to get Hoster in the code, you should take care of importing its header yourself. The second parameter of the resolve method template is the service identifier. It is used in case there are several services with one interface.

InjectPtr is a smart pointer to an object with delayed (lazy) initialization. Inside stores boost::shared_ptr on boost::shared_ptr on a stored object. The latter is initialized during the first dereference of InjectPtr . To create an instance of a stored object, InjectPtr gets a functor factory.
InjectPtr code
 #ifndef INJECT_PTR_H #define INJECT_PTR_H #include <cassert> #include <cstddef> #include <boost/shared_ptr.hpp> #include <boost/scoped_ptr.hpp> #include <boost/make_shared.hpp> #include <boost/function.hpp> #include <boost/thread/mutex.hpp> ///Pointer to lazy instantiative object template<typename T> class InjectPtr { private: typedef boost::function<T*() > Factory; boost::shared_ptr< boost::shared_ptr<T> > px; boost::shared_ptr< boost::scoped_ptr<boost::mutex> > instantiateMutex; Factory factory; public: ///Main constructor. Take factory for future instantiate object InjectPtr(Factory factory) : px(boost::make_shared<boost::shared_ptr<T> >()) , instantiateMutex(boost::make_shared<boost::scoped_ptr<boost::mutex> >(new boost::mutex)) , factory(factory) { } InjectPtr() : px(boost::make_shared<boost::shared_ptr<T> >()) , instantiateMutex(boost::make_shared<boost::scoped_ptr<boost::mutex> >()) { } InjectPtr(boost::shared_ptr<T> pObject) : px(boost::make_shared<boost::shared_ptr<T> >(pObject)) { assert(*px != 0); } InjectPtr(InjectPtr const &orig) : px(orig.px) , instantiateMutex(orig.instantiateMutex) , factory(orig.factory) { } InjectPtr & operator=(InjectPtr const & orig) { px = orig.px; instantiateMutex = orig.instantiateMutex; factory = orig.factory; return *this; } virtual ~InjectPtr() { } T & operator*() { instantiate(); return **px; } T * operator->() { instantiate(); return &**px; } bool operator!() const { return !*px; } void operator==(InjectPtr const& that) const { return *px == that->px; } void operator!=(InjectPtr const& that) const { return *px != that->px; } boost::shared_ptr<T> sharedPtr() { instantiate(); return *px; } void instantiate() { if (!*px && factory) { { boost::mutex::scoped_lock lock(**instantiateMutex); if (!*px) { px->reset(factory()); } } instantiateMutex->reset(); } } Factory getFactory() const { return factory; } void setFactory(Factory factory) { if(!*px && !this->factory){ if(!*instantiateMutex) instantiateMutex->reset(new boost::mutex); this->factory = factory; } } }; template<class T, class U> InjectPtr<T> static_pointer_cast(InjectPtr<U> r) { return InjectPtr<T>(boost::static_pointer_cast<T>(r.sharedPtr())); } #endif /* INJECT_PTR_H */ 


InjectPtr thread safe. During the creation of the object, the operation is blocked by the mutex.
Go to the IOC configuration file. We do full specializations of a ioc::resolve method ioc::resolve
Code
 ------- IOCModule.h ------ //      #ifndef IOCMODULE_H #define IOCMODULE_H #include <boost/functional/factory.hpp> #include <boost/bind.hpp> #include <IOC.h> #endif /* IOCMODULE_H */ ------- IOCModule.cpp ------ #include "Hoster.h" #include "SomeService.h" #include "InjectPtr.h" #include <IOCModule.h> #include <IOC.h> //Module like http://tapestry.apache.org/tapestry-ioc-modules.html //Now only for: - To provide explicit code for building a service using namespace ioc; ///methods like http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceBuilderMethods template<> InjectPtr<SomeService> resolve<SomeService>() { static InjectPtr<Hoster> result(boost::bind(boost::factory<SomeService*>())); return result; } ///Hoster takes SomeService in constructor template<> InjectPtr<Hoster> resolve<Hoster>() { static InjectPtr<Hoster> result(boost::bind(boost::factory<Hoster*>(), resolve<SomeService>())); return result; } 


GCC guarantees locking even when creating a static local variable of a function. But the standard does not guarantee this. I had to change the code and remove the InjectPtr keeper into a global static variable, which is probably initialized before the program code was launched. You can, of course, in separate variables, but then you have to invent a name for each. Here CoreStorage is the keeper for the Core IOC module:
IOCModule.cpp
 #include "Hoster.h" #include "SomeService.h" #include "InjectPtr.h" #include <IOCModule.h> #include <IOC.h> //Module like http://tapestry.apache.org/tapestry-ioc-modules.html //Now only for: - To provide explicit code for building a service using namespace ioc; struct CoreStorage { InjectPtr<SomeService> someService; InjectPtr<Hoster> hoster; }; static CoreStorage storage; ///methods like http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceBuilderMethods template<> InjectPtr<SomeService> resolve<SomeService>() { if(!storage.someService.getFactory()) { storage.someService.setFactory(boost::bind(boost::factory<SomeService*>())); } return storage.someService; } ///Hoster takes SomeService in constructor template<> InjectPtr<Hoster> resolve<Hoster>() { if(!storage.hoster.getFactory()) { storage.hoster.setFactory(boost::bind(boost::factory<Hoster*>(), resolve<SomeService>())); } return storage.hoster; } 


IOC module header files

This clause slightly increases the component connectivity within the IOC module, but reduces it during inter-module interaction.

For the interaction of IOC modules, it is convenient to create an interface header of the IOC module of the same name with the module itself. It should contain:

It is also convenient to have a private module header that imports a public one and does:

Source: https://habr.com/ru/post/226333/


All Articles