Fluent interface
- 1 year ago
- 0
- 0
Текучий интерфейс ( англ. fluent interface — в значении «плавный» или «гладкий» «интерфейс») в разработке программного обеспечения — способ реализации объектно-ориентированного API , нацеленный на повышение читабельности исходного кода программы. Название придумано Эриком Эвансом и Мартином Фаулером .
Текучий интерфейс хорош тем, что упрощается множественный вызов методов одного объекта. Обычно это реализуется использованием , передающих контекст вызова следующему звену (но текучий интерфейс влечет за собой нечто большее, чем просто цепочку методов ). Обычно, этот контекст:
Такой стиль косвенно полезен повышением наглядности и интуитивности кода [ источник не указан 4650 дней ] . Однако может весьма пагубно сказаться на отладке, если цепочка действует как одно выражение, куда отладчик не всегда может установить промежуточную точку останова .
Следующий пример показывает обычный класс и класс, реализующий текучий интерфейс, и различия в использовании. Пример написан на 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();