среда, 4 сентября 2013 г.

О fluent interface'ах

Придумка Фаулера (http://18delphi.blogspot.com/2013/09/blog-post_4.html) - конечно - ХОРОША.

Но не в переложении русскоязычного автора - http://18delphi.blogspot.com/2013/09/blog-post_3.html.

Русскоязычный автор "потерял" в своей статье использование new. И из-за этого - "смазал" всю идею Фаулера.

Я САМ таким - ДАВНО пользуюсь. Ещё задолго до Фаулера.

Меня тут спросили - "как я этим пользуюсь".

Да примерно также как и Фаулер.

Примерно в таком ключе:

TTreeBuilder.Make(Tree)
 .AddNode('Text1')
 .AddNode('Text2')
 .AddNode('Text3', TSpecialNodeType1)
 .AddNode('Text4', TSpecialNodeType2)
 .OpenLevel
  .AddNode('Sibling 1')
  .AddNode('Sibling 2')
  .AddNode('Sibling 3')
  .OpenLevel
   .AddNode('SubSibling 1')
  .CloseLevel
 .CloseLevel
 .AddNode('Text 5');

- я сам это придумал. Задолго до Фаулера.

И не считаю это "революционным". Я считаю это "эволюцией" цепочечных выражений вида:
 DoParams(TParamsList.Create.AddParam("Param1").AddParam(123).AddParam(TClass.Create).AddParam("ParamX"))

Более того, я считаю и это эволюцией идеи Кернигана и Ричи. В виде:
cout << "Hello, man" << "you're number" << 16 << "in the list of less than " << 100.5 << endln;

О чём  и написал Всеволод Леонов - http://18delphi.blogspot.com/2013/09/blog-post_3.html?showComment=1378273720721#c6925185551184111953

Хотя некоторые оппоненты и считают, что "пример Леонова не к месту".

Я считаю, что пример Леонова - УМЕСТЕН и грамматически и семантически правильный.

Что до того, что "сам придумал" - не спрашивайте меня proof-link'и. Их - НЕТУ. Придётся "поверить на слово". А я обычно - слово держу.

Я по молодости - статей не писал. А - зря. Сейчас - восполняю этот пробел.

Я САМ много чего "придумал" и SAX, и XML, и XPath и Publisher/Subscriber.

И IUnknown и "подсчёт ссылок".

Это конечно - выглядит "голословным", но это - так.

У меня есть коллеги, которые могут подтвердить. Если захотят. Ну а если не захотят.. То - не подтвердят. Имеют право...

Что до fluent interface'ов, то как ПРАВИЛЬНО заметили - они УДОБНО и БЕЗОПАСНО реализуются - ТОЛЬКО через ИНТЕРФЕЙСЫ (ключевое слово - interface).

Я же - привык - "считать копейки".

Я ОБЫЧНО - пишу код, а потом открываю отладчик и смотрю на его ассемблерный аналог.

В наш век "гигабайтов и гигагерц" это наверное выглядит - "причудой". Ну что же. Считайте меня - старомодным занудой.

Есть ОДИН БОЛЬШОЙ минус в fluent interface'ах. КАЖДЫЙ возврат интерфейса из функции ведёт к однократному, а то и двукратному вызову AddRef/Release.

А они - недёшевы. С учётом требований "атомарности".

Посему - я сам - стараюсь отходить от fluent interface'ов.

(Ну и не только поэтому).

Я сам сторонник "классического" подхода. "Данные и алгоритмы". Вирт.

Отдельно ДЕКЛАРАТИВНО описываем структуру, а отдельно ИМПЕРАТИВНО - алгоритм генерации экземпляров объектов по этой структуре.

И я продолжаю склоняться к этой парадигме.

А так - идея Фаулера - хороша. Только ничего "молодого" и "революционного" я в ней не вижу.

Но это - НЕ ВАЖНО.

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

  1. Использование интерфейсов в подобных выражениях ведет к утечке памяти в Delphi XE2 и ранее, в более поздних не смотрел.
    К утечке приводит тот факт, что при вызове функций цепочкой не вызывается _release.
    _AddRef вызывается в момент присвоения значения псевдопеременной Result, а _Release - в момент присвоения значения той переменной, в которую мы положим результат вызова функции. Если же этот результат мы не присваиваем переменной, то _Кудуфыу не будет вызван вовсе.

    Полный код примера.
    program fit;
    //сделана для ответа на этот пост
    //http://18delphi.blogspot.ru/2013/09/fluent-interface.html
    {$APPTYPE CONSOLE}
    {$R *.res}
    uses
    System.SysUtils;
    type
    ITFI=interface(IUnknown)
    function fi:ITFI;
    end;
    TTFI=class(TInterfacedObject,ITFI)
    function fi:ITFI;
    end;
    { TTFI }
    function TTFI.fi: ITFI;
    begin
    Writeln('Before: ',self.FRefCount);
    Result := Self;
    Writeln('After: ',self.FRefCount);
    end;
    var
    f:ITFI;
    begin
    try
    f:=ttfi.create;
    //f.FRefCount=1
    f:=f.fi;
    //f.FRefCount=2
    f:=f.fi.fi.fi.fi;
    //f.FRefCount=6
    f:=nil;
    //единственный вызов f._release Интересно, что же в счетчике ссылок?
    readln;//чтоб с экрана вывод не пропадал
    except
    on E: Exception do
    Writeln(E.ClassName, ': ', E.Message);
    end;
    end.

    ОтветитьУдалить
    Ответы
    1. NameRec:

      «Использование интерфейсов в подобных выражениях ведет к утечке памяти в Delphi XE2 и ранее, в более поздних не смотрел.»
      -- Не подтверждаю.
      Попробуйте запустить такой вариант:
      {code}
      {$apptype console}

      program fit;
      uses
      SysUtils;

      type
      ITFI = interface(IUnknown)
      function fi: ITFI;
      procedure Test(Prompt: String='');
      end;

      TTFI = class(TInterfacedObject, ITFI)
      public
      destructor Destroy; override;
      function fi: ITFI;
      procedure Test(Prompt: String='');
      end;

      { TTFI }

      destructor TTFI.Destroy;
      begin
      Test('Destroy');
      inherited;
      end;

      function TTFI.fi: ITFI;
      begin
      Result := Self;
      Test('fi');
      end;

      procedure TTFI.Test(Prompt: String='');
      begin
      if Prompt <> '' then
      Prompt := '[' + Prompt + '] ';
      WriteLn(Prompt, Self.RefCount);
      end;

      procedure Execute;
      var
      f, f1: ITFI;
      begin
      WriteLn('begin Execute');
      f := ttfi.create;
      f.Test('after ttfi.create');
      f := f.fi;
      f.Test('after f.fi');
      f := f.fi.fi.fi.fi;
      f.Test('after f := f.fi.fi.fi.fi');
      Move(f, f1, SizeOf(f));
      f := nil;
      f1.Test('after f := nil');
      WriteLn('end Execute');
      end;

      begin
      WriteLn('begin program');
      try
      WriteLn('before call Execute');
      Execute;
      WriteLn('after call Execute');
      except
      on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
      end;
      WriteLn('end program');
      end.
      {/code}
      По меньшей мере, в Delphi 2007 память освобождается корректно.
      Вызов TTFI.Destroy происходит после завершения процедуры Execute.
      Кстати, как Вы определили, что утечка памяти имеет место?

      Удалить
    2. У меня - нет проблем с _AddRef/_Release. У меня есть тесты для DUnit.

      Но я НЕ ИСПОЛЬЗУЮ TInterfacedObject. Я уже писал об этом. Может быть проблема в этом.

      Хотя - вряд ли.

      Удалить
    3. А я вам скажу в чём проблема. Для глобальных переменных уровня programm есть проблемы с compiler-magic и Release. Это как раз - проверено.

      Удалить
  2. Я вот "по-стариковски" даже НЕ ДУМАЛ, что это такая ЖИВОТРЕПЕЩУЩАЯ тема.

    Я просто "по ходу жизни" откомментировал.

    А тут ТАКАЯ жизнь!

    Но мне лично - СИЛЬНО интереснее - UML, DSL и TDD. Почему-то...

    Ну или "хардкорные" обёртки над локальными функциями...

    А "синтаксический сахар" - далеко не всегда интересен....

    ОтветитьУдалить
    Ответы
    1. NameRec:

      «Я вот "по-стариковски" даже НЕ ДУМАЛ, что это такая ЖИВОТРЕПЕЩУЩАЯ тема.»
      -- В комментариях - обсуждение возможной ошибки Delphi при работе с интерфейсами в контексте потомков TInterfacedObject.
      IMHO - тема, как минимум, заслуживающая внимания.

      Удалить
    2. Проблемы с TInterfacedObject - да.

      Удалить
  3. NameRec:

    «Но мне лично - СИЛЬНО интереснее - UML, DSL и TDD. Почему-то...
    Ну или "хардкорные" обёртки над локальными функциями...
    А "синтаксический сахар" - далеко не всегда интересен....»
    -- Александр, а Вы не находите, что fluent-техника, в сущности, DSL и есть? Она выглядит как DSL и правила работы с ней напоминают правила работы с DSL.
    Только DSL в контексте существующего языка.
    Вероятно, это не просто увидеть, поскольку обычно DSL *реализуется* средствами другого языка.

    fluent-техника - не синтаксический сахар в классическом понимании (http://ru.wikipedia.org/wiki/Синтаксический_сахар), поскольку не требует расширения синтаксиса языка программирования, в чём я вижу достоинство.

    Fluent-техника действительно очень похожа на DSL как по реализации, так и по применению.
    Поверхностно проверить моё утверждение можно относительно просто.
    Выше я говорил, что вижу ограниченную область применения этой техники в Delphi и обозначил описание SQL - как пример задачи, где fluent-техника может принести пользу.
    Учитывая, что SQL, в сущности, является DSL, можно посмотреть, получится ли использовать fluent-технику для описания объектами других известных DSL. Таких как XML, XPath, regexp, JSON и, не поверите, даже UML.
    Понятное дело, что генерация UML-диаграммы в программном коде выглядит, по меньшей мере, странно. Но технически это возможно.
    Для описания же XML, XPath, regexp и т.п. вполне можно найти достойные применения.
    Что делает это возможным? Наличие у языка описания, что ограничивает возможное число вариантов в контексте построения fluent-цепочки.

    Разумеется, никого не призываю "выкидывать свои DSL" и "прикручивать" вместо них текучие интерфейсы.
    Просто предлагаю под другим углом посмотреть на технику, которая (есть у меня такое ощущение) может оказаться полезной там, где требуется объектное описание *существующего* DSL.

    ОтветитьУдалить
  4. NameRec:

    Пример где это может оказаться востребованным.
    --
    Ряд СУБД (Oracle, Postgres, SQLite, Red Database (клон Firebird)), поддерживают полнотекстовый поиск, но при сходных возможностях, язык запросов в них заметно отличается.
    Предположим, мы хотим:
    1. Обеспечить независимое от СУБД хранение полнотекстовых запросов
    2. Предоставить пользователю возможность делать полнотекстовые запросы интерактивно и хотим его избавить от набора магических строчек вида «сверхпроводимость -"вики" -"яндекс" +"левитация" +"видео"», поскольку все эти плюсы-минусы и кавычки не интуитивны для неподготовленного пользователя.
    Нам потребуется создать интерфейс пользователя для формирования полнотекстового запроса. Вероятно, центральным элементом интерфейса будет древовидное представление запроса, поскольку в нём возможны скобки логические условия (AND, OR...).

    Ввиду [1], вероятно, потребуется разработать свой диалект синтаксиса для полнотекстовых запросов, который будет при исполнении транслироваться в строку для полнотекстового запроса к используемой СУБД.
    Ввиду [2] удобным способом описания языка будет набор соответствующих объектов.
    Таким образом, для сохранения полнотекстового запроса, сформированного пользователем с использованием интерактивных средств будет сериализация построенной совокупности объектов.

    Выше была преамбула. Теперь — амбула :-)
    Как быть, если мы хотим построить запрос полнотекстового поиска в коде? Разумеется, отвлекаясь от специфики СУБД, в контексте которой он будет исполняться.
    Очевидно, нам следует прибегнуть к нашему диалекту языка полнотекстовых запросов. По-простому — построить совокупность объектов.
    Как это сделать проще всего? - Что если попробовать fluent-технику?
    {code}
    _ // сверхпроводимость -"вики" -"яндекс" +"левитация" +"видео"
    _ full_text_req := NewFullTextRequest()
    _ _ .AND_Bracket([
    _ _ _ FullText_Words(['сверхпроводимость']),
    _ _ _ FullText_Minus(['вики', 'яндекс']),
    _ _ _ FullText_Plus(['левитация', 'видео'])
    _ _ ]);
    {/code}
    Что же здесь общего с DSL? - Кое-что есть :-)
    Например, проверку синтаксиса нашего «диалекта» языка запросов мы возлагаем на компилятор Delphi.
    Так, NewFullTextRequest возвращает интерфейс IFullTextRequest, содержащий методы AND_Bracket и OR_Bracket – скобки для условий поиска, элементы которых объединены условиями, соответственно, AND или OR.
    Подобно этому, AND/OR_Bracket принимают в качестве параметра массив элементов типа IFullTextCondition, которые умеют создавать функции FullText_Words/Minus/Plus, которые, в свою очередь, принимают на вход массивы строк, посредством которых описываются условия.
    Далее, если потребуется расширить наш язык новыми понятиями, мы можем следовать парадигме: определить место в существующей структуре, в которой востребованы новые понятия и поддержать соответствующие fluent-методы.
    Например: новый вид условий XOR можно поддержать «рядом» с AND/OR_Bracket.

    Отвечу на один вопрос из множества, которые могут появиться.
    [Q] Зачем «городить огород» с описанием диалекта посредством объектов? Почему не парсить строчку, оговорив предварительно её формат? Т.е. сформулировав диалект.
    [A] Например, по следующим причинам:
    1. При изменениях в языке, запросы, описанные в коде в виде строчек приведут к ошибкам при выполнении, а записанные во fluent-технике к ошибкам компиляции.
    2. Работать с объектами проще, чем со строками, поскольку пропадает потребность в разборе строк. А обрабатывать запрос, сформулированный в нашем диалекте придётся перед его выполнением средствами используемой СУБД.
    3. Объекты, вероятно, всё равно потребуются ввиду [2] технических требований. Поэтому, строки можно рассматривать как формат сериализации этих объектов, но если из технических соображений сделать объекты потомками TComponent, «из коробки» станет доступна и сериализация.

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

    ОтветитьУдалить