Fluent interface
- 1 year ago
- 0
- 0
Текучий интерфейс ( англ. fluent interface — в значении «плавный» или «гладкий» «интерфейс») в разработке программного обеспечения — способ реализации объектно-ориентированного API , нацеленный на повышение читабельности исходного кода программы. Название придумано Эриком Эвансом и Мартином Фаулером .
Текучий интерфейс хорош тем, что упрощается множественный вызов методов одного объекта. Обычно это реализуется использованием , передающих контекст вызова следующему звену (но текучий интерфейс влечет за собой нечто большее, чем просто цепочку методов ). Обычно, этот контекст:
Такой стиль косвенно полезен повышением наглядности и интуитивности кода [ источник не указан 4580 дней ] . Однако может весьма пагубно сказаться на отладке, если цепочка действует как одно выражение, куда отладчик не всегда может установить промежуточную точку останова .
Следующий пример показывает обычный класс и класс, реализующий текучий интерфейс, и различия в использовании. Пример написан на Delphi Object Pascal:
unit FluentInterface; interface type IConfiguration = interface procedure SetColor(Color: string); procedure SetHeight(height: integer); procedure SetLength(length: integer); procedure SetDepth(depth: integer); end; IConfigurationFluent = interface function SetColor(Color: string): IConfigurationFluent; function SetHeight(height: integer): IConfigurationFluent; function SetLength(length: integer): IConfigurationFluent; function SetDepth(depth: integer): IConfigurationFluent; end; TConfiguration = class(TInterfacedObject, IConfiguration) private FColor: string; FHeight: integer; FLength: integer; FDepth: integer; protected procedure SetColor(Color: string); procedure SetHeight(height: integer); procedure SetLength(length: integer); procedure SetDepth(depth: integer); end; TConfigurationFluent = class(TInterfacedObject, IConfigurationFluent) private FColor: string; FHeight: integer; FLength: integer; FDepth: integer; protected function SetColor(Color: string): IConfigurationFluent; function SetHeight(height: integer): IConfigurationFluent; function SetLength(length: integer): IConfigurationFluent; function SetDepth(depth: integer): IConfigurationFluent; public class function New: IConfigurationFluent; end; implementation procedure TConfiguration.SetColor(Color: string); begin FColor := Color; end; procedure TConfiguration.SetDepth(depth: integer); begin FDepth := depth; end; procedure TConfiguration.SetHeight(height: integer); begin FHeight := height; end; procedure TConfiguration.SetLength(length: integer); begin FLength := length; end; class function TConfigurationFluent.New: IConfigurationFluent; begin Result := Create; end; function TConfigurationFluent.SetColor(Color: string): IConfigurationFluent; begin FColor := Color; Result := Self; end; function TConfigurationFluent.SetDepth(depth: integer): IConfigurationFluent; begin FDepth := depth; Result := Self; end; function TConfigurationFluent.SetHeight(height: integer): IConfigurationFluent; begin FHeight := height; Result := Self; end; function TConfigurationFluent.SetLength(length: integer): IConfigurationFluent; begin FLength := length; Result := Self; end; end.
var C, D: IConfiguration; E: IConfigurationFluent; begin { Обычное использование:} C := TConfiguration.Create; C.SetColor('blue'); C.SetHeight(1); C.SetLength(2); C.SetDepth(3); { обычная реализация, упрощенная с помощью инструкции with } D := TConfiguration.Create; with D do begin SetColor('blue'); SetHeight(1); SetLength(2); SetDepth(3) end; { использование реализации с текучим интерфейсом } E := TConfigurationFluent.New .SetColor('Blue') .SetHeight(1) .SetLength(2) .SetDepth(3); end;
Начиная с C# 3.5 и выше введены продвинутые способы реализации текучего интерфейса:
namespace Example.FluentInterfaces { #region Standard Example public interface IConfiguration { string Color { set; } int Height { set; } int Length { set; } int Depth { set; } } public class Configuration : IConfiguration { string color; int height; int length; int depth; public string Color { set { color = value; } } public int Height { set { height = value; } } public int Length { set { length = value; } } public int Depth { set { depth = value; } } } #endregion #region Fluent Example public interface IConfigurationFluent { IConfigurationFluent SetColor(string color); IConfigurationFluent SetHeight(int height); IConfigurationFluent SetLength(int length); IConfigurationFluent SetDepth(int depth); } public class ConfigurationFluent : IConfigurationFluent { string color; int height; int length; int depth; public IConfigurationFluent SetColor(string color) { this.color = color; return this; } public IConfigurationFluent SetHeight(int height) { this.height = height; return this; } public IConfigurationFluent SetLength(int length) { this.length = length; return this; } public IConfigurationFluent SetDepth(int depth) { this.depth = depth; return this; } } #endregion public class ExampleProgram { public static void Main(string[] args) { // Обычный пример IConfiguration config = new Configuration { Color = "blue", Height = 1, Length = 2, Depth = 3 }; // Пример текучего интерфейса IConfigurationFluent fluentConfig = new ConfigurationFluent().SetColor("blue") .SetHeight(1) .SetLength(2) .SetDepth(3); } } }
Банальный пример в C++ — стандартный iostream , где текучесть обеспечивается перегрузкой операторов .
Пример обертки текучего интерфейса в C++:
// обычное задание class GlutApp { private: int w_, h_, x_, y_, argc_, display_mode_; char **argv_; char *title_; public: GlutApp(int argc, char** argv) { argc_ = argc; argv_ = argv; } void setDisplayMode(int mode) { display_mode_ = mode; } int getDisplayMode() { return display_mode_; } void setWindowSize(int w, int h) { w_ = w; h_ = h; } void setWindowPosition(int x, int y) { x_ = x; y_ = y; } void setTitle(const char *title) { title_ = title; } void create(); }; // обычное использование int main(int argc, char **argv) { GlutApp app(argc, argv); app.setDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_ALPHA|GLUT_DEPTH); // Set framebuffer params app.setWindowSize(500, 500); // Set window params app.setWindowPosition(200, 200); app.setTitle("My OpenGL/GLUT App"); app.create(); } // Обертка текучего интерфейса class FluentGlutApp : private GlutApp { public: FluentGlutApp(int argc, char **argv) : GlutApp(argc, argv) {} // наследуем родительский конструктор FluentGlutApp &withDoubleBuffer() { setDisplayMode(getDisplayMode() | GLUT_DOUBLE); return *this; } FluentGlutApp &withRGBA() { setDisplayMode(getDisplayMode() | GLUT_RGBA); return *this; } FluentGlutApp &withAlpha() { setDisplayMode(getDisplayMode() | GLUT_ALPHA); return *this; } FluentGlutApp &withDepth() { setDisplayMode(getDisplayMode() | GLUT_DEPTH); return *this; } FluentGlutApp &across(int w, int h) { setWindowSize(w, h); return *this; } FluentGlutApp &at(int x, int y) { setWindowPosition(x, y); return *this; } FluentGlutApp &named(const char *title) { setTitle(title); return *this; } // без разницы, вести ли цепь после вызова create(), поэтому не возвращаем *this void create() { GlutApp::create(); } }; // используем текучий интерфейс int main(int argc, char **argv) { FluentGlutApp app(argc, argv) .withDoubleBuffer().withRGBA().withAlpha().withDepth() .at(200, 200).across(500, 500) .named("My OpenGL/GLUT App"); app.create(); }
Некоторые API в Java реализуют такой интерфейс, например Java Persistence API :
public Collection<Student> findByNameAgeGender(String name, int age, Gender gender) { return em.createNamedQuery("Student.findByNameAgeGender") .setParameter("name", name) .setParameter("age", age) .setParameter("gender", gender) .setFirstResult(1) .setMaxResults(30) .setHint("hintName", "hintValue") .getResultList(); }
Библиотека позволяет использовать текучий интерфейс для выполнения вспомогательных задач, вроде итерирования структур , конвертирования информации, фильтрации, и т. д.
String[] datesStr = new String[] {"12-10-1492", "06-12-1978" }; ... List<Calendar> dates = Op.on(datesStr).toList().map(FnString.toCalendar("dd-MM-yyyy")).get();
Также, библиотека Mock-объект тестирования активно использует этот стиль для предоставления удобного интерфейса.
Collection mockCollection = EasyMock.createMock(Collection.class); EasyMock.expect(mockCollection.remove(null)).andThrow(new NullPointerException()).atLeastOnce();
Пример реализации класса с текучим интерфейсом в PHP :
class Car { private $speed, $color, $doors; public function setSpeed($speed){ $this->speed = $speed; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function setDoors($doors) { $this->doors = $doors; return $this; } } // Обычная реализация $myCar2 = new Car(); $myCar2->setSpeed(100); $myCar2->setColor('blue'); $myCar2->setDoors(5); // Текучий интерфейс $myCar = new Car(); $myCar->setSpeed(100)->setColor('blue')->setDoors(5);
Пример реализации класса с текучим интерфейсом в JavaScript :
var Car = (function(){ var speed, color, doors, pub; function setSpeed(new_speed) { speed = new_speed; return pub; } function setColor(new_color) { color = new_color; return pub; } function setDoors(new_doors) { doors = new_doors; return pub; } pub = { 'setSpeed': setSpeed, 'setColor': setColor, 'setDoors': setDoors, }; return pub; }) // Обычная реализация myCar2 = Car(); myCar2.setSpeed(100); myCar2.setColor('blue'); myCar2.setDoors(5); // Текучий интерфейс myCar = Car(); myCar.setSpeed(100).setColor('blue').setDoors(5);
Также можно использовать иной подход:
var $ = function(selector) { if(this.$) { return new $(selector); } if(typeof selector == "string") { this.init = document.getElementById(selector); } }; $.prototype = { text: function(text) { if(!text){ this.init.innerHTML; } this.init.innerHTML = text; return this; }, css: function(style) { for(var i in style){ this.init.style[i] = style[i]; } return this; } }; //пример использования: $('div').text('div').css({color: "red"});
Пример независящей от типа возвращаемого объекта реализации:
({ foo: function (a) { return a; } }).foo('foo').toUpperCase();