Decreasing component connectivity of C ++ code


We get rid of the shortcomings of classical OOP and write in C ++ in a modular style.

By the will of fate, I had to support and develop a project of medium complexity written in C ++. The project is written in the classic OOP style and is well structured by modules and classes. I must say that before that 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 ideas are copied from there.
So the project is structured, but any minor change in almost any header file leads to recompilation of half of the project. I am not particularly careful with the syntactic details when writing code (forgetting to include headers, namespaces, etc. is normal for me), so it happens that you have to fix errors and recompile again 2-3 times and it takes a lot of time. Therefore, I decided to introduce a number of practices to reduce component code connectivity into the project, which I want to share. I want to make a warning right away. The project requires compatibility with C ++ 98, so everything that goes beyond its scope is implemented using Boost.

Variable lifetime


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

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 retell in detail what Pimpl is, since there are enough articles on this subject. For example, the coat of arms of Sutter:

I will only make my comments and give my implementation of the idiom.
  1. The 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 a PImpl module compatible with C ++ 98
    The code
    #ifndef PIMPL_H
    #define	PIMPL_H
    ///idea from GotW #101: Compilation Firewalls, Part 2s http://herbsutter.com/gotw/_101/
    #include 
    template
    class PImpl {
    private:
      boost::scoped_ptr m;
    public:
      PImpl() : m(new T) {
      }
      template 
      PImpl(A1& a1) : m(new T(a1)) {
      }
    //тут объявления оберток от 2 до 9 параметров
    ….
      template 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 me;
    

    me borrowed from VBA
  5. If a pointer to the public part is required (to call public methods), then the Implpublic parameter is passed to the constructor as the first parameter thisand stored in the fieldppub
  6. An implementation with a full declaration is always declared as structit has scope only in the current module.
  7. An implementation usually needs to have constructors and overloaded operators that completely repeat the public ones. For copy constructors, and operator=do not forget to set meand correctly ppub.
  8. Java style Impl declarations. As you know, functions declared and defined immediately in a class are inline functions. We should not forget that inline is only advice 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 is known, unit testing often requires 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 easily replace the real implementation with a stub using the linker. Testing the 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 
    class Hasher {
      class Impl; //Предварительное объявление реализации class или struct не имеет значения
      PImpl me; //Указатель на реализацию
    public:
      Hasher();
      void execute();
      int getResults();
    };
    ------- Hasher.cpp ------
    #include “Hasher.h”
    #include 
    #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 
    #include 
    class Cryptor {
      class Impl;
      PImpl me;
    public:
      Cryptor(std::string salt);
      std::string crypt(std::string plain);
    };
    ------- Cryptor.cpp ------
    #include 
    #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 some CryptoLib) for which you need to write a test and a class Hasher(a wrapper for some HashLib) that depends on it Cryptor. but it Cryptoralso depends on the modules HashLiband SecTokens, and we absolutely do not need this for the test Cryptor. Instead, prepare MockHasher.cpp.
    The Cryptor.cpp code is included in TestCryptor.cpp, so to build the test we compile and compose only TestCryptor.cpp and MockHasher.cpp. I do not give examples based on unit testing libraries since this is not the topic of this article.


Revision of the inclusion of header files


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

Callbacks and functors instead of public functions


The project has a module in which I make all platform-dependent functions. It is called Platform. It turns out a module with unrelated functions that I just declared in the same namespace platform. In the future, I am going to replace the module with the implementation depending on the platforms. But here is the trouble. One of the functions should fill in the <key, value> pairs of the class (this std::map, but with a specific comparator) declared generally in the private part of another public class Settings.
You can make a private class public and split the Platform header into multiple headers. Then the filling function will not be included in classes that are not related to this filling and they will not become dependent on itstd::map. I am not a supporter of producing header files, except that changing the scope of the template comparator from private to more general will increase component connectivity. Any change in it will recompile everything that depends on the platform-specific placeholder.
Another way is to use boost::bindcallback functions. The placeholder function will take a pointer to a function
void fillDefaults(boost::function setDefault);

instead
void fillDefaults(std::map& defaults);

Create a callback in the private part 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 is sometimes more convenient to make a public function in the form of a wrapper for the private of the same name. Using the example above function
void Hasher::execute() {
  me->prepare();
  me->update(SecTokens::one);
  me->update(SecTokens::two);
  me->finalize();
}

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

but you can do this with the bind functor
------- Hasher.h ------
#include 
#include 
class Hasher {
  class Impl; //Предварительное объявление реализации class или struct не имеет значения
  PImpl me; //Указатель на реализацию
public:
  Hasher();
  boost::function 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 for execution to 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 declaration of the wrapper function was transformed into the boilerplate declaration of the boost functor declaration and remained only in the constructor.
It should be noted that there is a flip side to the coin. executeNow the public field of the class and a new value can be accidentally assigned to it during execution, which cannot happen with the function. Also, the usual overriding of the virtual method is no longer available, although this problem can be solved simply.
Thus, we get the charms of higher-order functions as in JavaScript.
A few words about functors beyond the main topic. Let us create a functor and want to make another functor based on it with fewer arguments
void myFunction(int, int);
int main(int argc, char** argv) {
  boost::function functor1(boost::bind(myFunction, _1, _2));
  boost::function functor2(boost::bind(functor1, 4, _1));
}

This call to 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 take the form:
int main(int argc, char** argv) {
  Bindable functor1(boost::bind(myFunction, _1, _2));
  Bindable functor2(functor1.bind(4, _1));
}

Bindable Code
#ifndef BINDABLE_H
#define	BINDABLE_H
#include 
#include 
template
struct Bindable : public boost::function {
  Bindable() {
  }
  template
  Bindable(const T& fn)
  : boost::function(fn) {
  }
  template
  Bindable bind(const A1& a1) {
    return boost::bind(this, a1);
  }
//тут объявления оберток от 2 до 9 параметров
  template
  Bindable 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 */


Concealing constructor options


First you need to decide what types of designer parameters can be:
  1. configuration parameters for a particular instance application. Usually these are parameters of a simple type - flags, lines, metrics. Be that as it may, these options cannot be hidden;
  2. objects obtained from the global scope for the implementation work. Here we will hide them.

The question may arise: “why transfer globally accessible objects to the constructor if you can access them at any time?”. Yes it is. But there are a number of reasons because of which it is better not to do so:
  1. retrieving a global object can be a resource-intensive operation, then it is 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 reference to it is also better to save in the class field
  3. You may need to modify a copy of the global object. Then the copy should also be in the class field
  4. it may be necessary to replace the used object for debugging purposes. Then only one call to the extraction and assignment to the class field changes.


Inheritance. Factories and interfaces

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

Composition

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

File structuring, modularity and lazy initialization


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

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

Similarly to the above, Platformall who use are globalStorageforced to know that they are exporting an interface GlobalStoragewith all external types. But it GlobalStorageshould really return an object of a given type (or that implements a given interface) and there is no way to solve the problem as in Platform.

So, the next goal is to transform the subsystems into something similar to the Apache Tapestry 5 IOC modules, simplify access to global objects (hereinafter the services will be similar to Tapestry services) and transfer the configuration of services to a separate file in the IOC module. As a result, we get the real components (see Component-Oriented Programming )
I want to say right away that we are not talking about a full-fledged IOC container. The described example is just a generalization of the Singleton service template and Factory. Using this approach, one can also implement Shadow services (we represent the service field as an independent service) and other sources of services.

IOC module service configuration

Create
IOC.h header
#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
      InjectPtr resolve();
      ///Singleton or factory case
      template
      InjectPtr resolve();
    };


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

the call will look
InjectPtr hoster = ioc::resolve();

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

InjectPtrThis is a smart pointer to an object with delayed (lazy) initialization. Inside stores boost::shared_ptron boost::shared_ptrto a stored object. The latter is initialized at the first dereferencing InjectPtr. To create an instance of a stored object, it InjectPtrreceives a functor factory.
InjectPtr Code
#ifndef INJECT_PTR_H
#define	INJECT_PTR_H
#include 
#include 
#include 
#include 
#include 
#include 
#include 
    ///Pointer to lazy instantiative object
    template class InjectPtr {
    private:
      typedef boost::function Factory;
      boost::shared_ptr< boost::shared_ptr > px;
      boost::shared_ptr< boost::scoped_ptr > instantiateMutex;
      Factory factory;
    public:
      ///Main constructor. Take factory for future instantiate object
      InjectPtr(Factory factory)
      : px(boost::make_shared >())
      , instantiateMutex(boost::make_shared >(new boost::mutex))
      , factory(factory) {
      }
      InjectPtr()
      : px(boost::make_shared >())
      , instantiateMutex(boost::make_shared >()) {
      }
      InjectPtr(boost::shared_ptr pObject)
      : px(boost::make_shared >(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 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 InjectPtr static_pointer_cast(InjectPtr r) {
      return InjectPtr(boost::static_pointer_cast(r.sharedPtr()));
    }
#endif	/* INJECT_PTR_H */


InjectPtrthread safe. During object creation, the operation is blocked by the mutex.
Go to the IOC configuration file. Making full template method specializationsioc::resolve
The code
------- IOCModule.h ------
//Этот файл один на все модули
#ifndef IOCMODULE_H
#define IOCMODULE_H
#include 
#include 
#include 
#endif	/* IOCMODULE_H */
------- IOCModule.cpp ------
#include "Hoster.h"
#include "SomeService.h"
#include "InjectPtr.h"
#include 
#include 
//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 resolve() {
  static InjectPtr result(boost::bind(boost::factory()));
  return result;
}
///Hoster takes SomeService in constructor
template<> InjectPtr resolve() {
  static InjectPtr result(boost::bind(boost::factory(), resolve()));
  return result;
}


GCC also guarantees blocking when creating a static local variable of a function. But the standard does not guarantee this. I had to change the code and move the keeper InjectPtrto a global static variable, which was probably initialized even before the program code was run. You can, of course, in separate variables, but then you have to invent a name for each. Here CoreStorageit is the keeper for the Core IOC module:
IOCModule.cpp
#include "Hoster.h"
#include "SomeService.h"
#include "InjectPtr.h"
#include 
#include 
//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;
  InjectPtr hoster;
};
static CoreStorage storage;
///methods like http://tapestry.apache.org/defining-tapestry-ioc-services.html#DefiningTapestryIOCServices-ServiceBuilderMethods      
template<> InjectPtr resolve() {
  if(!storage.someService.getFactory()) {
     storage.someService.setFactory(boost::bind(boost::factory()));
  }
  return storage.someService;
}
///Hoster takes SomeService in constructor
template<> InjectPtr resolve() {
  if(!storage.hoster.getFactory()) {
     storage.hoster.setFactory(boost::bind(boost::factory(), resolve()));
  }
  return storage.hoster;
}


IOC module header files

This item slightly increases the component connectivity within the IOC module, but reduces it during intermodular interaction.

For the interaction of IOC modules, it is convenient to create an interface header for the IOC module of the same name with the module itself. It should contain:
  • inclusion of public at the IOC level module of class interfaces;
  • full declarations of public at the IOC level module of transfers and simple structures;
  • public at the IOC level of the preprocessor definition module.

It is also convenient to have a private module header that imports the public one and does:
  • preliminary announcements of all classes of the project;
  • full declarations of internal transfers for IOC module and simple structures;
  • internal to the IOC preprocessor definition module.

Also popular now: