LINUX.ORG.RU

Интерфейс с различными аргументами в наследниках?

 , ,


0

2

Добрый день.

Пытаюсь тут упороться. Если кратко -> есть множество FSM, которые общаются между собой эвентами. Так же есть большой проект с кучей легаси.

В проекте эвенты делятся на запросы/ответы (упрощёно). Сейчас генерация ответа на запрос происходит рандомно + совсем нет никакого контроля о том был ли послан ответ на запрос или нет + нет защиты от повторного ответа.

Что я хочу сделать:

  1. добавить проверки ответа (просто в деструкторе)
  2. добавить проверку на то что ответ посылается только один раз
  3. ОБЯЗАТЬ запросы иметь 2 метода: ResponseSuccess(), ResponseError().

Если пункты 1 и 2 реализовать просто то возникает вопрос как адекватно сделать 3й пункт? Есть возможность допилить проект, чтобы реализовать хотя бы эти 3 пункта.

Подскажите, пожалуйста, в какую сторону смотреть? Ничего адекватного в голову не приходит.

Использовать можно 17й стандарт. Желательно чтобы это всё дело работало быстро.

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

#include <iostream>

enum EventId
{
	REQUEST_A
	, ERROR_A
	, SUCCESS_A

	, REQUEST_B
	, ERROR_B
	, SUCCESS_B
};


class EventReceiver;

class RequestA;
class ErrorA;
class SuccessA;
class RequestB;
class ErrorB;
class SuccessB;

class Event
{
public:
	using Id = uint64_t;

	explicit Event(EventReceiver* receiver = nullptr) : _receiver{receiver} {}

	virtual void ResponseError() const = 0;
	virtual void ResponseSuccess() const = 0;

	[[nodiscard]] virtual Id GetId() const = 0;

protected:
	EventReceiver* _receiver = nullptr;
};

class RequestA : public Event
{
public:
	explicit RequestA(std::string message) : _message{message} {};
	[[nodiscard]] Id GetId() const override { return REQUEST_A; }

	void ResponseError(std::string message) const {_receiver->Send(new ErrorA{message});};
	void ResponseSuccess(int value, std::string message) const {_receiver->Send(new SuccessA{value, message});};

	std::string _message;
};

class ErrorA : public Event
{
public:
	ErrorA(std::string message) : _message{message} { }
	[[nodiscard]] Id GetId() const override { return ERROR_A; }
	std::string _message;
};

class SuccessA : public Event
{
public:
	SuccessA(int value, std::string message) : _value{value}, _message{message} { }
	[[nodiscard]] Id GetId() const override { return SUCCESS_A; }

	int _value = 0;
	std::string _message;
};

// ещё N таких же запросов/ответов с разными конструкторами //


class EventReceiver
{
public:
	void Send(Event* rawEvent);
	void Receive(Event* rawEvent)
	{
		switch (rawEvent->GetId())
		{
			case REQUEST_A:
			{
				auto* event = dynamic_cast<RequestA*>(rawEvent);

				if (rand()%2)
				{
					event->ResponseError("Unlucky");
				}
				else
				{
					event->ResponseSuccess(42, "Lucky");
				}
				break;
			}
			case REQUEST_B:
			{
				auto* event = dynamic_cast<RequestB*>(rawEvent);

				if (rand()%3)
				{
					event->ResponseError("Unlucky", "Try again later", false, 42);
				}
				else
				{
					event->ResponseSuccess();
				}
				break;
			}
			case ERROR_A:
			case SUCCESS_A:
			case ERROR_B:
			case SUCCESS_B:
			{
				std::cout << "Received: " << rawEvent->GetId() << std::endl;
				break;
			}
		}
	}
};

ОБЯЗАТЬ запросы иметь 2 метода: ResponseSuccess(), ResponseError().

А какой в этом сакральный смысл?

PS. Система приема и обработки событий в вашем примере выглядит жутко. Если это то, что у вас уже есть и этим нужно пользоваться, то остается только посочувствовать.

eao197 ★★★★★ ()
Последнее исправление: eao197 (всего исправлений: 1)
Ответ на: комментарий от Crocodoom

Пример конечно не самый удачный, не спорю, да и описание.

Речь идёт про событие/event которое получает конечный автомат/FSM.

Не очень понял фразу о том что:

У конечных автоматов нет никаких эвентов

Понятное дело что нет, они просто их получают и в зависимости от события как-то реагируют. Если вам будет проще - используется акторная модель с применением FSM.

Извиняюсь если плаваю в терминах, думаю что сама суть понятна. На самом деле для вопроса это не важно, просто пытался описать свою мотивацию чтобы просто не сказали что так делать не нужно.

heh ()
Ответ на: комментарий от eao197

Сакральный смысл в том чтобы:

  • унифицировать посылку ответов на запросы
  • избавиться от копипасты
  • добавить хоть какую-то защиту от ошибок
  • вынести общий код в одно место
  • закрыть от пользовательского кода ненужные места

Для того чтобы достичь всех пунктов выше - мне хочется гарантировать что я даже не смогу послать запрос если не знаю как на него ответить + так же хочется обязать ВСЕХ кто создаёт новые запросы - использовать один унифицированный интерфейс, не плодить свои методы. Как это сделать кроме какой-то магии с pure virtual функциями - я не придумал, но какая магия для этого нужна не знаю.

Вот и пришел за советом.

Система приема и обработки событий в вашем примере выглядит жутко

Тут согласен, но к сожалению ничего не могу поделать. В реальности конечно всё немного лучше - есть макросы, которые чуть-чуть улучшают читаемость, но в целом ситуация не очень :)

Чисто для саморазвития - может быть у вас есть ссылки на проект/статью где по вашему мнению описывается хорошая реализация работы с событиями?

heh ()

реально упорин какой-то. если ты хочешь иметь методы только в запросах, то надо сделать класс Request и наследовать запросы от него, а не добавлять их в Event. вероятно понадобится множественное наследование, но может и нет, потом ты возможно ещё сам запутаешься в иерархии.

но вообще это делается примерно так:

template <typename T>
concept response_interface = requires (T t) {
  { t.ResponseSuccess() };
  { t.ResponseError() };
}

потом делаешь

void f(response_interface const auto &request) {}

в с++17 это будет делаться скорее всего плохо. посмотри как в с++14/17 эмулируются концепции, наверное сфинае надо обмазываться. лучше попробуй сначала поправить свою иерархию классов и выразить то, что ты хочешь в виде интерфейса для начала, может станет понятнее как дальше делать.

anonymous ()
Ответ на: комментарий от heh

унифицировать посылку ответов на запросы

Так вот и непонятно, зачем именно это. Ведь у вас же каждое сообщение обрабатывается индивидуально. Т.е. если прилетела команда Start, то она обрабатывается именно как Start. А если Stop, то она обрабатывается именно как Stop. Соответственно, в обработчике Start вы будете дергать методы Start-а. И другие методы вам компилятор вызвать не даст.

Может у вас там какое-то обобщение при обработке используется?

может быть у вас есть ссылки на проект/статью где по вашему мнению описывается хорошая реализация работы с событиями?

Есть такой проект. Вот обзорная статья о том, что и как.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Так вот и непонятно, зачем именно это. Ведь у вас же каждое сообщение обрабатывается индивидуально. Т.е. если прилетела команда Start, то она обрабатывается именно как Start. А если Stop, то она обрабатывается именно как Stop. Соответственно, в обработчике Start вы будете дергать методы Start-а. И другие методы вам компилятор вызвать не даст.

Тут согласен, да. По сути можно сказать что всё сейчас упирается в то что мне хочется иметь:

  • одинаковое имя для методов посылки
  • сделать их обязательными

Обобщения не используются (на самом деле используются, но не в контексте этой задачи :)).

Ещё основная мотивация - хочется спрятать указатель на EventReceiver, чтобы дать гарантию что ответ улетит именно туда куда нужно + это позволить добавить проверки на то что ответ вообще был создан и послан, т.к. другого способа послать ответ на запрос не будет, а для того чтобы его спрятать - необходимо его сделать protected.

Сейчас возможна такая ситуация:

event->_eventReceiver->Send(new ErrorA{"hello"});

От этого я хочу закрыться интерфейсом и заменить по смыслу ту же самую конструкцию на:

event->SendError("Hello");

Т.е. в этом вызове не кроется логика обработки самого события, по сути это просто отправка ответа на запрос.

За статью спасибо! Обязательно ознакомлюсь.

heh ()
Ответ на: комментарий от fsb4000

это можно и без варианта наверное сделать, как-то так:

template <typename T, typename ...Args>
void ResponseSuccess(Args... args);

и специализировать его для RequestA или для REQUEST_A. но зачем так упарыварываться на ровном месте не совсем понятно.

anonymous ()
Ответ на: комментарий от heh

Чисто теоретически я конечно могу просто этот указатель спрятать в protected и пока кто-то не реализует метод для посылки ответов в реквесте в целом не будет возможности это сделать.

Но тут мне не нравится что именование методов реквестов ложится на ревью и codestyle. Что если вдруг я подумаю и решу что теперь это не SendError, а SendReject, допустим это будет более точно выражать смысл метода. Придётся в IDE сидеть неизвестное кол-во времени и всё это вычищать. А так, в случае с интерфейсом IDE всё переименует за секунды.

Может быть я конечно реально слишком сильно упарываюсь и стоит сделать в лоб, но просто интересно.

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

heh ()
Ответ на: комментарий от heh

проверить интерфейс ты наверное не сможешь потому что у тебя его нет. ты хочешь сделать интроспекцию класса и проверить, что у него есть два метода которые имеют определённые имена. обычно это называется reflection. ещё тебе надо знать какие классы проверять, а какие нет, т.е. тебе на класс ещё надо поставить какой-то атрибут, что его надо проверять на наличие этих методов, а потом через тот же reflection их проверить. это будет где-то в с++23-26, может позже, может раньше, но без атрибутов.

anonymous ()
Ответ на: комментарий от heh

Сейчас возможна такая ситуация:
event->_eventReceiver->Send(new ErrorA{«hello»});
От этого я хочу закрыться интерфейсом и заменить по смыслу ту же >самую конструкцию на:
event->SendError(«Hello»);
Т.е. в этом вызове не кроется логика обработки самого события, по >сути это просто отправка ответа на запрос.

Если event знает тип класса ошибки, то можно сделать шаблончик. Псевдокод:

class Event
{
 public:
    template<typename... ARGS>
    void sendError(ARGS&& ...args)
    {
        _eventReceiver->send(new ErrorA(std::forward<ARGS>(args)...)
    }
    
private:
    EventReceiver *_eventReceiver;
}

// отправка
event->sendError("Hello");
ox55ff ★★★★ ()
Последнее исправление: ox55ff (всего исправлений: 1)
Ответ на: комментарий от anonymous

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

https://gcc.godbolt.org/z/5qr7Ea

fsb4000 ★★★★★ ()
Ответ на: комментарий от heh

сделать их обязательными

Если речь идет о том, чтобы в обработчике входящего события обязать разработчика вызвать либо SendSuccess, либо SendError для этого события, то способа сделать так в C++ я не знаю.

event->SendError(«Hello»);

Тут вообще не должно быть проблем. Прячьте обратный адрес внутри входящего события и выставляйте наружу нужные вам SendSuccess и SendError. Наверное, даже на шаблонах это можно сделать.

Посредством SObjectizer-а я бы набросал что-то подобное:

template<typename Ack, typename NAck>
class abstract_event_t {
  so_5::mbox_t reply_to_;
public:
  abstract_event_t(so_5::mbox_t reply_to) : reply_to_{std::move(reply_to)} {}

  template<typename... Args>
  void send_success(Args &&... args) const {
    so_5::send<Ack>(reply_to_, std::forward<Args>(args)...);
  }

  template<typename... Args>
  void send_error(Args &&... args) const {
    so_5::send<NAck>(reply_to_, std::forward<Args>(args)...);
  }
};

...
class success_result_A {...};
class error_result_A {...};
class event_A : public abstract_event_t<success_result_A, error_result_A> {...};
...
void some_processor::on_event_A(const event_A & cmd) {
  if(some_condition)
    cmd.send_success(...);
  else
    cmd.send_error(...);
}

За статью спасибо!

Там, правда, более старая версия описана. Сейчас актуальная выглядит немного по-другому местами. Но принципы те же остались.

Ну и делали мы SObjectizer как раз для подобных задач.

eao197 ★★★★★ ()
Ответ на: комментарий от fsb4000

Спасибо за пример! Это на самом деле почти то что я хочу (касаемо обязательности наличия)!

Нужно будет просто доделать чтобы для такой структуры (да и вообще в целом для любого кол-ва аргументов в ResponseSuccess и ResponseError) static_assert не падал:

struct Test
{
    void ResponseSuccess(std::string message){}
    void ResponseError(){}
};

Но для этого нужно будет разобраться со свежей головой.

Ещё раз спасибо!)

heh ()
Ответ на: комментарий от eao197

Если речь идет о том, чтобы в обработчике входящего события обязать разработчика вызвать либо SendSuccess, либо SendError для этого события, то способа сделать так в C++ я не знаю.

Почти верно. Я имею ввиду что я хочу обязать входящее событие (только request’ы) иметь как минимум по одной реализации метода SendSuccess() и SendError(). Почему как минимум по одной?

Потому что у события ответа на запрос может быть несколько конструкторов => я бы сказал что кол-во методов SendSuccess() + SendError() будет равно сумме конструкторов у классов ErrorResponse и SuccessResponse.

Касаемо обязательности вызова - это я думаю уже можно оставить на разработчике + плюс если что на функциональных/unit тестах упадём если флажок, говорящий о посылке ответа на запрос будет равен false.

За пример спасибо! Изучу его и постараюсь переложить на свои «реалии» :)

heh ()
Ответ на: комментарий от heh

Нужно будет просто доделать чтобы для такой структуры (да и вообще в целом для любого кол-ва аргументов в ResponseSuccess и ResponseError) static_assert не падал:

Тогда так: https://gcc.godbolt.org/z/13jbsd

fsb4000 ★★★★★ ()
Ответ на: комментарий от heh

Потому что у события ответа на запрос может быть несколько конструкторов => я бы сказал что кол-во методов SendSuccess() + SendError() будет равно сумме конструкторов у классов ErrorResponse и SuccessResponse.

В показанном мной примере шаблонные реализации send_success/send_error автоматически будут работать с конструкторами SuccessRespose/ErrorResponse любых форматов. Там ведь, по сути, просто делегирование осуществляется.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Да, согласен. Проглядел. Всё должно быть хорошо!

Спасибо ещё раз за ваши ответы, нужно какое-то время чтобы всё-это переварить. Позже обязательно вернусь сюда и отпишусь к чему в итоге пришел, вдруг кому-то поможет эта тема потом.

heh ()
Ответ на: комментарий от heh

Вот ещё проверку на то что возвращаемый тип у метода void добавил и чтобы корректно SFINAE проходило, если метода нет(то есть чтобы мы падали именно в нашем static_assert, а не раньше): https://gcc.godbolt.org/z/ajbY7z

fsb4000 ★★★★★ ()
Последнее исправление: fsb4000 (всего исправлений: 2)
Ответ на: комментарий от fsb4000

Заставить бы ещё всё это дело работать с вариантом #1 или #2 работать https://gcc.godbolt.org/z/r7G6vG :) Думаю что вариант #1 всё-таки предпочтительней, т.к. меньше копипасты, на выходных поковыряюсь.

Правда я бы хотел ещё подумать над тем как научиться писать что-то вроде такого:

struct Test : ourInterfaceImplemented<Test>
{
	void ResponseSuccess(){}
	void ResponseError(){}
}

Если конечно же такое возможно.

Но в целом всё выглядит как будто я с вашей помощью очень близок к решению, ещё раз спасибо!

heh ()
Ответ на: комментарий от heh

Тут мы проверяем что есть

void ResponseSuccess(){}
void ResponseError(){}

но позволяем существовать другим перегрузкам(просто делаем static_cast, чтобы уточнить какую именно перегрузка нам нужна))

https://gcc.godbolt.org/z/x3M77s

Как сделать для произвольных аргументов проверку я не знаю…

Но если у нас всего такие варианты:

void ResponseSuccess();
void ResponseSuccess(std::string message, int value = 0);
void ResponseSuccess(int value);

То можно сделать проверку которая будет проверять каждую перегрузку и например возвращать true, если хотя бы один метод есть…

fsb4000 ★★★★★ ()
Ответ на: комментарий от heh
struct Test : ourInterfaceImplemented<Test>
{
	void ResponseSuccess(){}
	void ResponseError(){}
}

Вот так я тоже не знаю как сделать.

Но можно делать так:

struct Test
{
  void ResponseSuccess();
  void ResponseError();
  void ResponseSuccess(std::string message, int value = 0);
  void ResponseSuccess(int value);
  void ResponseError(std::string message, int value = 0);
  void ResponseError(int value);
};
static_assert(ourInterfaceImplemented<Test>, "We must implement our interface!!!");

То есть static_assert писать в хедер файле, рядом с классом.

https://gcc.godbolt.org/z/KeMvs7

fsb4000 ★★★★★ ()
Ответ на: комментарий от fsb4000

ну это эмуляция концепций и там действительно есть частичное отражение времени компиляции. в принципе такое можно делать если сильная усрачка, но я лично против такой хуни. ещё люди которые таким занимались, типа портировали код с с++14 до 20, большие спецы по сфинае головного мозга, говорили, что это жуткая ебнина и давайте уже заканчивать, берите двадцатку и хватить полоскать мозги пожалуйста. но кому что нравится конечно.

anonymous ()

В общем посидел я ещё какое-то время, подумал, оценил адекватность.

Пришел к тому что все решения предложенные мне выше не совсем подходят и являются слишком оверхедными. Видимо реально самое адекватное - это сделать указатель на EventReceiver* protected и заставить всех людей пилить свою реализацию для посылки ответа.

Из плюсов такого подхода - я точно гарантирую что:

  • не получится изменить EventReceiver*
  • не получится послать ответ другому EventReceiver*
  • метод ответа будет рано или поздно создан
  • приложение упадёт с ошибкой если на сообщение никто не ответит

Из минусов:

  • не могу обязать запрос иметь как минимум по одному методу для генерации посылки ответа (ошибка/успех)
  • не могу гарантировать того, что методы для посылки ответа будут обязательно называться определённым образом (без лишних танцев с бубном)
  • не могу гарантировать того, что методы для посылки ответа будут возвращать один тип (void) (без лишних танцев с бубном)

Взвесив все плюсы и минусы решил что такой подход меня устроит.

Большое спасибо всем тем кто мне помог! Узнал много прикольных фишек, обязательно их заюзаю когда это будет оправдано!

heh ()