четверг, 31 марта 2011 г.

Паттерн Service Locator на C++

Паттерн Service locator представляет собой хранилище сервисных объектов. Фактически это некоторого рода ассоциативный массив с экземплярами объектов-реализаций, которые определяются по ключу – типу или имени интерфейса.

Применяется для внедрения зависимостей (Dependency Injection). В целом очень полезная фича. Иногда этот паттерн реализуется DI контейнерами заодно с основной функциональностью.

Алгоритм работы с Service Locator:
1. Регистрация экземпляров реализаций сервисных объектов в ServiceLocator путем ассоциирования с типом или именем интерфейса
2. Передача экземпляра ServiceLocator в классы, которым необходимы сервисные объекты
3. Класс, которому необходим сервисный объект, запрашивает его по ключу у контенера

Получаем развязку интерфейса и реализации.

Спроектируем и реализуем Service Locator на C++.

Терминология:
Сервис – экземпляр класса, реализующего интерфейс
Интерфейс сервиса – класс, который декларирует методы взаимодействия с сервисом, но не реализовывает их
Тег – строковой идентификатор объекта в контейнере



Актерами выступают:
- конфигуратор – функционал, создающий ServiceLocator и настраивающий сервисы
- пользователь – функционал, использующий сервисы

Конфигуратор может:
- создать ServiceLocator
- регистрировать ассоциацию типа интерфейса с экземпляром объекта сервиса с указанием тега
- регистрировать ассоциацию типа интерфейса с экземпляром объекта сервиса без указания тега

Пользователь может:
- получать сервис по запросу типа интерфейса с указанием тега
- получать сервис по запросу типа интерфейса без указания тега
- получать сервис по запросу имени типа интерфейса с указанием тега
- получать сервис по запросу имени типа интерфейса без указания тега

Реализация

Интерфейс IServiceLocator

#include <string>
#include <list>

class IServiceLocator
{
public:
virtual ~IServiceLocator() {};

virtual void RegisterService(std::string name, void* serviceObject, std::string tag = "") = 0;
virtual void* Locate(std::string name, std::string tag = "") = 0;

template<typename T>
void RegisterService(T* serviceObject, std::string tag = "")
{
RegisterService(typeid(T).name(), reinterpret_cast<void*>(serviceObject), tag);
}

template<typename T>
T* Locate(std::string tag = "")
{
return (T*)Locate(typeid(T).name(),tag);
}
};


Декларация реализации ServiceLocator


#include <list>
#include <string>

class ServiceLocator : public IServiceLocator
{
public:
virtual void RegisterService(std::string name, void* serviceObject, std::string tag = "");
virtual void* Locate(std::string name, std::string tag = "");
virtual ~ServiceLocator();

protected:
struct Service
{
void* ServiceObject;
std::string Name;
std::string Tag;
};

private:
std::list<struct Service> services_;
typedef std::list<struct Service>::iterator ServiceIterator;
};


Реализация ServiceLocator

ServiceLocator::~ServiceLocator()
{
for (ServiceIterator i = services_.begin(); i != services_.end(); i++)
{
if (i->ServiceObject != NULL)
{
delete i->ServiceObject;
i->ServiceObject = NULL;
}
}
services_.clear();
}

void ServiceLocator::RegisterService(std::string name, void* serviceObject, std::string tag)
{
if (serviceObject == NULL)
throw std::invalid_argument("serviceObject");

for (ServiceIterator i = services_.begin(); i != services_.end(); i++)
{
if ((i->Name == name) && (i->Tag == tag))
{
throw std::runtime_error(std::string("Duplicate service registration for '") + name +
std::string("' and tag '") + tag + std::string("'"));
}
}

struct Service service;
service.Name = name;
service.ServiceObject = serviceObject;
service.Tag = tag;

services_.push_back(service);
}

void* ServiceLocator::Locate(std::string name, std::string tag)
{
for (ServiceIterator i = services_.begin(); i != services_.end(); i++)
{
if ((i->Name == name) && (i->Tag == tag))
{
return reinterpret_cast<void*>(i->ServiceObject);
}
}
throw std::runtime_error(std::string("Service not found for '") + name +
std::string("' and tag '") + tag + std::string("'"));
}


Пример использования:

IServiceLocator *serviceLocator = new ServiceLocator();

serviceLocator->RegisterService<IService>(new ServiceRelease());
serviceLocator->RegisterService<IService>(new OtherServiceRelease(),"svc");

IService* service = serviceLocator->Locate<IService>();
IService* otherService = serviceLocator->Locate<IService>("svc");


Читать дальше...

среда, 30 марта 2011 г.

Валидация параметров конструктора класса в секции инициализации

Принимая параметры-указатели в конструктор, иногда полезно их проверять на валидность:


MyClass::MyClass (A* a, B* b)
{
if (!a) throw std::invalid_argument("a");
if (!b) throw std::invalid_argument("b");
a_ = a;
b_ = b;
}

Красивее (да и правильнее) было бы выполнять присвоение в секции инициализации:

MyClass::MyClass (A* a, B* b)
:a_(a), b_(b)
{
if (!a) throw std::invalid_argument("a");
if (!b) throw std::invalid_argument("b");
}

Недостатки налицо: некрасиво и не очень функционально. Присвоение выполняется еще до проверок. Кроме того, бывает еще такая ситуация:


class MyClass
{
A* a_;
B& b_;

MyClass(A* a)
:b_(a->getRefB())
{
}
}

Проблема в том, что если в качестве параметра будет передан невалидный указатель, то будет сгенерировано системное исключение еще до вызова конструктора. А переносить инициализацию внутрь конструктора нельзя: ссылка может быть проинициализирована только в секции инициализации класса.
Возможный выход в том, чтобы не попадать в подобную ситуацию. То есть – не инициализировать ссылку через указатель на объект. Но если мы хотим использовать Dependency Injection вместе со средствами C++ на полную катушку, то такая ситуация рано или поздно возникнет.
Идея состоит в том, чтобы проверить валидность указателя именно в секции инициализации. Помогут функции с шаблонными параметрами.

Итак, возможные ошибки с параметрами:
- передан нулевой указатель
- передан указатель на объект несовместимого типа
- передан «мусор»: значение, не являющееся указателем вовсе
Для проверки на соответствие указателя ожидаемому типу будем использовать RTTI.
Напишем функции с шаблонным параметром, которые смогут проверить указатель на ошибки:


template<typename T>
T NotNullChecker (T value, std::string variableName)
{
if (value == NULL)
throw std::invalid_argument(variableName + " cannot be NULL");

return value;
}

template<typename T>
T ConvertableTypeChecker(T value, std::string variableName)
{
if (dynamic_cast<T>(value) == NULL)
throw std::invalid_argument(variable + " must be convertible to type " + std::string(typeid(T).name()));

return value;
}

Для удобства допишем пару макросов для автоматической подстановки имен параметров:


#define NotNull(p) NotNullChecker(p,#p)
#define ConvertableType(p) ConvertableTypeChecker(p,#p)
#define ValidArgument(p) ConvertableTypeChecker(NotNullChecker(p,#p),#p)

Использование выглядит очень лаконично на мой взгляд:

class MyClass
{
A* a_;
B& b_;

MyClass(A* a)
:b_(ValidArgument(a) ->getRefB())
{
}
}

Вот так можно проверить аргумент на валидность еще в секции инициализации. Если развить идею дальше – можно написать таким же образом функцию проверки целочисленного аргумента на попадание в допустимый диапазон и т.д.

Читать дальше...