Jump to content

Свободный интерфейс

В разработке программного обеспечения свободный интерфейс — это объектно-ориентированный API , конструкция которого во многом зависит от цепочки методов . Его цель — повысить разборчивость кода за счет создания предметно-ориентированного языка (DSL). Этот термин был придуман в 2005 году Эриком Эвансом и Мартином Фаулером . [1]

Выполнение

[ редактировать ]

Свободный интерфейс обычно реализуется с использованием цепочки методов для реализации каскадирования методов (в языках, которые не поддерживают каскадирование), в частности, когда каждый метод возвращает объект, к которому он прикреплен, часто называемый this или self. Говоря более абстрактно, беглый интерфейс передает контекст инструкции последующего вызова в цепочке методов, где обычно контекст

  • Определяется через возвращаемое значение вызываемого метода
  • Самореферентный , где новый контекст эквивалентен последнему контексту.
  • Завершено из-за возврата пустого контекста

Обратите внимание, что «свободный интерфейс» означает больше, чем просто каскадирование методов посредством цепочки; это влечет за собой разработку интерфейса, который читается как DSL, с использованием других методов, таких как «вложенные функции и область видимости объекта». [1]

Термин «свободный интерфейс» был придуман в конце 2005 года, хотя этот общий стиль интерфейса восходит к изобретению каскадирования методов в Smalltalk в 1970-х годах и многочисленных примеров в 1980-х. Типичным примером является библиотека iostream в C++ , которая использует << или >> операторы для передачи сообщений, отправки нескольких данных одному и тому же объекту и разрешения «манипуляторов» для вызовов других методов. Другие ранние примеры включают систему Garnet (с 1988 года на Lisp) и систему Amulet (с 1994 года на C++), которые использовали этот стиль для создания объектов и назначения свойств.

C# широко использует свободное программирование в LINQ для построения запросов с использованием «стандартных операторов запросов». Реализация основана на методах расширения .

var translations = new Dictionary<string, string>
{
    {"cat", "chat"},
    {"dog", "chien"},
    {"fish", "poisson"},
    {"bird", "oiseau"}
};

// Find translations for English words containing the letter "a",
// sorted by length and displayed in uppercase
IEnumerable<string> query = translations
	.Where(t => t.Key.Contains("a"))
	.OrderBy(t => t.Value.Length)
	.Select(t => t.Value.ToUpper());

// The same query constructed progressively:
var filtered   = translations.Where(t => t.Key.Contains("a"));
var sorted     = filtered.OrderBy  (t => t.Value.Length);
var finalQuery = sorted.Select     (t => t.Value.ToUpper());

Интерфейс Fluent также можно использовать для объединения набора методов, которые управляют одним и тем же объектом или совместно используют его. Вместо создания класса клиента мы можем создать контекст данных, который можно украсить плавным интерфейсом следующим образом.

// Defines the data context
class Context
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Sex { get; set; }
    public string Address { get; set; }
}

class Customer
{
    private Context _context = new Context(); // Initializes the context

    // set the value for properties
    public Customer FirstName(string firstName)
    {
        _context.FirstName = firstName;
        return this;
    }

    public Customer LastName(string lastName)
    {
        _context.LastName = lastName;
        return this;
    }

    public Customer Sex(string sex)
    {
        _context.Sex = sex;
        return this;
    }

    public Customer Address(string address)
    {
        _context.Address = address;
        return this;
    }

    // Prints the data to console
    public void Print()
    {
        Console.WriteLine($"First name: {_context.FirstName} \nLast name: {_context.LastName} \nSex: {_context.Sex} \nAddress: {_context.Address}");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Object creation
        Customer c1 = new Customer();
        // Using the method chaining to assign & print data with a single line
        c1.FirstName("vinod").LastName("srivastav").Sex("male").Address("bangalore").Print();
    }
}

Распространенным использованием интерфейса Fluent в C++ является стандартный iostream , который объединяет перегруженные операторы в цепочку .

Ниже приведен пример предоставления оболочки свободного интерфейса поверх более традиционного интерфейса на C++:

 // Basic definition
 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(){;}
 };
 // Basic usage
 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();
 }

 // Fluent wrapper
 class FluentGlutApp : private GlutApp {
 public:
     FluentGlutApp(int argc, char **argv) : GlutApp(argc, argv) {} // Inherit parent constructor
     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;
     }
     // It doesn't make sense to chain after create(), so don't return *this
     void create() {
         GlutApp::create();
     }
 };
 // Fluent usage
 int main(int argc, char **argv) {
     FluentGlutApp(argc, argv)
         .withDoubleBuffer().withRGBA().withAlpha().withDepth()
         .at(200, 200).across(500, 500)
         .named("My OpenGL/GLUT App")
         .create();
 }

Пример свободного тестирования в среде тестирования jMock: [1]

mock.expects(once()).method("m").with( or(stringContains("hello"),
                                          stringContains("howdy")) );

Библиотека jOOQ моделирует SQL как свободный API на Java:

Author author = AUTHOR.as("author");
create.selectFrom(author)
      .where(exists(selectOne()
                   .from(BOOK)
                   .where(BOOK.STATUS.eq(BOOK_STATUS.SOLD_OUT))
                   .and(BOOK.AUTHOR_ID.eq(author.ID))));

Процессор аннотаций fluflu позволяет создавать гибкий API с использованием аннотаций Java.

Библиотека JaQue позволяет представлять лямбды Java 8 в виде объектов в виде деревьев выражений во время выполнения, что позволяет создавать типобезопасные интерфейсы, т.е. вместо:

Customer obj = ...
obj.property("name").eq("John")

Можно написать:

method<Customer>(customer -> customer.getName() == "John")

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

Collection mockCollection = EasyMock.createMock(Collection.class);
EasyMock
    .expect(mockCollection.remove(null))
    .andThrow(new NullPointerException())
    .atLeastOnce();

В Java Swing API интерфейс LayoutManager определяет, как объекты-контейнеры могут контролировать размещение компонентов. Один из наиболее мощных LayoutManager реализациями является класс GridBagLayout, который требует использования GridBagConstraints класс, чтобы указать, как происходит управление макетом. Типичный пример использования этого класса выглядит примерно следующим образом.

GridBagLayout gl = new GridBagLayout();
JPanel p = new JPanel();
p.setLayout( gl );

JLabel l = new JLabel("Name:");
JTextField nm = new JTextField(10);

GridBagConstraints gc = new GridBagConstraints();
gc.gridx = 0;
gc.gridy = 0;
gc.fill = GridBagConstraints.NONE;
p.add( l, gc );

gc.gridx = 1;
gc.fill = GridBagConstraints.HORIZONTAL;
gc.weightx = 1;
p.add( nm, gc );

Это создает много кода и затрудняет понимание того, что именно здесь происходит. Packer class предоставляет гибкий механизм, поэтому вместо этого вы должны написать: [2]

JPanel p = new JPanel();
Packer pk = new Packer( p );

JLabel l = new JLabel("Name:");
JTextField nm = new JTextField(10);

pk.pack( l ).gridx(0).gridy(0);
pk.pack( nm ).gridx(1).gridy(0).fillx();

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

Существует множество примеров библиотек JavaScript, использующих тот или иной вариант этого: jQuery, вероятно, наиболее известен. Обычно для реализации «запросов к базе данных» используются флюентные компоновщики, например в клиентской библиотеке Dynamite:

// getting an item from a table
client.getItem('user-table')
    .setHashKey('userId', 'userA')
    .setRangeKey('column', '@')
    .execute()
    .then(function(data) {
        // data.result: the resulting object
    })

Простой способ сделать это в JavaScript — использовать наследование прототипов и this.

// example from https://schier.co/blog/2013/11/14/method-chaining-in-javascript.html

class Kitten {
  constructor() {
    this.name = 'Garfield';
    this.color = 'orange';
  }

  setName(name) {
    this.name = name;
    return this;
  }

  setColor(color) {
    this.color = color;
    return this;
  }

  save() {
    console.log(
      `saving ${this.name}, the ${this.color} kitten`
    );
    return this;
  }
}

// use it
new Kitten()
  .setName('Salem')
  .setColor('black')
  .save();

Scala поддерживает свободный синтаксис как для вызовов методов, так и для примесей классов , используя типажи и with ключевое слово. Например:

class Color { def rgb(): Tuple3[Decimal] }
object Black extends Color { override def rgb(): Tuple3[Decimal] = ("0", "0", "0"); }

trait GUIWindow {
  // Rendering methods that return this for fluent drawing
  def set_pen_color(color: Color): this.type
  def move_to(pos: Position): this.type
  def line_to(pos: Position, end_pos: Position): this.type

  def render(): this.type = this // Don't draw anything, just return this, for child implementations to use fluently

  def top_left(): Position
  def bottom_left(): Position
  def top_right(): Position
  def bottom_right(): Position
}

trait WindowBorder extends GUIWindow {
  def render(): GUIWindow = {
    super.render()
      .move_to(top_left())
      .set_pen_color(Black)
      .line_to(top_right())
      .line_to(bottom_right())
      .line_to(bottom_left())
      .line_to(top_left())
   }
}

class SwingWindow extends GUIWindow { ... }

val appWin = new SwingWindow() with WindowBorder
appWin.render()

В Raku существует множество подходов, но один из самых простых — объявить атрибуты как доступные для чтения/записи и использовать given ключевое слово. Аннотации типов не являются обязательными, но встроенная постепенная типизация делает запись непосредственно в общедоступные атрибуты гораздо безопаснее.

class Employee {
    subset Salary         of Real where * > 0;
    subset NonEmptyString of Str  where * ~~ /\S/; # at least one non-space character

    has NonEmptyString $.name    is rw;
    has NonEmptyString $.surname is rw;
    has Salary         $.salary  is rw;

    method gist {
        return qq:to[END];
        Name:    $.name
        Surname: $.surname
        Salary:  $.salary
        END
    }
}
my $employee = Employee.new();

given $employee {
    .name    = 'Sally';
    .surname = 'Ride';
    .salary  = 200;
}

say $employee;

# Output:
# Name:    Sally
# Surname: Ride
# Salary:  200

В PHP можно вернуть текущий объект, используя метод $this специальная переменная, представляющая экземпляр. Следовательно return $this; заставит метод вернуть экземпляр. В приведенном ниже примере определяется класс Employee и три способа установить имя, фамилию и зарплату. Каждый возвращает экземпляр Employee класс, позволяющий связывать методы.

class Employee
{
    private string $name;
    private string $surname; 
    private string $salary;

    public function setName(string $name)
    {
        $this->name = $name;

        return $this;
    }

    public function setSurname(string $surname)
    {
        $this->surname = $surname;

        return $this;
    }

    public function setSalary(string $salary)
    {
        $this->salary = $salary;

        return $this;
    }

    public function __toString()
    {
        $employeeInfo = 'Name: ' . $this->name . PHP_EOL;
        $employeeInfo .= 'Surname: ' . $this->surname . PHP_EOL;
        $employeeInfo .= 'Salary: ' . $this->salary . PHP_EOL;

        return $employeeInfo;
    }
}

# Create a new instance of the Employee class, Tom Smith, with a salary of 100:
$employee = (new Employee())
                ->setName('Tom')
                ->setSurname('Smith')
                ->setSalary('100');

# Display the value of the Employee instance:
echo $employee;

# Display:
# Name: Tom
# Surname: Smith
# Salary: 100

В Python возвращение self в методе экземпляра — это один из способов реализации беглого шаблона.

Однако создатель языка Гвидо ван Россум не одобряет этого. [3] и поэтому считается непитоническим (не идиоматическим) для операций, которые не возвращают новые значения. Ван Россум приводит операции обработки строк в качестве примера, когда он считает подходящим плавный шаблон.

class Poem:
    def __init__(self, title: str) -> None:
        self.title = title

    def indent(self, spaces: int):
        """Indent the poem with the specified number of spaces."""
        self.title = " " * spaces + self.title
        return self

    def suffix(self, author: str):
        """Suffix the poem with the author name."""
        self.title = f"{self.title} - {author}"
        return self
>>> Poem("Road Not Travelled").indent(4).suffix("Robert Frost").title
'    Road Not Travelled - Robert Frost'

В Swift 3.0+ возвращение self в функциях — это один из способов реализации беглого шаблона.

class Person {
    var firstname: String = ""
    var lastname: String = ""
    var favoriteQuote: String = ""

    @discardableResult
    func set(firstname: String) -> Self {
        self.firstname = firstname
        return self
    }

    @discardableResult
    func set(lastname: String) -> Self {
        self.lastname = lastname
        return self
    }

    @discardableResult
    func set(favoriteQuote: String) -> Self {
        self.favoriteQuote = favoriteQuote
        return self
    }
}
let person = Person()
    .set(firstname: "John")
    .set(lastname: "Doe")
    .set(favoriteQuote: "I like turtles")

Неизменяемость

[ редактировать ]

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

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

Пример JavaScript

[ редактировать ]

Используя семантику копирования при записи, приведенный выше пример JavaScript выглядит следующим образом:

class Kitten {
  constructor() {
    this.name = 'Garfield';
    this.color = 'orange';
  }

  setName(name) {
    const copy = new Kitten();
    copy.color = this.color;
    copy.name = name;
    return copy;
  }

  setColor(color) {
    const copy = new Kitten();
    copy.name = this.name;
    copy.color = color;
    return copy;
  }

  // ...
}

// use it
const kitten1 = new Kitten()
  .setName('Salem');

const kitten2 = kitten1
  .setColor('black');

console.log(kitten1, kitten2);
// -> Kitten({ name: 'Salem', color: 'orange' }), Kitten({ name: 'Salem', color: 'black' })

Проблемы

[ редактировать ]

Ошибки не могут быть зафиксированы во время компиляции.

[ редактировать ]

В типизированных языках использование конструктора, требующего всех параметров, приведет к сбою во время компиляции, в то время как свободный подход сможет генерировать только ошибки во время выполнения , пропуская все проверки безопасности типов современных компиляторов. Это также противоречит « быстродействующему » подходу к защите от ошибок.

Отладка и отчеты об ошибках

[ редактировать ]

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

java.nio.ByteBuffer.allocate(10).rewind().limit(100);

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

java.nio.ByteBuffer
    .allocate(10)
    .rewind()
    .limit(100);

Однако некоторые отладчики всегда показывают первую строку в трассировке исключения, хотя исключение было создано в любой строке.

Ведение журнала

[ редактировать ]

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

ByteBuffer buffer = ByteBuffer.allocate(10).rewind().limit(100);

Чтобы зарегистрировать состояние buffer после rewind() вызов метода, необходимо разбить беглые вызовы:

ByteBuffer buffer = ByteBuffer.allocate(10).rewind();
log.debug("First byte after rewind is " + buffer.get(0));
buffer.limit(100);

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

static class ByteBufferExtensions
{
    public static ByteBuffer Log(this ByteBuffer buffer, Log log, Action<ByteBuffer> getMessage)
    {
        string message = getMessage(buffer);
        log.debug(message);
        return buffer;
    } 
}

// Usage:
ByteBuffer
    .Allocate(10)
    .Rewind()
    .Log( log, b => "First byte after rewind is " + b.Get(0) )
    .Limit(100);

Подклассы

[ редактировать ]

Подклассам в строго типизированных языках (C++, Java, C# и т. д.) часто приходится переопределять все методы своего суперкласса, которые участвуют в свободном интерфейсе, чтобы изменить тип возвращаемого значения. Например:

class A {
    public A doThis() { ... }
}
class B extends A{
    public B doThis() { super.doThis(); return this; } // Must change return type to B.
    public B doThat() { ... }
}
...
A a = new B().doThat().doThis(); // This would work even without overriding A.doThis().
B b = new B().doThis().doThat(); // This would fail if A.doThis() wasn't overridden.

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

abstract class AbstractA<T extends AbstractA<T>> {
	@SuppressWarnings("unchecked")
	public T doThis() { ...; return (T)this; }
}	
class A extends AbstractA<A> {}
	
class B extends AbstractA<B> {
	public B doThat() { ...; return this; }
}

...
B b = new B().doThis().doThat(); // Works!
A a = new A().doThis();          // Also works.

Обратите внимание: чтобы иметь возможность создавать экземпляры родительского класса, нам пришлось разделить его на два класса — AbstractA и A, последний без содержимого (он будет содержать только конструкторы, если они необходимы). Этот подход можно легко расширить, если мы хотим иметь подподклассы (и т. д.):

abstract class AbstractB<T extends AbstractB<T>> extends AbstractA<T> {
	@SuppressWarnings("unchecked")
	public T doThat() { ...; return (T)this; }
}
class B extends AbstractB<B> {}

abstract class AbstractC<T extends AbstractC<T>> extends AbstractB<T> {
	@SuppressWarnings("unchecked")
	public T foo() { ...; return (T)this; }
}
class C extends AbstractC<C> {}
...
C c = new C().doThis().doThat().foo(); // Works!
B b = new B().doThis().doThat();       // Still works.

В зависимо типизированном языке, например Scala, методы также могут быть явно определены как всегда возвращающие this и, следовательно, может быть определен только один раз, чтобы подклассы могли воспользоваться преимуществами свободного интерфейса:

class A {
    def doThis(): this.type = { ... } // returns this, and always this.
}
class B extends A{
    // No override needed!
    def doThat(): this.type = { ... }
}
...
val a: A = new B().doThat().doThis(); // Chaining works in both directions.
val b: B = new B().doThis().doThat(); // And, both method chains result in a B!

См. также

[ редактировать ]
  1. ^ Jump up to: а б с Мартин Фаулер , « FluentInterface », 20 декабря 2005 г.
  2. ^ «Пакет Интерфейсов200.Пакер» . Оракул . Проверено 13 ноября 2019 г. .
  3. ^ Россум, Гвидо ван (17 октября 2003 г.). «[Python-Dev] возвращаемое значение sort()» . Проверено 1 февраля 2022 г.
[ редактировать ]
Arc.Ask3.Ru: конец переведенного документа.
Arc.Ask3.Ru
Номер скриншота №: 78bb4de3ece6d777e3a528f5d26a7d8e__1713547800
URL1:https://arc.ask3.ru/arc/aa/78/8e/78bb4de3ece6d777e3a528f5d26a7d8e.html
Заголовок, (Title) документа по адресу, URL1:
Fluent interface - Wikipedia
Данный printscreen веб страницы (снимок веб страницы, скриншот веб страницы), визуально-программная копия документа расположенного по адресу URL1 и сохраненная в файл, имеет: квалифицированную, усовершенствованную (подтверждены: метки времени, валидность сертификата), открепленную ЭЦП (приложена к данному файлу), что может быть использовано для подтверждения содержания и факта существования документа в этот момент времени. Права на данный скриншот принадлежат администрации Ask3.ru, использование в качестве доказательства только с письменного разрешения правообладателя скриншота. Администрация Ask3.ru не несет ответственности за информацию размещенную на данном скриншоте. Права на прочие зарегистрированные элементы любого права, изображенные на снимках принадлежат их владельцам. Качество перевода предоставляется как есть. Любые претензии, иски не могут быть предъявлены. Если вы не согласны с любым пунктом перечисленным выше, вы не можете использовать данный сайт и информация размещенную на нем (сайте/странице), немедленно покиньте данный сайт. В случае нарушения любого пункта перечисленного выше, штраф 55! (Пятьдесят пять факториал, Денежную единицу (имеющую самостоятельную стоимость) можете выбрать самостоятельно, выплаичвается товарами в течение 7 дней с момента нарушения.)