Upload
platonov-sergey
View
1.175
Download
3
Embed Size (px)
Citation preview
DI в C++ тонкости и нюансы
а так же IoC контейнеры на примере Hypodermic
Щербаков Антонкомпания СИГНАТЕК
Обо мне:
- решения в области СОРМ и телекоммуникаций
- 24x7 сервисы с минимум GUI- непрерывное усиление и развитие
команды
Специфика работы
- Много долгоживущих проектов- Которые нужно дорабатывать- Дорабатывать быстро и качественно
Выводы:
Код должен быть тестируемКод должен быть тестируем простоТестовое окружение должно быть максимально простым
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();
}
Минусы?
Избыточный дизайн (чем как правило нужно)“Архитектурный” шум в виде интерфейсов
НО тестируемость важнее!
Зависимость через указатель
Как контролировать потерю объекта?А при многопоточности?Кто отвечает за время жизни зависимости?Чистые указатели - это уже дурной тон
unique_ptr
Только класс клиент владеет и отвечает за время жизни зависимоости.Но тестировать не получится поэтомуunique_ptr
shared_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
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++ и всегда нужно держать ее в головеПодход нужно рассматривать комплексно
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());}