Войти
ПрограммированиеФорумОбщее

[C++] Yet another architecture question

Advanced: Тема повышенной сложности или важная.

#0
13:57, 25 ноя. 2016

Предположим имеется следующий код:

void run()
{
    AsyncHandlerDispatcher async_dispatcher;
    TcpMessageWrapper message_wrapper;
    TcpPeerManager peer_manager(message_wrapper, async_dispatcher);

    ThreadPoolRunner thread_pool(async_dispatcher);

    thread_pool.start(4);

    thread_pool.wait_for_stop();
}

Как видно из кода - имеется функция, которая запускает некоторый TcpPeerManager с определёнными настройками.
Код работает, каждый компонент делает своё дело и посему все они простые и легко тестируются. Все довольны.

Но теперь требуется запустить в приложении несколько таких (в т.ч. разных) TcpPeerManager'ов, и при этом сохранить доступ к ним и к их диспатчерам, чтобы можно было, например, из другого потока вызывать методы экземпляра TcpPeerManager'а, или останавливать их работу через остановку диспатчера.
Собственно, как?

Первое очевидное решение, которое напрашивается - завернуть все эти объекты в новый объект:

class TcpPeerManagerContainer
{
    AsyncHandlerDispatcher async_dispatcher;
    TcpMessageWrapper message_wrapper;
    TcpPeerManager peer_manager;
    ThreadPoolRunner thread_pool;
}

Но сразу же проявляется несколько проблем:
1) А что если мы захотим, чтобы у двух TcpPeerManager'ов был один общий диспатчер?
Создавать по контейнеру на каждый случай не вариант. Можно использовать shared_ptr, но об этом позже.

2) Этот объект должен быть простым контейнером или всё таки содержать какое-то поведение?
Если простой контейнер, то мы теряем инкапсуляцию, а с этим есть шанс размазать логику там где не нужно.
Если же создавать поведение, то придётся пробрасывать все нужные методы к компонентам, чего конечно же делать не хочется, т.к. методов может быть много:

void TcpPeerManagerContainer::add_server(... a lot of args ...)
{
    peer_manager.add_server(... a lot of args ...);
}

3) Как создавать такой объект? У него может быть очень большой, и при этом совсем не очевидный конструктор, т.к. он должен принимать аргументы для создания каждого внутреннего объекта.
Здесь я надеялся на инициализацию с помощью std::initializer_list, но увы не получилось, т.к. она (а) пытается копировать внутренние объекты и (б) не работает с std::make_unique/shared.

Что касается std::shared_ptr:
Да, он может решить почти все проблемы (1 и 3 точно), вплоть до того, что может даже освободить от создания объектов-контейнеров (со своими нюансами).
Но я его не рассматриваю, поскольку:
1) В данном случае его использование будет инвазивно - все объекты которые мы куда-либо передаём нужно будет создавать, передавать и хранить с помощью std::shared_ptr.
2) Объекты будут всегда аллоцироваться на куче.
3) При столь повсеместном (инвазивном) использовании std::shared_ptr резко возрастает вероятность не нарочно получить циклические ссылки.


Проблема напрямую связана с Dependency Injection и IoC контейнерами. И если в рамках других языков типа C# всё более менее хорошо (нет проблемы циклических ссылок; все объекты и так по указателю), то в рамках C++ решение проблемы совсем не очевидно (по крайней мере для меня).

Заранее спасибо откликнувшимся.


#1
14:59, 25 ноя. 2016

- такой конструктор контейнера можно попытаться делать через variadic templates / perfect forwarding
- не хочешь срать, не мучай жопу, контейнеры можно и не делать, только приготовьте словарь синонимов для именования переменных
- в с++ есть RAII, диспетчеры можно попробовать хранить в общем scope хоть на стеке и передавать через обычные указатели, что куда лучший аналог shared_ptr
(в отличие от упомянутых вами java/c#, где всё аллоцируется на куче)
(это не значит, что их нужно инициализировать сразу по входу / и ждать освобождения до выхода, это обозначает, что их там можно хранить)
- а потом ещё создать пул диспетчеров, который будет их создавать и реюзать - я знаю, вы такое любите

#2
16:08, 25 ноя. 2016

is0urce
> - такой конструктор контейнера можно попытаться делать через variadic templates
> / perfect forwarding
Вероятно это можно сделать, но не думаю что в этом будет толк, т.к. всё равно будет один большой конструктор, которому нужно скормить кучу с виду не связанных аргументов. А ещё при этом потеряем названия аргументов.

is0urce
> - не хочешь срать, не мучай жопу, контейнеры можно и не делать, только
> приготовьте словарь синонимов для именования переменных
Можно много чего делать и не делать. Вопрос в том, _как_ сделать, чтобы было удобнее.

is0urce
> - в с++ есть RAII, диспетчеры можно попробовать хранить в общем scope хоть на
> стеке и передавать через обычные указатели, что куда лучший аналог shared_ptr
> (в отличие от упомянутых вами java/c#, где всё аллоцируется на куче)
> (это не значит, что их нужно инициализировать сразу по входу / и ждать
> освобождения до выхода, это обозначает, что их там можно хранить)
В моём примере и так объекты создаются на стеке в некотором scope, и по ссылке передаются внутрь. В том то и дело, что хотелось бы найти универсальный способ, когда, например можно создать свой отдельный диспатчер, либо взять откуда-нибудь существующий.

Нужен механизм в чём-то аналогичный лямбдам, при объявлении которых мы неявно создаём структуру данных с нужными полями, некоторые из которых могут быть значениями, а некоторые - ссылками.
std::tuple также неплохо подходит, но неудобство в том, что у нас нет нормальных имён у элементов tuple. Возможно Structured Bindings из C++17 эту проблему решат.

Есть ещё Boost.DI https://boost-experimental.github.io/di/index.html
Читаю, возможно это то, что нужно.

#3
16:54, 25 ноя. 2016

>Возможно Structured Bindings из C++17 эту проблему решат

в таком случае, может, наоборот, вспомнить неименованые структуры?

struct
{
   dispatcher & d;
   wrapper w;
   value v;
} one, two, three, many[500];

struct
{
   dispatcher & d;
   wrapper & w;
   value v;
} an_special_one;

конечно, объявление загромоздят, но доступ более чем прозрачный (это одновременно и недостаток, абстракций на них не выстроишь)

#4
19:38, 26 ноя. 2016

is0urce
> в таком случае, может, наоборот, вспомнить неименованые структуры?
Неименованным структурам нельзя задать свой конструктор.

На самом деле если я хочу вынести контейнер за пределы scope в котором он создаётся, то я вижу только два варианта - std::shared_ptr, или вручную описанный класс-контейнер (как в #0 TcpPeerManagerContainer).
С std::shared_ptr всё ясно, а вот с классом-контейнером остаются вопросы из #0. Можно закрыть глаза на то, что нужен будет разный контейнер на разные случаи (например, общий или уникальный диспатчер), и на конструктор (хотя я очень хочу заюзать initializer_list вариант).
Самый важный вопрос касается логики, и я склоняюсь к варианту простого "тупого" контейнера. Если же какой-то логики недостаёт, значит должен быть ещё один соответствующий компонент. Покритикуйте, что ли.

Alexander K
> Есть ещё Boost.DI https://boost-experimental.github.io/di/index.html
> Читаю, возможно это то, что нужно.
Штука интересная, позволяет как-бы создавать контекст (разные объекты, типа диспатчеров в моём примере), а потом просто запросить создать некоторый объект, и либа сама выберет нужные объекты-зависимости из контекста для конструктора запрошенного объекта, при этом либа сама решает как хранить эти объекты-зависимости (может даже синглтон заюзать в некоторых случаях).

Есть как минимум пара проблем:
1) Неявное задание аргументов для конструкторов объектов.
2) Если нужно задать в контексте два экземпляра одного объекта, то им нужно дать имена. Затем каждый класс в своём конструкторе (!) должен с помощью макроса указать какой именно (по имени) объект он хочет. Макросы, да ещё и в пользовательском классе, которому вообще должно быть не важно, кто там будет его создавать - мне абсолютно не нравится. По идее можно было обойтись без макросов, но я не так силён в компайл-тайм программировании в С++, возможно напишу автору.

Проблему топика, тем не менее, либа вполне решает.

#5
2:02, 27 ноя. 2016

Alexander K
> У него может быть очень большой, и при этом совсем не очевидный конструктор,
> т.к. он должен принимать аргументы для создания каждого внутреннего объекта.
какой большой? хоста и порта недостаточно?  : )

иди от функциональности верхнего уровня
всё остальное культурно заворачивай по ходу движения вниз
внутри могут быть и shared и unique по ситуации

пример

#6
5:27, 27 ноя. 2016

Alexander K I\III
Будь проще, будь слабее.

class TcpPeerManagerContainer
{
    std::shared_ptr<AsyncHandlerDispatcher> async_dispatcher;
    std::shared_ptr<TcpMessageWrapper> message_wrapper;
    std::shared_ptr<TcpPeerManager> peer_manager;
    std::shared_ptr<ThreadPoolRunner> thread_pool;

    std::weak_ptr<AsyncHandlerDispatcher> getAsyncHandlerDispatcher() { return std::weak_ptr <AsyncHandlerDispatcher> (async_dispatcher); };
    std::weak_ptr<TcpMessageWrapper> getAsyncHandlerDispatcher() { return std::weak_ptr <TcpMessageWrapper> (message_wrapper); };
    std::weak_ptr<TcpPeerManager> getAsyncHandlerDispatcher() { return std::weak_ptr <TcpPeerManager> (peer_manager); };
    std::weak_ptr<ThreadPoolRunner> getAsyncHandlerDispatcher() { return std::weak_ptr <ThreadPoolRunner> (thread_pool); };
};
Кишки диспетчеров всё равно размажутся
Alexander K II
Его логика - держать и не пущать. Не нужно умножать сущности и болеть ООП жопного мозга.
Alexander K
> На самом деле если я хочу вынести контейнер за пределы scope в котором он
> создаётся,
то только вариант с кучей.
#7
16:10, 27 ноя. 2016

1) сделай функцию add_server мультипоточной, расставь локи.

2) использовать шаблонные лямбда функции для передачи аргументов в диспатчер.

3) загрузка параметров сервера из файла.

4) сделай add_server до запуска потока, тогда просто передавай параметры синхронно.

5) используй установку всех параметров и добавление всех серверов до старта пира и до старта потока (как бы сделал я).

#8
16:34, 27 ноя. 2016

Sh.Tac.
> какой большой? хоста и порта недостаточно? : )
Это всего лишь пример, объекты могут быть любыми.

Sh.Tac.
> иди от функциональности верхнего уровня
> всё остальное культурно заворачивай по ходу движения вниз
> внутри могут быть и shared и unique по ситуации
Я обычно всегда так и делал, но у этого подхода есть несколько недостатков:
1) Проброс вызовов во внутренние объекты, иногда в несколько этажей.
2) Объекты часто рискуют стать god-object'ами. Их тяжело тестировать, и при этом они сложные.
3) Заметно снижается гибкость (переиспользование) объектов.

Если же компоновать объекты как в примере в #0, то этих проблем нет.

iLoled
Про shared_ptr я уже писал. Да, он позволит не плодить сущности (даже от контейнера можно отказаться), но у нас не C# какой-нибудь - у нас есть RAII и объекты на стеке. Нужен подход, который сохранит это преимущество.

gamedeveloper01
Прошу сначала прочитать первый пост и разобраться в проблеме, прежде чем писать.

to all
Объекты и их названия в #0 - это всего лишь пример. На их месте могут быть любые другие.

#9
16:38, 27 ноя. 2016

Alexander K
> Прошу сначала прочитать первый пост и разобраться в проблеме, прежде чем
> писать.
ясно

#10
16:44, 27 ноя. 2016

Alexander K
> Если же компоновать объекты как в примере в #0, то этих проблем нет
есть проблема что все провода торчат наружу якобы для более гибкого использования
я всё же рекомендую прятать их в щиток с одной ручкой для конечного юзера, а для продвинутого в другой с двумя ручками

#11
16:54, 27 ноя. 2016

Alexander K
> Здесь я надеялся на инициализацию с помощью std::initializer_list, но увы не
> получилось, т.к. она (а) пытается копировать внутренние объекты и (б) не
> работает с std::make_unique/shared.

// create std::initializer_list
auto initList = { 10, 20 };
// create std::vector using std::initializer_list ctor
auto spv = std::make_shared<std::vector<int>>(initList);
#12
17:02, 27 ноя. 2016

Alexander K
да, кста, по поводу жирной инициализации
сделай класс в котором будут все нужные аргументы, заправляй его из конфига
и нет нужды передавать его обязательно в конструкторе

З.Ы. в идеале можешь даже отношения между классами конфигурить снаружи

#13
17:18, 27 ноя. 2016

Sh.Tac.
> есть проблема что все провода торчат наружу якобы для более гибкого
> использования
> я всё же рекомендую прятать их в щиток с одной ручкой для конечного юзера, а
> для продвинутого в другой с двумя ручками
Вот тут поподробнее, пожалуйста, и лучше с примером.

В моём примере, работая с TcpPeerManager, для его запуска мы должны дёргать метод из ThreadPoolRunner, что может быть как минимум не очевидно.
И если придётся, к примеру, для того же запуска не просто дёргать один метод, а например писать нечто вроде:

if (peer_manager.do_something())
{
    thread_pool.start(4);
}
То появляется неинкапсулированная логика, что уже просто недопустимо.
Можно создать метод start() у самого TcpPeerManager'а, и там инкапсулировать логику запуска. Но пока этой логики нет (а чаще всего её и не будет), то такие методы будут просто пробросами вызова вроде:
void TcpPeerManager::start()
{
    _thread_pool.start();
}
Что мне совершенно не нравится.

dave
Прикольно, не догадался до этого. Но от копирования (или перемещения) объектов всё равно не избавиться. Я хотел конструировать следующим способом:

TcpPeerManagerContainer container
{
    AsyncHandlerDispatcher(),
    TcpMessageWrapper(),
    TcpPeerManager(container.message_wrapper, container.async_dispatcher),
    ThreadPoolRunner(container.async_dispatcher)
};

Sh.Tac.
> да, кста, по поводу жирной инициализации
> сделай класс в котором будут все нужные аргументы, заправляй его из конфига
> и нет нужды передавать его обязательно в конструкторе
>
> З.Ы. в идеале можешь даже отношения между классами конфигурить снаружи
Начиная с "заправки из конфига" я ничего не понял. Можешь подробнее пояснить?

#14
17:55, 27 ноя. 2016

Alexander K
> Что мне совершенно не нравится
нормальная обёртка, весь плюсовый код, если приглядеться, состоит из обёрток, которые ничего особенного не делают : )

> Начиная с "заправки из конфига" я ничего не понял

struct TcpPeerManagerContainer
{
  void init (const struct TcpPeerManagerContainerInitializer &);

private:

  TcpPeerManager peer_manager;
  //...
};

struct TcpPeerManagerContainerInitializer
{
  struct ServerInitializer
  {
    std::string name;
    ushort port;
  };
  std::vector<ServerInitializer> serverList;
  //...
};

TcpPeerManagerContainer::init (const TcpPeerManagerContainerInitializer & arg)
{
  for (TcpPeerManagerContainerInitializer::ServerInitializer server : arg.serverList)
    peer_manager.add_server(server);
  //...
}

latter in config file (e.g. json):

"TcpPeerManagerContainer":
{
  "ServerList":
   [
     { "name": "Login", "port": "5000" },
     { "name": "Gateway1", "port": "5001" }
   ]
   //...
}
что-то типа того, в плюсах нельзя настоящий data-driven, но до некоторой степени возможно
применительно к сетевой части так лучче не делать конечно, ибо UPnP service discovery, но как пример сойдёт

ПрограммированиеФорумОбщее

Тема в архиве.