вторник, 25 ноября 2014 г.

[prog.flame] Когда же АлгТД круче ООП и наоборот? ;)

Фактически послесловие к закрытой дискуссии на eax.me. Прям сейчас возник пример ситуации, в которой алгебраические типы данных (АлгТД) удобнее объектно-ориентированного подхода.

Допустим, я делаю приложение, которое в одном рабочем потоке выгребает запросы из какого-то MQ-сервиса (не важно, будет это MQTT, AMQP или DDS), после чего обрабатывает их в этом же потоке. Прочитанные из MQ сообщения могут приводить к одному или нескольким прикладным событиям в приложении, в том числе к отложенным на какое-то время или повторяющимся с каким-то интервалом событиям. Например, поступила команда запустить электродвигатель и разогнать его до определенной скорости, нужно записать специальную команду в специальный I/O-порт, затем периодически опрашивать другие I/O-порты дабы проверять скорость вращения и, при необходимости, выдавать дополнительные команды. И так до тех пор, пока из MQ не придет команда на выключение двигателя.

В общем, на верхнем уровне в рабочем потоке будет что-то вроде вот такого цикла обработки событий (с прицелом на MQTT и libmosquittopp):

mosqpp::mosquittopp mosq(...);
auto so_env = so_5::launch_single_threaded_env(...);
...
while(true)
{
   // Process any MQTT-related events.
   auto r = mosq.loop(
      // Wait no more time than next timer event.
      to_milliseconds( so_env.timeout_before_next_event( millisecons(1000) ) ) );
   if( r != MOSQ_ERR_SUCCESS )
      ... // Some error handling.
   else
      // Application events can be processed.
      so_env.process_events();
}

Вроде бы все просто, но возникает маленький вопросик: а что, если нужно ограничить длительность работы process_events?

Сходу придумываются три типа ограничений: выполнение не более N событий (не важно сколько времени это займет), выполнение событий в течении M миллисекунд (не важно сколько именно событий будет выполнено), комбинированный -- либо N событий, либо не менее M миллисекунд.

Ok, как эти ограничения передать в метод process_events? Не делая при этом перегрузки process_events для разных списков параметров.

В случае АлгТД решение лежит на поверхности (в Scala-синтаксисе):

abstract class EventsConstraints
case class NoMoreThan(limit: Intextends EventsConstraints
case class NoLongerThan(limit: Durationextends EventsConstraints
case class NoMoreNoLonger(maxEvents: Int, maxTime: Durationextends EventsConstraints

Соответственно, вызов so_env.process_events мог бы быть записан, например, как process_events(NoMoreThan(100)) или process_events(NoMoreNoLonger(100,10ms)).

Ну теперь представьте, во что бы все это вылилось, если бы пришлось обходиться чистым, да еще и убогим, как в Java, ООП :)

Кстати, почему нельзя создавать разные варианты process_events? Потому, что это в данном примере process_events такой простой и получает всего один аргумент. В реальной жизни это может быть метод с несколькими параметрами. Например: process_events(exception_handling_policy, logging_parameters, events_constraints). Соответственно, у каждого параметра могут быть свои варианты. И на каждую комбинацию значений аргументов свой вариант process_events не напишешь.

АглТД здесь хороши еще и тем, что если со временем появится еще один тип ограничений (например, выполнение событий до тех пор, пока заполненность очереди сообщений не упадет ниже некоторого порога), то внутренности process_events не скомпилируются, если там нет должной обработки нового типа ограничения или их явного игнорирования (т.к. нормально реализованный паттерн-матчинг полноту вариантов проверяет).

Но этот же пример показывает ситуацию, в которой ООП выгоднее АлгТД. Например, насколько удобно посредством алгебраических типов представлять тип объекта so_env? Что это вообще будет за тип, какие у него внутренности, как именно он работает?

На начальных стадиях проектирования об этом мало что можно сказать. Можно разве что определить его интерфейс, вроде вот такого:

class single_threaded_environment
{
public :
   virtual ~single_threaded_environment();

   virtual void
   process_events() = 0;

   virtual std::chrono::steady_clock::duration
   timeout_before_next_event(
      const std::chrono::steady_clock::duration & default_timeout ) = 0;
};

Для дальнейшей разработки в ОО-подходе этого уже достаточно. Детали работы реализаций интерфейса single_threaded_environment будут выяснены позже и там, где это нужно. А в других местах об этом ничего знать не следует. Более того, таких реализаций может быть несколько (скажем, под конкретные платформы или же отдельная реализация для release-режима, отдельная для debug). Как эти реализации будут устроены, будут ли они иметь какие-то общие части или уж тем более, будут ли они наследовать реализацию друг друга -- это уже дело десятое, по большому счету.

Кстати, если у кого-то уже появилось желание написать в коментариях про то, что наследование реализации (а не интерфейса) -- это зло и что за это нужно отрывать руки/гнать ссанными тряпками/читать блог вот того крутого чувака (нужное подчеркнуть), то крайне рекомендую сначала прочитать "Повторное использование против сжатия". Это глава из книги Ричарда Гэбриеля, одного из авторитетнейших Lisp-еров, которого нельзя заподозрить в любви к ООП. Лучше чем он вы вряд ли сможете высказаться на эту тему.

Кстати, в упомянутом обсуждении на eax.me некоторые альтернативно одаренные личности ставили в вину ООП одно из его главных достоинств -- сокрытие данных, т.е. инкапсуляцию, по сути. Что, на мой взгляд, уже вообще далеко за гранью здравого смысла. Ну глупо же инструменту, главная черта которого инкапсуляция, ставить в вину именно инкапсуляцию.

Комментариев нет: