понедельник, 11 февраля 2019 г.

[prog.memories] Фича, о которой я уже успел пожалеть раза три, минимум. И это еще не предел, наверное...

Когда долго ведешь какой-то проект, то рано или поздно приходится сталкиваться с тем, что принятые когда-то решения, казавшиеся когда-то привлекательными, приносят проблемы. Сегодня попробую рассказать об одном таком решении. Рассказ будет связан с SObjectizer-ом, но главным в рассказе будет не это. Так что кому интересно, как я посыпаю себе голову пеплом, милости прошу под кат.

Итак, летом 2014-го года мы выпустили SObjectizer-5.3.0, в котором была такая фича, как ad-hoc агенты. Ее суть была вот в чем: для того, чтобы сделать обычного агента, нужно было объявить класс, наследующий от agent_t, в этом классе, наверняка, нужно было определить конструктор и какие-то методы, только после этого можно было создать объект этого класса. В случае, если агент должен выполнить какую-то одну простую операцию, вся эта возня с наследованием казалась неоправданной.

Ну вот, допустим, для тестов мне бывает нужен агент, который при старте должен кому-то отослать сообщение start. В виде обычного агента это будет выглядеть так:

class start_sender final : public so_5::agent_t {
   // Почтовый ящик, на который нужно отослать стартовое сообщение.
   const so_5::mbox_t target_;
public:
   // Обязательно нужен конструктор, чтобы определить target.
   start_sender(context_t ctx, so_5::mbox_t target)
      :  so_5::agent_t{ std::move(ctx) }, target_{ std::move(target) }
   {}

   void so_evt_start() override {
      // Вот здесь делаем то, ради чего агент нужен.
      so_5::send< start >(target_);
   }
};
...
// Создаем агента внутри какой-то кооперации.
coop.make_agent<start_sender>(target);

Поэтому возникла мысль, а что, если дать возможность создавать агентов "по месту" без предварительного описания классов для них? Просто какой-то объект создается самим SObjectizer-ом, а пользователь затем задает этому объекту необходимые реакции на те или иные события через лямбда функции. Показанный выше пример с помощью ad-hoc агентов выглядел бы так:

// Создаем агента внутри какой-то кооперации.
coop.define_agent().on_start([target] {
      // Вот здесь делаем то, ради чего агент нужен.
      so_5::send< start >(target_);
   });

В общем-то идея выглядела здравой и привлекательной. Для меня лично. Ибо тогда у меня был "бзик" по поводу сокращения объема бойлерплейт кода, который нужно писать пользователю. Ибо на фоне тогдашних актуальных конкурентов, CAF и Theron, SObjectizer казался сильно многословным и это меня лично сильно расстраивало.

Короче говоря, ad-hoc агенты были добавлены, состоялся релиз. И начались разочарования.

Первый раз о добавлении ad-hoc агентов довелось пожалеть сразу после релиза. Ибо энтузиазма у потенциальных пользователей эта фича не вызвала. Положительных отзывов не было, наоборот, были только жалобы о том, что код на лямбдах становится еще более нечитаемым.

Далее о наличии ad-hoc агентов приходилось жалеть при любых более-менее крупных нововведениях в SObjectizer-е. Появляются унифицированные send-функции? Значит нужно проверять их поведение дважды: и с обычными агентами, и с ad-hoc агентами. Вводится новый формат обработчиков? Значит нужно поддержать его дважды: и для обычных агентов, и для ad-hoc агентов. И т.д., и т.п.

В конце-концов я пришел к выводу, что затрат на поддержание ad-hoc агентов в SObjectizer-е больше, чем выгод от их наличия. Поэтому при первой серьезной переделке SObjectizer-а было решено от ad-hoc агентов избавиться.

И тут об этом решении довелось пожалеть еще раз. Поскольку, как оказалось, из порядка 400 тестов в SObjectizer-е в приблизительно 70 использовались как раз ad-hoc агенты. И все это добро нужно было переписать. На что ушло, ни много, ни мало, три дня. На протяжении двух из которых меня не покидала мысль о том, а не занимаюсь ли я бесполезной работой?...

Ко всему прочему у меня есть подозрение, что после релиза версии 5.6.0 придется пожалеть уже об удалении ad-hoc агентов из SObjectizer-а. Поскольку может внезапно (с) выясниться, что кто-то их активно использует и переход на версию 5.6 без ad-hoc агентов станет для них непривлекательным. Как бы не пришлось ad-hoc агентов возвращать... :)


Мораль сей басни такова: нам не дано предугадать как наше слово отзовется хорошо, когда в библитеке есть возможность облегчить жизнь пользователей в каких-то ситуациях путем добавления дополнительных возможностей. Лично я не сторонник принципа "есть только один верный способ сделать что-то", тем более, когда речь идет о C++. Но при этом нужно понимать, что у каждой внесенной в библиотеку фичи есть своя стоимость. И эта стоимость может значительно увеличиваться с течением времени. Вот об этом увеличении стоимости и нужно помнить.

6 комментариев:

NN​ комментирует...

Создание по месту нужная фича.
Например в C# есть паттерн с наследованием от IDisposable.
Только ленивый не добавил себе создание наследника по месту из лямбды.
Классы конечно хорошо, но когда у тебя нет состояния или нужно передать переменные через замыкание, городить класс с конструктором бывает перебором.

Yauheni Akhotnikau комментирует...

@NN
Практика показала, что в реальных проектах ad-hoc агенты не применяются. Только в каких-то демонстрационных целях и совсем простых тестах. Т.е. фича больше демонстрационная, нежели востребованная.

NN​ комментирует...

То есть как раз то что ты сделал в тестах.
А почему в тестах не оставил создание по месту ? Можно же было только там и использовать

Yauheni Akhotnikau комментирует...

@NN
В тестах тоже не все однозначно. Зачастую вместо простого ad-hoc агента получался вполне себе монстр, в котором нужно было разбираться.

Ad-hoc агенты были убраны дабы сократить разнообразие API, т.к. это увеличивает накладные расходы на сопровождение и дальнейшее развитие.

Впрочем, есть простой вариант предоставить разработчику возможность определять агентов "по месту", но не создавая для этого новые сущности и не расширяя API SObjectizer-а. Достаточно лишь иметь тип агента, у которого будет возможность задавать обработчики on_start/on_finish, а так же делать подписки. Такой агент создается обычным образом (через make_agent), а потом настраивается под то, что нужно пользователю.

NN​ комментирует...

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

P.S.
А как ты из Google+ комментируешь (я про значёк G+ рядом с именем) ?
У меня там всё сдохло и комментировать оттуда уже не могу.

Yauheni Akhotnikau комментирует...

> А как ты из Google+ комментируешь (я про значёк G+ рядом с именем)

Я из блога комментирую. Просто пока меня здесь еще по G+ аутентифицируют, отсюда и значок.