Вступление
В последнее время очень часто сталкиваюсь с ситуацией, когда приходится иметь дело с непониманием разработчиками необходимости дробления систем на компоненты. А если это понимание есть, то обнаруживается непонимание сокрытия реализации компонента. А уж об удобстве интерфейсов и говорить не приходится.
Здесь и далее имеются в виду компоненты в смысле частей одного одного процесса. Компоненты, являющиеся отдельными процессами или сервисами, работа с которыми идет через RPC или еще как-то, здесь не рассматриваются. Хотя в большой мере все нижесказанное относится и к ним.
Пример первый:
Мне нужен компонент, давно разработанный и протестированный в другом отделе. Подхожу к разработчику. Завязывается диалог:
— Вот мне нужна вот эта штука. Как мне ее использовать в своем проекте?
— Да, вот нужно залить проект из CVS вот по этой метке. Скомпилить. Получится либа, и вот с ней нужно линковаться.
— Ок, спасибо.
Выкачиваешь проект. Компилишь. Вылезает куча ошибок, не хватает каких-то инклудов. Начинаешь выяснять. Оказывается для сборки проекта надо выкачать из CVS еще кучу проектов, их тоже собрать. Некоторые собираются стандартно студией, некоторые с бубном, вроде autoconf, make и иже с ними. Все. Собралось. Начинаются проблемы с линковкой. Не линкуется одно, второе, третье. Сторонних библиотек не хватает. В итоге — куча потерянного времени на непроизводительный труд и вникание в использованные библиотеки, сторонние компоненты и технологии.
Пример второй:
— Вот мне нужна вот эта штука. Как мне ее использовать в своем проекте?
— Понимаешь, этой штуки отдельно нет. Вот надо скачать проект и взять из него вот этот файлик, вот этот класс, вот это заинклудить, вот эти проекты взять, вот так положить…
— А-а-а!
Пример третий:
— Вот мне нужна вот эта штука. Как мне ее использовать в своем проекте?
— Да, вот либа, и вот с ней нужно линковаться.
— Ок, спасибо.
Подключаешь либу, а она не линкуется, не хватает символов. Опять начинается поиск и сборка компонентов, от которых она зависит.
Возникающие проблемы, навскидку:
1. Собрал не то, что протестировали. Да и в общем-то без разницы, протестированный компонент или нет, после пересборки его все равно надо тестировать.
2. Собрал не с теми версиями библиотек.
3. Собрал не с теми версиями сторонних компонентов.
4. Собрал не с теми опциями.
5. Просто собрал не то, что хотел.
А нужен был всего-то один метод одного класса… быстрее сам бы написал.
Откуда зло
Компонент — он на то и компонент, что полностью реализует законченную модель какого либо объекта или поведения. Не надо мешать все в кучу. Обычно, если компонент был полностью обсужден, спроектирован и разработан, то для его тестирования не нужно включения компонента в работающую систему. Компонент имеет интерфейс, с использованием которого и можно сделать автоматический тест, юнит тест, нагрузочный тест и много чего еще. Ведь так и понимать систему проще, и работать с ней удобнее.
Получается, что все зло в непонимании необходимости разделения интерфейса и реализации. И не непонимания необходимости реализацию скрывать. Ну не хочу я видеть и изучать, как там что-то работает и устроено. Мне надо функционал задействовать и свой проект закончить.
Давайте рассмотрим правильное, с моей точки зрения, оформление компонентов.
Примеры будут на C++.
Как должен выглядеть компонент
По моему скромному мнению, компонент должен быть динамической библиотекой с заголовочным файлом в комплекте, описывающим интерфейсы, реализованные компонентом (предчувствуя, что полетят камни, сообщу, что знаю про dll-hell, но в правильно оформленной и установленной системе его не будет). Можно, в зависимости от ситуации, добавить сюда .def и .lib файлы, но в правильно оформленном компоненте достаточно .dll (.so) и .h. Желательно еще, чтобы наша динамическая библиотека была статически слинкована с runtime-библиотеками. Это избавит нас от проблемы различных Redistributable Packаges под Windows.
А вот статические библиотеки вообще нельзя считать компонентами. В статические библиотеки лучше складывать различные общие части реализации для разных компонентов, сильно завязанные на сторонние контейнеры, типа STL или boost.
В итоге, готовый компонент должен как бы фиксировать реализованный и протестированный функционал, предоставляя удобный интерфейс для его использования.
Интерфейсы
Рассмотрим примеры и решения.
Статические библиотеки и интерфейсы рассматривать не будем, сразу перейдем к динамическим.
Вариант плохого интерфейса:
#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; };
Что тут не так? Ну, во-первых, реализация не скрыта. Мы видим член класса, мы видим сторонний контейнер, в данном случае std::string. Т.е. видим часть реализации, а это плохо. Кто-то возмутится и скажет, какой же он сторонний, если это стандартный контейнер? А сторонний потому, что в компоненте может быть использована реализация Microsoft STL, а мы хотим STLPort. И никогда тогда такой компонент использовать напрямую не сможем. Во-вторых: интерфейс не кроссплатформенный. Инструкции __declspec есть далеко не во всех компиляторах. В третьих: использование явной линковки для компонента с таким интерфейсом, мягко говоря, затруднительно.
Для решения первой проблемы подойдет PIMPL идиома и отказ от внешних контейнеров с заменой их на встроенные типы. Для решения второй расширим директивы define.
#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; };
Реализацию скрыли, кроссплатформенности добавили. От стандартных контейнеров на самом деле можно не отказываться, если использование реализации STL в разработке жестко регламентировано. Однако при использовании смешанных систем возможны проблемы.
Как быть с простотой явной линковки?
Для этого нужно использовать абстрактные классы и фабричную функцию.
#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; };
В данном случае мы имеем одну единственную функцию, имя которой известно и не меняется. Загрузить и слинковть такой компонент очень просто. Достаточно после загрузки библиотеки получить по имени и вызвать функцию GetComponent. Далее нам становится доступно все множество методов интерфейса Component.
Можно пойти дальше. Если функция будет возвращать интерфейс фабричного класса, то у нас появляется возможность расширять интерфейс неограниченно, при этом процедура загрузки библиотеки-компонента не изменится.
#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; };
Ну, и в качестве высшего пилотажа можно зарядить что-нибудь из COM-идеологии, что позволит неограниченно расширять функциональность компонента, при этом сохраняя полную обратную совместимость с уже работающими с ним системами.
#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; };
В данном случае мы можем добавлять интерфейсы в компонент, сохраняя обратную совместимость. Мы можем расширять сами интерфейсы наследованием, или использовать их как фабрики. Мы можем реализовывать в интерфейсах ConnectionPoint-ы и неограниченно расширять возможности по использованию обработчиков событий. Управление памятью в примере сильно упрощено, но можно, по аналогии с COM, использовать подсчет ссылок и смарт-указатели.
COM-идеология часто сложна для понимания, особенно новичкам, но разработка интерфейсов с ее использованием позволяет гибко изменять интерфейсы и реализовывать различные требования, не нарушая целостности уже рабочих проектов. COM-подход полностью кроссплатформенный, и он не должен смущать разработчиков.
Конечно использование чистого COM-подхода почти всегда излишне, лучше комбинировать его с простой работой с абстрактными классами, как в предыдущем примере.
Часто и вовсе достаточно абстрактной фабрики и фабричной функции, когда понятно, что расширения возможностей компонента в будущем не потребуется.
Когда есть четкая регламентация компиляторов, сторонних библиотек, а также понимание, что обратная совместимость с работающими системами не требуется, то для простоты интерфейса отлично подходит и PIMPL идиома.
В заключение
Правильно спроектированный интерфейс и сокрытие реализации сильно помогает в работе при многократном использовании одних и тех же компонентов. В этом случае компонент собирается и тестируется один раз, что значительно экономит ресурсы на тестирование и разработку. Он доступен в виде динамически загружаемой библиотеки и всегда готов к использованию, не обязывая разработчика разбираться в тонкостях его реализации и компиляции. Взял библиотеку с заголовочным файлом и используй, наслаждайся жизнью.
P.S.
Правильно оформлять компоненты и интерфейсы необходимо и в Java. Но об этом в следующий раз.
ссылка на оригинал статьи http://habrahabr.ru/post/160735/
Добавить комментарий