- PVSM.RU - https://www.pvsm.ru -
Избавляемся от недостатков классического ООП и пишем на С++ в модульном стиле.
Волею судеб мне пришлось поддерживать и развивать проект средней сложности написанный на С++. Проект написан в классическом ООП стиле и неплохо структурирован по модулям и классам. Нужно сказать, что до этого я провел немало времени разрабатывая проект на Java и Apache Tapestry 5. В частности очень хорошо разобрался с идеологией его IOC контейнера. Поэтому часть идей скопировано оттуда.
Итак проект структурирован, но любое незначительное изменение практически в любом заголовочном файле приводит к перекомпиляции половины проекта. Я не отличаюсь особо большой внимательностью с синтаксическим деталям при написании кода (забывание включения заголовков, пространств имен и т.п. для меня норма), поэтому бывает что приходится исправлять ошибки и перекомпилировать заново 2-3 раза и это отнимает очень много времени. Поэтому я решил внедрить в проект ряд практик по снижению компонентной связности кода о чем и хочу поделится. Сразу хочу сделать предупреждение. Проект требует совместимости с С++ 98, поэтому все что выходит за его рамки реализовано с помощью Boost.
Один из базовых принципов ООП — инкапсуляция. К нему относится правило, что переменная должна быть доступна только там, где она используется. Доступность почти эквивалентна времени жизни автоматических переменных. Поэтому если переменная типа MyStack
является частным членом класса A, то все пользователи класса вынуждены импортировать также заголовок MyStack.h. Если же эта переменная используется только одной функцией и не содержит состояние, то ее нужно сделать вообще статической переменной. Кроме этого не стоит забывать что автоматические переменные живут до конца блока и пользоваться этим для уничтожения более ненужных переменных добавлением скобок блока кода.
Проблему сокрытия реализации частной части класса частично решает указатель на реализацию (Pimpl). Я не хотел бы заново пересказывать в подробностях что-такое Pimpl, так как статей на эту тему можно найти достаточно. Вот, например, у Герба Саттера:
Я сделаю только свои замечания и приведу мою реализацию идиомы.
#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 */
class Impl;
PImpl<Impl> me;
me
позаимствовано из VBA
Impl
первым параметром передается публичный this
и сохраняется в поле ppub
struct
так как имеет область видимости только в текущем модуле.
operator=
не забываем правильно устанавливать me
и ppub
.
PImpl
, то мы можем очень просто заменить настоящую реализацию заглушкой с помощью компоновщика. Тестирование же сокрытой реализации возможно включением в тестовый модуль с помощью директивы #include кода реализации.
------- 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
}
Итак есть класс Cryptor
(обертка для некого CryptoLib
), для которого нужно написать тест и класс Hasher
(обертка для некого HashLib
) от которого зависит Cryptor
. но Cryptor
еще зависит от модулей HashLib
и SecTokens
, а это нам совершенно не нужно для теста Cryptor
. Вместо него подготавливаем MockHasher.cpp.
Код Cryptor.cpp включен в TestCryptor.cpp, поэтому для сборки теста компилируем и компонуем только TestCryptor.cpp и MockHasher.cpp. Я не привожу примеров на базе библиотек модульного тестирования так как это не относится к теме данной статьи.
Тут просто. Заголовок нужно включать как можно позже по ходу разбора кода, но желательно в начале файла. Т.е. если только реализация класса использует сторонний заголовок, то переносим его в модуль реализации класса из заголовка класса.
В проекте есть модуль в который я выношу все платформозависимые функции. Называется он Platform
. Получается модуль с несвязанными между собой функциями, которые я просто объявил в одном пространстве имен platform
. В дальнейшем я собираюсь заменять модуль с реализацией в зависимости от платформ. Но вот беда. Одна из функций должна заполнять пары <ключ, значение> класса (это std::map
, но со специфическим компаратором) объявленного вообще в частной части другого публичного класса Settings
.
Можно вынести частный класс в публичную видимость и разбить заголовок Platform на несколько заголовков. Тогда функция заполнения не будет включена в классы не имеющие отношения к этому заполнению и они не приобретут зависимость от этого std::map
. Я не сторонник плодить заголовочные файлы, кроме этого изменение области видимости шаблонного компаратора с частной на более общую приведет к увеличению компонентной связности. При любом изменении в нем будет перекомпиляция всего, что зависит от платформозависимого заполнителя.
Другой путь это использовать boost::bind
и callback функции. Функция-заполнитель будет принимать указатель на функцию
void fillDefaults(boost::function<void(std::string, std::string) > setDefault);
вместо
void fillDefaults(std::map<std::string, std::string, ci_less>& defaults);
Создаем callback в частной части Settings
:
void setDefault(std::string key, std::string value) {
defaults[key] = value;
}
void fillDefaults() {
platform::fillDefaults(boost::bind(&SettingsManager::Impl::setDefault, this, _1, _2));
}
вместо
void fillDefaults() {
platform::fillDefaults(defaults);
}
Используя pimpl иногда удобнее публичную функцию сделать в виде обертки для одноименной частной. Используя пример выше функцию
void Hasher::execute() {
me->prepare();
me->update(SecTokens::one);
me->update(SecTokens::two);
me->finalize();
}
можно представить как
void Hasher::Impl::execute() {
prepare();
update(SecTokens::one);
update(SecTokens::two);
finalize();
}
void Hasher::execute() {
me->execute();
}
но можно это сделать и с помощью bind функтора
------- 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;
}
Мы избавились от определения функции
Теперь execute может вызываться как и раньше
void f(Hasher& h) {
h.execute();
}
и, например, отправлен на исполнение в отдельный исполнитель
void f(Hasher& h, boost::asio::io_service& executor) {
executor.post(h.execute);
}
вместо
void f(Hasher& h, boost::asio::io_service& executor) {
executor.post(boost::bind(&Hasher::execute, &h));
}
boilerplate объявления функции-обертки трансформировался в boilerplate объявления boost функтора и остался только в конструкторе.
Нужно заметить, что есть и обратная сторона медали. execute
теперь публичное поле класса и к нему может быть случайно присвоено новое значение во время исполнения, чего не может произойти с функцией. Также теперь недоступно обычное переопределение виртуального метода, хотя эта проблема решается просто.
Таким образом получаем прелести функций высшего порядка как в JavaScript.
Еще пару слов о функторах за рамками основной темы. Пусть мы создали функтор и хотим сделать на базе него еще один функтор с меньшим числом аргументов
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));
}
Вот этот вызов boost::bind(functor1, 4, _1) режет глаз. Почему-бы не объединить function pointer и bind, ведь они редко когда используются по отдельности. Тогда код выше приобретет вид:
int main(int argc, char** argv) {
Bindable<void(int, int)> functor1(boost::bind(myFunction, _1, _2));
Bindable<void(int)> functor2(functor1.bind(4, _1));
}
#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 */
Для начала нужно определиться какие виды параметров конструктора могут быть:
Может возникнуть вопрос: “а зачем передавать глобально доступные объекты в конструктор, если можно обратиться к ним в любой момент?”. Да это так. Но есть ряд причин, из-за которых так лучше не делать:
globalStorage.getProfiles().getProfile(“Default”)
. Что-бы не повторять такое выражение, объект или ссылку на него также лучше сохранить в поле класса
Используя абсолютно абстрактные классы как интерфейсы (достаточно заголовочного файла) и создав наследника с нужными параметрами конструктора можно избежать публикования параметров. Для создания экземпляра в этом случае используется фабрика. Это может быть фабричный метод, объявленный в интерфейсе и определенный в модуле реализации, а может быть и самостоятельный класс, объект которого возвращает новый объект или указатель на новый объект.
Я давно склоняюсь к тому, что при возможности выбора использовать наследование или композицию я выбираю композицию. Дополнительно убедился в правильности этого подхода получив ошибку Pure Virtual Function Called
Если в класс внедрена идиома pimpl, то при создании частной реализации можно передать ей в конструктор не параметры конструктора публичной части, а объекты из глобальной области видимости. т.е. в публичном конструкторе не остается параметров глобального значения, только флаги и т.п. параметры которые действительно нужно знать и задавать в участке кода, создающего экземпляр.
Проект содержит около 50 файлов “.cpp” плюс заголовочные файлы. Файлы логически разнесены по каталогам — подсистемам. В коде присутствует ряд глобальных переменных простых типов и объект доступа к разделяемым объектам пользовательских типов. Получение доступа к объектам может выглядеть так
globalStorage.getHoster()->invoke();
или так:
Profile pr=globalStorage.getProfiles()->getProfile(“Default”);
Аналогично рассмотренному выше Platform
все кто использует globalStorage
вынуждены знать что экспортирует интерфейс GlobalStorage
со всеми внешними типами. Но GlobalStorage
должен действительно вернуть объект заданного типа (или реализующего заданный интерфейс) и нет возможности решить проблему как в Platform
.
Итак следующая цель — преобразовать подсистемы во что-то похожее на IOC модули Apache Tapestry 5, упростить доступ к глобальным объектам (в дальнейшем сервисы — по аналоги с сервисовами Tapestry) и вынести конфигурирование сервисов в самостоятельный файл в IOC модуле. В итоге мы получим самые настоящие компоненты (см. Компонентно-ориентированное программирование [5])
Сразу хочу сказать что о полноценном IOC контейнере речь не идет. Описанный пример это только генерализация шаблона Синглтон сервиса и Фабрика. Используя этот подход можно также реализовать Теневые сервисы [6] (поле сервиса представляем как самостоятельный сервис) и другие источники сервисов.
Создаем
#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();
};
теперь вместо
boost::shared_ptr<Hoster> hoster = globalStorage.getHoster();
вызов будет выглядеть
InjectPtr<Hoster> hoster = ioc::resolve<Hoster>();
Как видим эта конструкция не импортирует ничего лишнего. Если в коде нужно получить Hoster
, то следует самостоятельно позаботится о импорте его заголовка. Второй параметр шаблона метода resolve
это идентификатор сервиса. Используется в случае если есть несколько сервисов с одним интерфейсом.
InjectPtr
это умный указатель на объект с отложенной (ленивой) инициализацией. Внутри хранит boost::shared_ptr
на boost::shared_ptr
на хранимый объект. Последний инициализируется при первом разыменовании InjectPtr
. Для создания экземпляра хранимого объекта InjectPtr
получает функтор-фабрику.
#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
потокобезопасный. Во время создания объекта операция блокируется мутексом.
Переходим к файлу конфигурации IOC. Делаем полные специализации шаблонного метода ioc::resolve
------- 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 гарантирует блокировку и при создании static локальной переменной функции. Но стандарт этого не гарантирует. Пришлось изменить код и вынести хранителя InjectPtr
в глобальную статическую переменную, которая наверняка инициализируется еще до запуска кода программы. Можно, конечно и в отдельные переменные, но тогда придется изобретать имя для каждой. Тут CoreStorage
это хранитель для IOC модуля Core:
#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 модуля, но снижает ее при межмодульном взаимодействии.
Для взаимодействия IOC модулей удобно создать интерфейсный заголовок IOC модуля одноименный с самим модулем. Он должен содержать:
Так же удобно иметь частный заголовок модуля, который импортирует публичный и делает:
Автор: slonm
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/pimpl/62405
Ссылки в тексте:
[1] GotW #24: Compilation Firewalls: http://www.gotw.ca/gotw/024.htm
[2] GotW #28: The Fast Pimpl Idiom: http://www.gotw.ca/gotw/028.htm
[3] GotW #100: Compilation Firewalls: http://herbsutter.com/gotw/_100/
[4] GotW #101: Compilation Firewalls, Part 2: http://herbsutter.com/gotw/_101/
[5] Компонентно-ориентированное программирование: http://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%BF%D0%BE%D0%BD%D0%B5%D0%BD%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5
[6] Теневые сервисы: http://tapestry.apache.org/shadowbuilder-service.html
[7] Источник: http://habrahabr.ru/post/226333/
Нажмите здесь для печати.