CxxMock — принцип действия
Иногда бывает интересно изучить архитектуру какого либо изделия, и посмотреть как оно устроено. Вот бывало разберешь часы, а обратно собрать не можешь… Но в отличии от часов программные продукты при доступе к исходникам можно разобрать, и собрать. А найденные решения применять уже в своей практике.Когда у меня возникла необходимость в создании CxxMock, о котором я писал в статье CxxMock — Mock-объекты в C++, я разобрал принцип действия похожего GoogleMock. Или еще раньше разобрал основную идею c10k сервера mathopd, что последующих проектах позволило мне лучше маневрировать в проектировании архитектуры.
Поэтому, я расскажу об основных концепциях и за счет которых работает CxxMock. И которые было интересно придумывать. Возможно, некоторые трюки покажутся вам простыми, а другие смогут вам помочь в вашей практике.
CxxMock взгляд изнутриИнтересные решения о которых будет идти речь: Имитация поведения как будто у нас есть отражение
Обеспечение регистрации фабрик объектов без оверхеда.
Создание нужной реализации интерфейса.
Программирование поведения метода.
Контроль выполнения метода
Сравнение аргументов.
Выполнение пользовательского метода.
Имитация поведения как будто у нас есть отражение
В С# отражение есть, в C++ нет, но есть RTTI который может помочь идентифицировать типы, но не может ни вызывать методы, ни строить классы динамически во время исполнения программы. То есть, для того чтобы что-то создать, нужно чтобы оно уже существовало во время компиляции, и чтобы ядро CxxMock знало о том ЧТО надо создать и КАК надо создать. Для достижения этого, можно применить парсер кода также, как это делает CxxTest для построения оглавления тестов и Qt для создания QMetaOBject содержащий ссылки на все сигналы и слоты. В случае с CxxMock пришлось соответствовать концепции CxxTest и написать генератор на python со всякими страшными regex и алгоритмом разбора скобок чтобы учитывать такие случаи:
namespace NS4 { namespace NS5 {
class Interface
{
public:
virtual void method (int a)=0;
virtual Type* method2(const Type& a)=0;
virtual ~Interface (){};
};
}}
на выходе, генератор создает заголовочный файл с классом (классами) который реализует интерфейс
namespace NS4 { namespace NS5 {
class CXXMOCK_DECL (Interface)
{
public:
virtual int method (int a) {
CXXMOCK (int, a)
}
virtual Type* method2(const Type& a){
return CXXMOCK (Type*, a)
}
virtual ~Interface (){};
};
CXXMOCK_IMPL (Interface)
}}
то есть вполне читаемый код, который можно написать и вручную и который сможет разобрать почти любой парсер синтаксиса в IDE. Первая версия так и делалась, с написания таких классов вручную.Обеспечение регистрации фабрик объектов без оверхеда
Если подходить в лоб, то пришлось бы для каждого класса имеющий специальную реализацию интерфейса в какой-то точке setUp () или сразу вначале писать код типа такого:
cxxmock: Repository: instance ().registerFactory
Мы имеем ограничения:
В большей части случаев при применении CxxTest не требуется вручную переписывать функцию main (). То есть мы в нее не можем вставить наш код. Однако, также не удобно делать регистрацию в каждом методе setUp () каждого тестового набора, даже подключая оглавлание через директиву #include И выполнять какой-то код в том же месте где объявляется наш класс тоже вроде бы нельзя Но когда нельзя, но очень хочется — то можно. Для этой цели применяется вызов макроса
CXXMOCK_IMPL (Interface)
Макрос отвечает за создание статической переменной — контейнера типизированного интерфейсом и нашим созданным классом — заглушкой
#define CXXMOCK_IMPL (interface) CxxMock: Container
Создание нужной реализации интерфейса
Для того чтобы что-то создать, нужно знать ЧТО создавать и найти того кто знает КАК создавать то что нам нужно.В .NET мы можем просто написать:
_registry[ typeof (factory) ] = factory;
для C++ нужно применять магию с RTTI:
template
template
Программирование поведения метода Программирование поведения метода это сама важная часть CxxMock, ведь необходимо явно вызывать метод интерфейса так, чтобы IDE подсветила все нужные аргументы, но при этому нужно сообщить CxxMock какие-то параметры касающиеся именно этого вызова.Идеально это должно выглядеть так (Rhino.Mocks, C#):
Expect.Call (mock→method (5)).returns (10); Expect.Call (()=> { mock→voidMethod (5); }).repeat.any; На самом деле тут происходит происходит два вызова:
Сначала честно вызывается наш метод интерфейса mock→method (), затем Результат, неважно какой, передается в вызов Expect.Call () который возвращает структуру CalllInfo содержащую информацию о вызове. Также в Rhino.Mocks используется класс Expect который '''знает''' о текущем контексте и активном MockRepository.Для С++ версии я применил похожий трюк:
TS_EXPECT_CALL (mock→method (10)).returns (5); , но с использованием макроса TS_EXPECT_CALL с совместимой с CxxTest сигнатурой в который спрятал вызов:
CxxMock: Repository: instance ().expectCall (mock→method (10)) отличие от Rhino.Mocks здесь в том, что во первых не используется дополнительный класс для упрятывая обращения к экземпляру репозитория (MocksRepository), а во вторых есть возможность замаскировать способ вызова метода method ().После того как структура CallInfo возвращается по ссылке из expectCall () происходит обычная работа по настройке объекта.
Передача аргументов Интересный вопрос с записыванием аргументов с которым был метод вызван и обеспечение запоминания и возврата возвращаемого значения. Аргументы надо хранить и с ними нужно сравнивать.В CxxMock применено смешанное решение:
1. Автогенератор создает класс с использованием макроса CXXMOCK, что дает возможность простого использования в ручном режиме.
int method (int a) { return CXXMOCK (int, a); } 2. Макрос CXXMOCK, в свою очередь, вызывает перегруженный метод cxxmock_object.mock (MOCK_FUNCID, args); который имеет произвольную типизацию аналогично Action<> в C# (до 10 аргументов) и сообщает ядру CxxMock строковое представление названия метода. Так как нам важно точно знать какой именно метод был вызван и какая у него сигнатура, и в С++ возможна перегрузка в том числе и чистых виртуальных методов, то используется регистрация по вызова по полной сигнатуре метода используя макрос MOCK_FUNCID реализующий __PRETTY_FUNCTION__ или __FUNCDNAME__ в зависимости о компилятора.
template
Ожидали вызов: Interface: method (5)Фактически вызван: Interace: method2(6)
Так как разработчик может применять свои типы данных, и если он использует какой-то фреймвок для тестирования, то он не должен писать ничего дополнительно. Поэтому тут также применен CxxTest:
template
Выполнение этих двух вещей, это единственное место где реально используется интеграция с CxxTest.Выполнение пользовательского метода
Для того чтобы выполнить пользовательский метод необходимо несколько условий: Нужно сохранить информацию о методе, который надо вызвать, и его тип чтобы правильно его вызвать.
Мы должны одинаково инициировать вызов пользоватеского метода назависимо от того сколько у него аргументов.
Учитывая строгую типизацию, нужно построить вызов метода как будто у нас переменое число аргументов.
Чтобы сохранить информацию о методе, просто сделаем шаблонный класс который хранит указатель на функцию который мы получили на входе:
template< typename Sender, typename T>
CallInfo& action (Sender* sender, T method)
{
_action = new Action
Заключение Главные тезисы примененных трюков: Даже простой текста может сделать имитацию «отражения» когда очень хочется Можно выполнять любой код до передачи управления в main () используя конструктор объекта Чтобы привязать шаблонные классы к общей точке нужно наследовать интерфейс, dynamic_cast все остальное сделает как надо. Можно строить цепочки последовательностей за счет неявного использования глобального контекста (Expect.Call (…)). Десяток перегруженных методов и коллекции контейнеров аргумента может упростить создание своей версии RPC или задачу сравнения списка аргументов Не нужно делать все самому, иногда платформа уже предоставляет для этого возможности Сложный тип в шаблоне может быть разложен на более простые типы, что позволит точнее выбрать реализующий метод Вот наверное и все основные трюки примененные в этой простой библиотеке CxxMock, основной код которой занимает всего 15 кб, но позволяющей сильно упросить жизнь разработчику и IDE.
Все лежит на SourceForge и GitHub.
Спасибо за внимание.
Ссылки Основной сайт CxxMock Зеркало на SourceForge Зеркало на GitHub CxxTest Rhino.Mocks GoogleMock Что почитать Для постижения ДАО программирования, также рекомендую: Майерс Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ Джон Бентли. Жемчужины программирования