Интерфе́йс (interface) — синтаксическая структура, определяющая отношение между объектами, которые разделяют определенное поведенческое множество и не связаны никак иначе. При проектировании классов, разработка интерфейса тождественна разработке спецификации (множества методов, который каждый класс, использующий интерфейс должен реализовывать).

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


Интерфейсы позволяют наладить множественное наследование объектов и в то же время решить проблему ромбовидного наследования. В языке C++ она решается через наследование классов с использованием ключевого слова virtual.

Описание и использование интерфейсов

Описание ООП-интерфейса, если отвлечься от деталей синтаксиса конкретных языков, состоит из двух частей: имени и методов интерфейса.

Разные языки и среды разработки имеют различные соглашения по оформлению кода, в соответствии с которыми имена интерфейсов могут формироваться по некоторым правилам, которые помогают отличать имя интерфейса от имён других элементов программы.
Например, в технологии COM и во всех поддерживающих её языках действует соглашение, следуя которому, имя интерфейса строится по шаблону «I<Имя>», то есть состоит из написанного с заглавной буквы осмысленного имени, которому предшествует прописная латинская буква I (IUnknown, IDispatch, IStringList и т. п.).
Методы интерфейса. В описании интерфейса определяются имена и сигнатуры входящих в него методов, то есть процедур или функций класса.


Использование интерфейсов возможно двумя способами:

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

Один класс может реализовать несколько интерфейсов одновременно.

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

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

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

С одной стороны, интерфейс — это «договор», который обязуется выполнить класс, реализующий его, с другой стороны, интерфейс — это тип данных, потому что его описание достаточно чётко определяет свойства объектов, чтобы наравне с классом типизировать переменные.

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

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

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


Интерфейсы и абстрактные классы

Интерфейс — это просто чистый абстрактный класс, то есть класс, в котором не определено ничего, кроме абстрактных методов.

Если язык программирования поддерживает множественное наследование и абстрактные методы (как, например, C++), то необходимости во введении в синтаксис языка отдельного понятия «интерфейс» не возникает. Данные сущности описываются с помощью абстрактных классов и наследуются классами для реализации абстрактных методов.

Поддержка множественного наследования в полном объёме достаточно сложна и вызывает множество проблем, как на уровне реализации языка, так и на уровне архитектуры приложений.

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


На уровне исполнения классическая схема множественного наследования вызывает дополнительный ряд неудобств:
* если объект может параллельно наследовать n классов, существует n независимых способов к нему обращаться, а значит должно существовать (n — 1) дополнительных указателей на него; с точки зрения автоматического управления памятью это будет означать, что возникают ссылки, указывающие в середину объекта;
* поддержка виртуальных вызовов подразумевает, что в объекте хранится ссылка на его виртуальную таблицу, а в случае множественного наследования n ссылок; активное использовании множественного наследования сильно увеличит объём памяти, занимаемый каждым объектом (экземпляром).

Использование схемы с интерфейсами (вместо множественного наследования) позволяет отбросить эти проблемы, если не считать вопроса о вызове интерфейсных методов (то есть виртуальных вызовов методов при множественном наследовании). Классическое решение состоит в том (например, в JVM для Java или CLR для C#), что интерфейсные методы вызываются менее эффективным способом, без помощи виртуальной таблицы: при каждом вызове сначала определяется конкретный класс объекта, а затем в нём ищется нужный метод.



Множественное наследование и реализация интерфейсов

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

Тем не менее, одна коллизия при множественном наследовании интерфейсов и при реализации нескольких интерфейсов одним классом всё-таки возможна. Она возникает, когда в двух или более интерфейсах, наследуемых новым интерфейсом или реализуемых классом, имеются методы с одинаковыми сигнатурами. Разработчики языков программирования вынуждены выбирать для таких случаев те или иные способы разрешения противоречий. Вариантов здесь несколько: запрет на реализацию, явное указание конкретного и реализация базового интерфейса или класса.

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

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

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




Интерфейсы в языках программирования



C++ поддерживает множественное наследование и абстрактные классы, поэтому отдельная синтаксическая конструкция для интерфейсов в этом языке не нужна. Интерфейсы определяются при помощи абстрактных классов, а реализация интерфейса производится путём наследования этих классов.

Пример определения интерфейса:

// interface.Openable.h
#ifndef INTERFACE_OPENABLE_HPP
#define INTERFACE_OPENABLE_HPP
// Класс интерфейса iOpenable. Определяет возможность открытия/закрытия чего-либо.
class iOpenable
{
public:
virtual ~iOpenable(){}

virtual void open()=0;
virtual void close()=0;
};
#endif

Интерфейс реализуется через наследование:

// class.Door.h
#include "interface.Openable.h"
#include <iostream>

class Door : public iOpenable
{
public:
Door(){std::cout << "Door object created" << std::endl;}
virtual ~Door(){}

//Конкретизация методов интерфейса iOpenable для класса Door
virtual void open(){std::cout << "Door opened" << std::endl;}
virtual void close(){std::cout << "Door closed" << std::endl;}

//Специфические для класса Door свойства и методы
std::string mMaterial;
std::string mColor;
//...
};


// class.Book.h
#include "interface.Openable.h"
#include <iostream>

class Book: public iOpenable
{
public:
Book(){std::cout << "Book object created" << std::endl;}
virtual ~Book(){}

//Конкретизация методов интерфейса iOpenable для класса Book
virtual void open(){std::cout << "Book opened" << std::endl;}
virtual void close(){std::cout << "Book closed" << std::endl;}

//Специфические для класса Book свойства и методы
std::string mTitle;
std::string mAuthor;
//...
};



// test.Openable.cpp
#include "interface.Openable.h"
#include "class.Door.h"
#include "class.Book.h"

//Функция открытия/закрытия любых разнородных объектов, в которых реализован интерфейс iOpenable
void openAndCloseSomething(iOpenable& smth)
{
smth.open();
smth.close();
}

int main()
{
Door myDoor;
Book myBook;

openAndCloseSomething(myDoor);
openAndCloseSomething(myBook);
system ("pause");
return 0;
}




Java

В Java интерфейсы изначально входят в язык, являясь неотъемлемой его частью.

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

Объявление интерфейсов очень похоже на упрощённое объявление классов.

Оно начинается с заголовка. Сначала указываются модификаторы. Интерфейс может быть объявлен как public, и тогда он будет доступен для общего использования, либо модификатор доступа может не указываться, в этом случае интерфейс доступен только для типов своего пакета.

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

Далее записывается ключевое слово interface и имя интерфейса.

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

Наследование интерфейсов действительно очень гибкое. Так, если есть два интерфейса, A и B, причем B наследуется от A, то новый интерфейс C может наследоваться от них обоих. Впрочем, понятно, что при наследовании от B, указание наследования от A является избыточным, так как все элементы этого интерфейса и так будут получены по наследству через интерфейс B.

Затем в фигурных скобках записывается тело интерфейса.


Пример объявления интерфейса:

public interface Drawable extends Colorable, Resizable
{
}


Тело интерфейса состоит из объявления элементов, то есть полей-констант и абстрактных методов.

Все поля интерфейса автоматически являются public final static, так что эти модификаторы указывать необязательно и даже нежелательно, чтобы не загромождать код.


Поскольку поля являются финальными, необходимо их сразу инициализировать.

public interface Directions
{
int RIGHT=1;
int LEFT=2;
int UP=3;
int DOWN=4;
}


Все методы интерфейса являются public abstract, и эти модификаторы также необязательны.

public interface Moveable {
void moveRight();
void moveLeft();
void moveUp();
void moveDown();
}


Реализация интерфейса

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

interface I
{
void interfaceMethod();
}

public class ImplementingInterface implements I
{
void interfaceMethod()
{
System.out.println("Этот метод реализован из интерфейса I");
}
}

public static void main(String[] args)
{
ImplementingInterface temp = new ImplementingInterface();
temp.interfaceMethod();
}

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



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

interface A
{
int getValue();
}

interface B
{
double getValue();
}

interface C
{
int getValue();
}

public class Correct implements A, C // класс правильно наследует методы с одинаковой сигнатурой
{
int getValue()
{
return 5;
}
}

class Wrong implements A, B // класс вызывает ошибку при компиляции
{
int getValue()
{
return 5;
}

double getValue()
{
return 5.5;
}
}



C#

В C# интерфейсы могут наследовать один или несколько других интерфейсов. Членами интерфейсов могут быть методы, свойства, события и индексаторы:

interface I1
{
void Method1();
}
interface I2
{
void Method2();
}

interface I : I1, I2
{
void Method();
int Count { get; }
event EventHandler SomeEvent;
string this[int index] { get; set; }
}
При реализации интерфейса класс должен реализовать как методы самого интерфейса, так и его базовых интерфейсов:

public class C : I
{
public void Method()
{
}

public int Count
{
get { throw new NotImplementedException(); }
}

public event EventHandler SomeEvent;

public string this[int index]
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}

public void Method1()
{
}

public void Method2()
{
}
}



Comments and questions

Publish comment or question

Copyright 2019 © ELTASK.COM
All rights reserved.