В чем смысл DSL / Fluent-интерфейсов

Я недавно смотрел веб-трансляцию о том, как создать свободный DSL, и должен признать, что не понимаю причин, по которым можно было бы использовать такой подход (по крайней мере, для данного примера).

Веб-конференция представила класс изменения размера изображения, который позволяет вам указать входное изображение, изменить его размер и сохранить его в выходной файл, используя следующий синтаксис (с использованием C#):

Sizer sizer = new Sizer();
sizer.FromImage(inputImage)
     .ToLocation(outputImage)
     .ReduceByPercent(50)
     .OutputImageFormat(ImageFormat.Jpeg)
     .Save();

Я не понимаю, чем это лучше, чем "обычный" метод, который принимает некоторые параметры:

sizer.ResizeImage(inputImage, outputImage, 0.5, ImageFormat.Jpeg);

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

sizer.ToLocation(outputImage).Save();

Итак, на мои вопросы:

1 - Есть ли способ улучшить удобство использования свободного интерфейса (т. Е. Сообщить пользователю, что он должен делать)?

2. Является ли этот гибкий интерфейс просто заменой несуществующих параметров именованного метода в C#? Могут ли именованные параметры сделать свободные интерфейсы устаревшими, например, что-то подобное предлагает объект C:

sizer.Resize(from:input, to:output, resizeBy:0.5, ..)

3 - Используются ли свободные интерфейсы чрезмерно просто потому, что они сейчас популярны?

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

Кстати: я знаю о jquery и вижу, насколько легко он упрощает работу, поэтому я не ищу комментариев по этому или другим существующим примерам.

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

Ответов (7)

Решение

2. Является ли этот гибкий интерфейс просто заменой несуществующих параметров именованного метода в C#? Могут ли именованные параметры сделать свободные интерфейсы устаревшими, например, что-то подобное предлагает объект C:

Ну да и нет. Свободный интерфейс дает вам большую гибкость. То, что не может быть достигнуто с помощью named params:

sizer.FromImage(i)
 .ReduceByPercent(x)
 .Pixalize()
 .ReduceByPercent(x)
 .OutputImageFormat(ImageFormat.Jpeg)
 .ToLocation(o)
 .Save();

FromImage, ToLocation и OutputImageFormat в гибком интерфейсе кажутся мне неприятным запахом. Вместо этого я бы сделал что-то в этом роде, что, на мой взгляд, гораздо яснее.

 new Sizer("bob.jpeg") 
 .ReduceByPercent(x)
 .Pixalize()
 .ReduceByPercent(x)
 .Save("file.jpeg",ImageFormat.Jpeg);

Свободные интерфейсы имеют те же проблемы, что и многие методы программирования, они могут использоваться неправильно, чрезмерно или недостаточно. Я думаю, что при эффективном использовании этой техники можно создать более богатую и лаконичную модель программирования. Даже StringBuilder поддерживает это.

var sb = new StringBuilder(); 
sb.AppendLine("Hello")
 .AppendLine("World"); 

В дополнение к предложению @ sam-saffron относительно гибкости Fluent Interface при добавлении новой операции:

Если нам нужно было добавить новую операцию, такую ​​как Pixalize (), то в сценарии «метод с несколькими параметрами» для этого потребуется добавить новый параметр в сигнатуру метода. Затем может потребоваться модификация каждого вызова этого метода во всей кодовой базе, чтобы добавить значение для этого нового параметра (если используемый язык не допускает необязательный параметр).

Следовательно, одним из возможных преимуществ Fluent Interface является ограничение воздействия будущих изменений.

Это один из способов реализовать что-то.

Для объектов, которые ничего не делают, кроме манипулирования одним и тем же элементом снова и снова, в этом нет ничего плохого. Рассмотрим потоки C++: они лучшие в этом интерфейсе. Каждая операция снова возвращает поток, поэтому вы можете объединить в цепочку еще одну операцию потока.

Если вы выполняете LINQ и снова и снова манипулируете объектом, это имеет смысл.

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

var obj1 = object.Shrink(0.50); // obj1 is now 50% of obj2
var obj2 = object.Shrink(0.75); // is ojb2 now 75% of ojb1 or is it 75% of the original?

Если obj2 составлял 75% от исходного объекта, это означает, что вы каждый раз делаете полную копию объекта (и во многих случаях имеет свои преимущества, например, если вы пытаетесь создать два экземпляра одного и того же объекта, но немного иначе).

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

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

Рассмотреть возможность:

sizer.ResizeImage(inputImage, outputImage, 0.5, ImageFormat.Jpeg);

Что делать, если вы использовали менее понятные имена переменных:

sizer.ResizeImage(i, o, x, ImageFormat.Jpeg);

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

Благодаря свободному интерфейсу это становится яснее:

 sizer.FromImage(i)
 .ToLocation(o)
 .ReduceByPercent(x)
 .OutputImageFormat(ImageFormat.Jpeg)
 .Save();

Также не важен порядок методов. Это эквивалентно:

 sizer.FromImage(i)
 .ReduceByPercent(x)
 .OutputImageFormat(ImageFormat.Jpeg)
 .ToLocation(o)
 .Save();

Кроме того, возможно, у вас могут быть значения по умолчанию для формата выходного изображения и уменьшения, поэтому это может быть:

 sizer.FromImage(i)
 .ToLocation(o)
 .Save();

Для достижения того же эффекта потребуются перегруженные конструкторы.

Я бы сказал, что с плавными интерфейсами немного переборщили, и я думаю, что вы выбрали только один такой пример.

Я считаю, что свободный интерфейс особенно хорош, когда вы создаете с его помощью сложную модель. Под моделью я имею в виду, например, сложную взаимосвязь экземпляров объектов. Тогда свободный интерфейс - это способ помочь разработчику правильно конструировать экземпляры семантической модели. Такой плавный интерфейс - отличный способ отделить механику и взаимосвязи модели от «грамматики», которую вы используете для построения модели, по существу скрывая детали от конечного пользователя и сокращая количество доступных глаголов, возможно, до тех, которые имеют отношение к конкретный сценарий.

Ваш пример кажется излишним.

Недавно я сделал некоторый свободный интерфейс поверх SplitterContainer из Windows Forms. Возможно, семантическую модель иерархии элементов управления сложно правильно построить. Предоставляя небольшой свободный API, разработчик теперь может декларативно выразить, как его SplitterContainer должен работать. Использование идет как

var s = new SplitBoxSetup();
s.AddVerticalSplit()
 .PanelOne().PlaceControl(()=> new Label())
 .PanelTwo()
 .AddHorizontalSplit()
 .PanelOne().PlaceControl(()=> new Label())
 .PanelTwo().PlaceControl(()=> new Panel());
form.Controls.Add(s.TopControl);

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

Надеюсь это поможет

Вам следует прочитать Domain Driven DesignЭрика Эванса, чтобы понять, почему DSL считается хорошим выбором для дизайна.

Книга полна хороших примеров, советов по передовой практике и шаблонов проектирования. Настоятельно рекомендуется.

Можно использовать вариант интерфейса Fluent для принудительного применения определенных комбинаций необязательных параметров (например, требовать, чтобы присутствовал хотя бы один параметр из группы, и требовать, чтобы, если задан определенный параметр, какой-либо другой параметр должен быть опущен). Например, можно было бы предоставить функциональность, аналогичную Enumerable.Range, но с синтаксисом вроде IntRange.From (5) .Upto (19) или IntRange.From (5) .LessThan (10) .Stepby (2) или IntRange ( 3) .Count (19) .StepBy (17). Принуждение во время компиляции чрезмерно сложных требований к параметрам может потребовать определения раздражающего количества структур или классов промежуточных значений, но в некоторых случаях такой подход может оказаться полезным в более простых случаях.