Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Внедрение зависимостей в C

Anna | 24.06.2014 | нет комментариев
MagicClass::getInstance().getFooFactory().createFoo().killMePlease();

«Внедрение зависимостей» и «Статическую типизицаю» сложно назвать лучшими друзьями на все времена. Некоторые наработки в этом направлении существуют и легко гуглятся, впрочем увлекательно, насколько реально сделать собственную примитивную реализацию баз хаков, искусных ухищрений и подключения внешних библиотек. Без специальной эластичности, вот Дабы дословно два действия — настройка и внедрение. Вопросы многопоточности затрагиваться не будут, Дабы не отвлекаться от стержневой идеи. Выходит, что же лично я хочу от внедрения зависимостей.

Постановка

Управление временем жизни объектов. Ньюансы, касающиеся времени жизни, не обязаны разбавлять основную логику приложения. Почаще каждого мне не необходимо знать, что требуемый объект является экземпляром класса-одиночки либо создается с поддержкой какого-то фабричного способа. Легко необходим пригодный для применения экземпляр. Вовсе отлично, если существует вероятность изменять правила управления временем жизни объекта, не затрагивая основную логику приложения.

Тестирование. Одна из первоочередных целей внедрения зависимостей. Подразумевает вероятность в подменять определенные объекты тестовыми заглушками, вновь же, не затрагивая основную реализацию.

Упрощенный доступ к объектам. Информация, какой объект откуда дозволено получить Почаще каждого абсолютно неинтересна. Больше того, она отвлекает от базовой функциональности и содействует больше крепкому связыванию подсистем плана, чем того хочется. Также нежелание программиста продумывать адекватные точки доступа к добавляемым сервисам может отрицательно сказаться на всеобщей архитектуре системы. «В последние дни я получал примерно все надобные мне объекты из модуля номер N, закину и эти туда же…».

Рабочий вариант с тестовым примером дозволено взять отсель.

Перейдем к делу. Начнем со времени жизни. Хотелось бы иметь вероятность применять следующие варианты.

  1. Одиночка (Singleton). Существует только один статический экземпляр объекта на протяжении каждого времени жизни приложения.
  2. Объект всеобщего пользования (Shared). Схож на одиночку. Основное различие — объект существует, пока им кто-то пользуется. Все заказчики применяют ссылку на один и тот же экземпляр. Может быть сделан и разломан за время работы несколько раз, даже ни одного раза, если желающих не нашлось.
  3. Объект (Object). Время жизни объекта совпадает со временем существования заказчика.
  4. Объект времени исполнения (Runtime). Все заказчики применяют один и тот же объект, тот, что может изменяться во время работы программы.

Безусловно, заказчика не должно заботить, к какому именно виду принадлежит внедренный объект.
Второе пожелание — естественность внедрения. Хочется, Дабы объект внедрялся в клиентский посредством синтаксиса, максимально приближенного к объявлению поля класса. Собственно, именно разновидностью свойства класса он и является.

class SomeClass
{
public:

private:
	inject(SomeInterface, mFieldName)
};

Должен сразу сознаться, что первоначально в мечтах, безусловно, были образцы, впрочем получить примитивную и прозрачную реализацию описанных выше типов внедрений как-то сходу не удалось. А вот с приземленными макросами получилось невзначай легко.

Должно существовать место в плане, где зависимости настраиваются дальнейшим образом.

inject_as_share(Interface1, Class1)
inject_as_singleton(Interface2, Class1)
inject_as_object(Interface3, Class1)
inject_as_runtime(Interface4)

1-й параметр — интерфейс, 2-й — реализующий его класс. Безусловно, класс может выступать в роли интерфейса для самого себя. Для объекта времени исполнения (runtime) потребуется еще инициализация где-то в глубинах программы (не самый успешный вариант внедрения, но изредка и такой необходим)

inject_set_runtime(Interface4, &implementation4)

Первое, что кидается в глаза — inject_as_shareinject_as_singleton и inject_as_object обязаны иметь вероятность создавать экземпляры классов, что подразумевает вызов их конструкторов. Простота-простотой, но рассчитывать экстраординарно на конструкторы по умолчанию было бы слишком легковерно. Следственно будет реализована вероятность передавать в макросы инициализации параметры конструкторов соответствующих классов, что-то типа

inject_as_share(Interface1, Class1, "param1", 1234, true)

Перед тем, как углубиться в реализацию предложенной доктрины, хотелось бы коротко остановиться на других методах задания зависимости объекта o1 от o2

  1. o1 непринужденно создает экземпляр o2. Самый «лобовой» и негибкий метод, для подмены o2 придется перелопатить исходники и заменить создание одного на иной. Раз o1 сотворил o2, то и ответственность за уничтожение берет на себя традиционно он же.
  2. Передача o2 в качестве параметра в конструктор. Достаточно общеизвестный метод внедрения зависимости, тот, что, впрочем может оказаться неудобным, если зависимостей и/или конструкторов у клиентского класса много.
  3. Фабричный метод/класс. Подход разрешает результативно инкапсулировать тонкости создания объекта o2, впрочем сами фабрики (исключительно когда их много) добавляют информационный шум в архитектуру приложения. Еще один значительный недочет фабрик — их применение не отражено в интерфейсе класса-заказчика.
Конфигуратор

Перейдем собственно к реализации. Начнем с внедрения одиночки. Для пользователя каждая настройка будет сводится к строке

inject_as_singleton(Interface, Class, [constructor_parameters_list])

От того что число доводов конструктора в всеобщем случае неведомо, inject_as_singleton — это макрос с переменным числом доводов. Применяться все виды инъекций будут единообразно (в качестве свойства класса), следственно первоначально необходимо придумать интерфейс, тот, что дозволил бы обеспечить доступ к объектам вне зависимости от метода их создания. В нашем случае это будет конструкция Factory с исключительным способом get, тот, что возвращает ссылку на объект, реализующий данный интерфейс.

struct Factory 
{ 
    Interface& get();
};

Реализация же класса одиночки будет классическая

Interface& getInstance()
{ 
    static T instance = T(...); 
    return instance; 
}

Вызов конструктора пришлось записать в несколько непривычном (по крайней мере, для меня) виде, потому что вариант

static T instance(...);

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

И, Дабы избежать раздоров, разместим всю эту реализацию в пространство имен injectorXXX, где XXX — имя интерфейса внедрения. Итоговый макрос будет выглядеть так

#define inject_as_singleton(Interface, T, ...) 
namespace injector##Interface{ 
	Interface& getInstance() 
	{ 
		static T instance = T(__VA_ARGS__); 
		return instance; 
	} 
	struct Factory 
	{ 
		Interface& get() { return getInstance(); } 
	}; 
}

Дальше настоль пошагово расписывать не буду, но подход неизменно один — реализация стратегии управления временем жизни «как получится» фиксированная конструкция-интерфейс Factory, и все это в отдельном пространстве имен.

Перейдем к объекту всеобщего пользования. Он существует, пока кому-то необходим, но, в различие от одиночки, может разрушаться, создаваться вторично и вообще не создаваться ни разу. Реализуем эту тактику с поддержкой подсчета ссылок — сколько обладателей фабрики, столько и пользователей внедряемого класса.

#define inject_as_share(Interface, T, ...) 
namespace injector##Interface{ 
	struct Factory 
	{ 
		Factory() { 
			if (refCount == 0) { 
				object = new T(__VA_ARGS__); 
			} 
			  refCount; 
		} 
		~Factory() { 
			if (--refCount == 0) { 
				delete object; 
			} 
		} 
		Interface& get() { return *object; } 
		static T* object; 
		static unsigned int refCount; 
	}; 
	T* Factory::object = 0; 
	unsigned int Factory::refCount = 0; 
}

Код в специальных пояснениях не нуждается — в конструкторе увеличиваем счетчик, при необходимости создавая экземпляр внедряемого класса, в деструкторе счетчик сокращаем и, если пробил час, удаляем непотребный объект.

Внедрение «свой объект для всякого заказчика» реализуется вовсе легко — как поле фабрики.

#define inject_as_object(Interface, T, ...) 
namespace injector##Interface{ 
	struct Factory 
		{ 
		Factory() : object(__VA_ARGS__) {} 
		Interface& get() { return object; } 
		T object; 
	}; 
}

Объектом, тот, что дозволено менять во время выполнения программы, фабрика не обладает, она легко предоставляет конечный данный экземпляр и разрешает менять его.

#define inject_as_runtime(Interface) 
namespace injector##Interface{ 
	struct Factory 
	{ 
		Interface& get() { return *object; } 
		static Interface* object; 
	}; 
	Interface* Factory::object = 0; 
}

Для окончательной реализации стратегии потребуется особый макрос для задания объекта

#define inject_set_runtime(Interface, Value) injector##Interface::Factory::object = (Value);
Внедрение

С фабриками завершили, сейчас перейдем к самому внедрению. Тактика будет дальнейшая — объект обладает экземпляром волнующей его фабрики, обращаясь при необходимости к предоставляемому ею объекту. Термин «фабрика» тут, безусловно, неидеален, так как она не неизменно создает экземпляры, но на момент написания статьи ничего благополучнее не придумалось.

Тут мы сталкиваемся с задачей. Принуждать пользователя всякий раз вручную вызывать способ get не хочется — все-таки сам факт существования фабрики хотелось бы спрятать, Дабы развязать себе руки для дальнейших доработок механизма. Необходимо каким-то образом сделать имитатор обыкновенного свойства. Первое, что приходит в голову — реализация интерфейса посредством наследования. Как приходит, так и уходит. Если это реализуемо, то будет довольно нетривиально. Следственно поступим проще — сделаем класс-обертку, в котором переопределим оператор ->, принудив его возвращать экземпляр внедряемого класса. Получится примерно безупречно. Из ограничений пользователь столкнется только с обязанностью пользоваться внедренным объектом как указателем, что теснее абсолютно терпимо.

Выходит, макрос внедрения свойства будет выглядеть так

#define inject(Interface, Name) 
	struct Interface##Proxy 
	{ 
		Interface* operator->() 
		{ 
			return &factory.get(); 
		} 
		injector##Interface::Factory factory; 
	}; 
	Interface##Proxy Name;

Легко класс-обертка, обладающий требуемой фабрикой и обращающийся к ней в операторе ->

Пример

Как дозволено видеть, реализация довольно простая. Применять же предлагаемый механизм еще проще. Разглядим примитивный пример. Пускай есть интерфейс

class IWork
{
public:
	virtual void doSmth() = 0;
	virtual ~IWork() {};
};

И несколько классов Work1, Work2 и т.п, его реализующие, некоторые из этих классов имеют конструкторы с параметрами (полный текст примера находится тут). Вначале конфигурируем

inject_as_share(IWork, Work1)
inject_as_singleton(Work2, Work2, 1)
inject_as_object(Work3, Work3, 1, true)
inject_as_runtime(Work4)

Потом создаем класс для навыков

class Employee
{
public:

	void doIt()
	{
		mWork1->doSmth();
		mWork2->doSmth();
		mWork3->doSmth();
		mWork4->doSmth();
	}

private:
	inject(IWork, mWork1)
	inject(Work2, mWork2)
	inject(Work3, mWork3)
	inject(Work4, mWork4)
};

И используем

Work4 w4;
inject_set_runtime(Work4, &w4)
Employee e1;
e1.doIt();

Самое основное тут — клиентский класс Employee вообще не делает никаких предположений касательно времени жизни внедренных объектов — об этом заботятся директивы конфигурирования inject_as_XXX.

 

Источник: programmingmaster.ru

 

Оставить комментарий
БАЗА ЗНАНИЙ
СЛУЧАЙНАЯ СТАТЬЯ
СЛУЧАЙНЫЙ БЛОГ
СЛУЧАЙНЫЙ МОД
СЛУЧАЙНЫЙ СКИН
НОВЫЕ МОДЫ
НОВЫЕ СКИНЫ
НАКОПЛЕННЫЙ ОПЫТ
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB