47
DI в C++ тонкости и нюансы а так же IoC контейнеры на примере Hypodermic Щербаков Антон компания СИГНАТЕК

DI в C++ тонкости и нюансы

Embed Size (px)

Citation preview

DI в C++ тонкости и нюансы

а так же IoC контейнеры на примере Hypodermic

Щербаков Антонкомпания СИГНАТЕК

Обо мне:

- решения в области СОРМ и телекоммуникаций

- 24x7 сервисы с минимум GUI- непрерывное усиление и развитие

команды

О презентации:

- личный опыт использования DI и IoC- примеры тестов на GMOCK и GTEST- … и c++ :)

Специфика работы

- Много долгоживущих проектов- Которые нужно дорабатывать- Дорабатывать быстро и качественно

С чего все начиналось...

Выводы:

Код должен быть тестируемКод должен быть тестируем простоТестовое окружение должно быть максимально простым

Что же такое DI?

...Кто слышал?

Dependency Injection

это design pattern● Dependency это объект, который

используется (Service)● Injection - передача зависимости,

зависящему классу (Client-у). При этом Dependency становится состоянием класса.

Пример с кофеваркой/* Без внедрения зависимостей */class CoffeMaker {public:

CoffeMaker() {m_heater = new Heater();m_pump = new Pump();

}

Coffe MakeCoffe() { /* ... */ }private:

Heater* m_heater;Pump* m_pump;

};

int main() {CoffeMaker* coffeMaker = new CoffeMaker();coffeMaker->MakeCoffe();

}

/* Ручное внедрение зависимостей */class CoffeMaker {public:

CoffeMaker( Heater* heater, Pump* pump ) {m_heater = heater;m_pump = pump;

}

Coffe MakeCoffe() { /* ... */ }private:

Heater* m_heater;Pump* m_pump;

};

int main() {Heater* heater = new Heater();Pump* pump = new Pump();CoffeMaker* coffeMaker = new CoffeMaker( heater,

pump );coffeMaker->MakeCoffe();

}

class Heater : public IHeater {public:

virtual void Heat() = 0;};

class Pump : public IPump {public:

virtual void Drip() = 0;};

class CoffeMaker {public:

CoffeMaker( IHeater* heater, IPump* pump ) {m_heater = heater;m_pump = pump;

}Coffe MakeCoffe() { /* ... */ }

private:IHeater* m_heater;IPump* m_pump;

};

int main() {// Пользователь класса определяет что именно

передаватьIHeater* heater = new Heater(); IPump* pump = new Pump();CoffeMaker* coffeMaker = new CoffeMaker( heater,

pump );coffeMaker->MakeCoffe();

}

Что получаем?

Кого инстанциировать решает пользователь(MOCK, STUB)

Получаем слабою связностьКласс стал проще тестироваться

class Mock_IHeater : public IHeater { public:

MOCK_METHOD0( Heat, void() );};class Mock_IPump : public IPump { public: MOCK_METHOD0( Drip, void() );};

TEST_F( CoffeMakerTest, Should_heat_water ) {

Mock_IHeater heater;

Mock_IPump pump;

CoffeMaker coffeMaker( &heater, &pump );

EXPECT_CALL( pump, Drip() )

.Times( 1 );

EXPECT_CALL( heater, Heat() )

.Times( 1 );

coffeMaker.MakeCoffe();

}

Минусы?

Избыточный дизайн (чем как правило нужно)“Архитектурный” шум в виде интерфейсов

НО тестируемость важнее!

Зависимость через указатель

Как контролировать потерю объекта?А при многопоточности?Кто отвечает за время жизни зависимости?Чистые указатели - это уже дурной тон

Какие альтернативы?

std/boost:shared_ptrunique_ptrweak_ptr

unique_ptr

Только класс клиент владеет и отвечает за время жизни зависимоости.Но тестировать не получится поэтомуunique_ptr

shared_ptr

Класс клиент так же отвечает за время жизниИли ответственность передана полностьюПотокобезопасен

НО есть проблема с циклическими связями

weak_ptr

За время жизни класс не отвечает и готов к тому, что ее могут отнять

В качестве способа передачи зависимости остались:shared_ptrweak_ptr

Внедряем зависимости:

Конструктор как правило shared_ptr

Set-теркак правило weak_ptr

class CoffeMaker {public:

CoffeMaker( std::shared_ptr< IHeater > heater, std::shared_ptr< IPump > pump ) {m_heater = heater;m_pump = pump;

}

void SetBell( std::weak_ptr< IBell > bell ) {m_bell = bell;

}

Coffe MakeCoffe() {/* ... */

}private:

std::shared_ptr< IHeater > m_heater; // Класс не может делать кофе без насоса и нагревателяstd::shared_ptr< IPump > m_pump;std::weak_ptr< IBell > m_bell; // Класс может делать кофе и без колокольчика

};

Внедряем зависимости с weak_ptr

При DI лучше обходиться без weak_ptr

Циклические связи - плохой “запах” в архитектуре и дизайне

class CoffeMaker {public:

CoffeMaker( std::shared_ptr< IHeater > heater, std::shared_ptr< IPump > pump ) {m_heater = heater;m_pump = pump;

}/* ... */

private:std::shared_ptr< IHeater > m_heater;std::shared_ptr< IPump > m_pump;

};

TEST_F( CoffeMakerTest, Should_heat_water ) {std::shared_ptr< IHeater > heater = std::make_shared< Mock_IHeater >();std::shared_ptr< IPump > pump = std::make_shared< Mock_IPump >();CoffeMaker coffeMaker( heater, pump );

EXPECT_CALL( *pump, Drip() ).Times( 1 );

EXPECT_CALL( *heater, Heat() ).Times( 1 );

coffeMaker.MakeCoffe();}

Все хорошо… НО

Кто все это будет конструировать?А внедрять зависимости?И как это проверять?

Пример ручного связывания {

auto transportLayer = std::make_shared< TcpTransportSystem >();

auto sormController = std::make_shared< SormControllerComponent >( sormType, acceptTimeOut );auto sormMsgProcessor = std::make_shared< SormMsgProcessor >();auto sormTransport = std::make_shared< TcpTransport >( transportLayer sormIp, sormPort, sormMessageProcessor );auto sormComponent = std::make_shared< SormComponent >( sormType, sormController, sormTransport );auto puTranspoort = std::make_shared< TcpTransport >( puIp, puPort, sormMessageProcessor );auto puComponent = std::make_shared< PuModule >( puTransport );auto operationScheduler = std::make_shared< RealTimeOperationScheduler >();auto ioController = std::make_shared< IoController >( operationScheduler );

auto app = std::make_shared< DelveryService >( sormComponent, puComponent, transportLayer, ioController );

app.Start();

app.Release();}

Composition root и Register Resolve Release

{// Registerauto transportLayer = std::make_shared< TcpTransportSystem >();auto sormController = std::make_shared< SormControllerComponent >( sormType, acceptTimeOut );auto sormMsgProcessor = std::make_shared< SormMsgProcessor >();auto sormTransport = std::make_shared< TcpTransport >( transportLayer sormIp, sormPort, sormMessageProcessor );auto sormComponent = std::make_shared< SormComponent >( sormType, sormController, sormTransport );auto puTranspoort = std::make_shared< TcpTransport >( puIp, puPort, sormMessageProcessor );auto puComponent = std::make_shared< PuModule >( puTransport );auto operationScheduler = std::make_shared< RealTimeOperationScheduler >();auto ioController = std::make_shared< IoController >( operationScheduler );

// Resolveauto app = std::make_shared< DelveryService >( sormComponent, puComponent, transportLayer, ioController );app.Start();

// Releaseapp.Release();

}

IoC контейнеры

Регистрация реализации для интерфейсаРазрешают необходимые зависимостиВыполняют авто связывание

Hypodermicint main(){

// RegisterContainerBuilder builder;

builder.registerType< Heater >().as< IHeater >();builder.registerType< Pump >().as< IPump >();

builder.registerType< CoffeMaker >( CREATE( std::make_shared< CoffeMaker >(

INJECT( IHeater ), INJECT( IPump ) ) ) );

auto container = builder.build();

// Resolveauto coffeMaker = container->resolve< CoffeMaker >();coffeMaker->MakeCoffe();

}

Регистрация типа{

ContainerBuilder builder;

// Регистрация непосредственно имплементацииbuilder.registerType< Pump >();

auto container = builder.build();auto pump = container->resolve< Pump >();

ASSERT_TRUE( pump != nullptr );}

Регистрация типа{

ContainerBuilder builder;

// Регистрация по интерфейсуbuilder.registerType< Pump >().as< IPump >();

auto container = builder.build();auto pump= container->resolve< IPump >();

ASSERT_TRUE( pump != nullptr );}

Регистрация типа{

ContainerBuilder builder;

// Регистрация по интерфейсу и непосредственно имплементацииbuilder.registerType< Pump >().as< IPump >().asSelf();

auto container = builder.build();auto concreetePump = container->resolve< Pump >();auto abstractPump = container->resolve< IPump >();

ASSERT_TRUE( concreetePump != nullptr ); ASSERT_TRUE( abstractPump != nullptr );}

Регистрация типа{

ContainerBuilder builder;

// Регистрация экземпляраauto pump = std::make_shared< Pump >();builder.registerInstance( pump );

auto container = builder.build();auto samePump = container->resolve< IPump >();

ASSERT_TRUE( samePump == pump );}

Регистрация типа{

ContainerBuilder builder;

// SingleInstancebuilder.registerType< Pump >().as< IPump >().singleInstance();

auto container = builder.build();auto pump = container->resolve< IPump >();auto samePump = container->resolve< IPump >();

ASSERT_TRUE( samePump == pump );}

Регистрация типа{

ContainerBuilder builder;

// Регистрация одной имплементации по нескольким интерфейсамbuilder.registerType< PumpAndHeater >().as< IPump >().as< IHeater >();

auto container = builder.build();auto pump = container->resolve< IPump >();auto heater = container->resolve< IHeater>(); // Опустим cast-ы для наглядностиASSERT_TRUE( heater == pump );

}

Именованная регистрация типа{

ContainerBuilder builder;

auto pump1 = std::make_shared< Pump >();auto pump2 = std::make_shared< Pump >();builder.registerInstance( pump1 ).named< IPump >( "pump1" );builder.registerInstance( pump2 ).named< IPump >( "pump2" );

auto container = builder.build();

ASSERT_TRUE( container->resolveNamed< IPump >( "pump1" ) == pump1 );ASSERT_TRUE( container->resolveNamed< IPump >( "pump2" ) == pump2 );

}

Инжекция зависимостей{

ContainerBuilder builder;

builder.registerType< Pump >().as< IPump >();builder.registerType< Heater >().as< IHeater >();

builder.registerType< CoffeMaker >( CREATE( std::make_shared< CoffeMaker >(

INJECT( IHeater ), INJECT( IPump ) ) ) ).as< CoffeMaker >();

auto container = builder.build();

auto coffeMaker = container->resolve< CoffeMaker >();

ASSERT_TRUE( coffeMaker != nullptr );}

Инжекция зависимостей{

ContainerBuilder builder;

builder.registerType< Pump >().named< IPump >( "pump" );builder.registerType< Heater >().named< IHeater >( "heater" );

builder.registerType< CoffeMaker >( CREATE( std::make_shared< CoffeMaker >(

INJECT_NAMED( IHeater, "heater" ), INJECT_NAMED( IPump, "pump" ) ) ) ).as< CoffeMaker >();

auto container = builder.build();

auto coffeMaker = container->resolve< CoffeMaker >();

ASSERT_TRUE( coffeMaker != nullptr );}

Регистрация через автосвязываниеstruct ServiceA : IServiceA { typedef AutowiredConstructor< ServiceA() > AutowiredSignature;

...};

struct ServiceB : IServiceB { typedef AutowiredConstructor< ServiceB(IServiceA*) > AutowiredSignature;

ServiceB( std::shared_ptr< IServiceA > serviceA ) : serviceA_( serviceA ) { }private: std::shared_ptr< IServiceA > serviceA_;};

Resolve через автосвязывание{

ContainerBuilder builder;

builder.autowireType< ServiceA >().as< IServiceA >();builder.autowireType< ServiceB >().singleInstance();

auto container = c.build();auto serviceB = container->resolve< ServiceB >();

ASSERT_TRUE( serviceB != nullptr );}

Как решать проблемы связывания?

Кидать исключение если зависимость не заданаКидать исключение если зависимость nullptr

Статика и динамика в IoC

IoC контейнеры позволяют создать каркас приложенияIoC контейнеры не нужно передавать в классы

Итоги:

DI в C++ вполне работаетЕсть своя специфика в C++ и всегда нужно держать ее в головеПодход нужно рассматривать комплексно

Другие IoC контейнеры:

boost::di - header only!walaroo

Как можно облегчить себе жизнь?

Для GMOCK есть python скрипт для генерации кода моков

TypemockIsolator++TEST( CoffeMakerTest, Should_heat_and_drip_water ){ auto mockPump = FAKE< IPump >(); auto mockHeater = FAKE< IHeater >(); // Код теста...}

TEST_F( IsolatorPPTests, IsExpired_YearIs2018_ReturnTrue ){ Product product; // Подготавливаем время для теста SYSTEMTIME fakeTime; fakeTime.wYear = 2018; // Подделываем вызов системной функции FAKE_GLOBAL( GetSystemTime ); WHEN_CALLED( GetSystemTime( RET( &fakeTime ) ) ).Ignore(); ASSERT_TRUE(product.IsExpired());}

TypemockIsolator++

Для legacy кода. Только Windows :(

А теперь вопросы

?