Свободный интерфейс
В разработке программного обеспечения свободный интерфейс — это объектно-ориентированный 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
[ редактировать ]Существует множество примеров библиотек 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
[ редактировать ]В 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!
См. также
[ редактировать ]Ссылки
[ редактировать ]- ^ Jump up to: а б с Мартин Фаулер , « FluentInterface », 20 декабря 2005 г.
- ^ «Пакет Интерфейсов200.Пакер» . Оракул . Проверено 13 ноября 2019 г. .
- ^ Россум, Гвидо ван (17 октября 2003 г.). «[Python-Dev] возвращаемое значение sort()» . Проверено 1 февраля 2022 г.
Внешние ссылки
[ редактировать ]- Оригинальная запись Мартина Фаулера в бликах, в которой появился этот термин
- Пример Delphi написания XML с гибким интерфейсом
- Библиотека свободной проверки .NET, написанная на C#. Архивировано 23 декабря 2017 г. на Wayback Machine.
- Учебное пособие по созданию формальных API-интерфейсов Java с использованием нотации BNF.
- Свободные интерфейсы — это зло
- Разработка свободного API — это так здорово