Наверняка у многих из вас слово "интерфейс" ассоциируется с внешним видом любой программы, то есть кнопочками, виджетами, иконками и прочим ее оформлением. Да, несомненно графический пользовательский интерфейс является одним из значений этого понятия, но существует и масса других!
Хотите узнать больше? В общем случае под словом интерфейс понимают правила и рамки взаимодействия двух произвольных объектов. В рамках компьютерной терминологии такими объектами обычно выступают люди, оборудование, программное обеспечение или его компоненты, но этот термин применим и далеко за ее пределами.
Вернувшись к примеру из первого абзаца мы теперь можем вполне аргументированно объяснить почему GUI так часто приравнивают к слову интерфейс: он просто является частным случаем интерфейса между приложением и его пользователем. Можно было бы привести еще массу примеров различных интерфейсов, скажем сокет в качестве интерфейса между процессором и материнской платой, но целью написания этого поста было вовсе не это.
Уже догадались? Да, это я так неспеша плавно подводил разговор к объектно-ориентированному программированию. Термин интерфейс широко применяется и в нем. Как не трудно предположить, в роли объектов в этом случае выступают как сами классы, так и их экземпляры (которые, впрочем, тоже принято называть словом объект).
В общем случае интерфейсом класса выступает совокупность его public методов и переменных, то есть доступных для обращения из других частей приложения. Этот факт вполне логичен - именно благодаря им и осуществляется взаимодействие класса (или его объекта) с "внешним миром". Но не все так просто, особенно с точки зрения шаблонов проектирования, немаловажную роль в взаимодействии классов и объектов играет абстракция. Хочется обратить внимание, что формально имеется ввиду даже не сами методы, а их заголовки, то есть название, набор получаемых переменных и тип возвращаемого значения (этот набор данных принято также принято называть интерфейсом методов или функций), само тело метода (реализация) в данном случае не важно.
Иными словами, если один класс (будем называть его клиент) взаимодействует с каким-либо другим объектом, то по большому счету он абсолютно не обязан знать какого класса этот объект является экземпляром (может конечно, но это совсем не обязательно). Единственное, что интересует класс-клиент, это интерфейс объекта, с которым он взаимодействует, этой информации вполне достаточно для полноценной совместной работы.
Сразу напрашивается вполне резонный вопрос: а как же тогда клиент может быть уверен, что в классе, с которым он работает, какой-либо конкретный интерфейс реализован? Допустим ему нужен во-о-о-он тот метод, а как же узнать доступен ли он и получит ли клиент в ответ данные нужного типа? Ответ на этот вопрос реализован в каждом языке программирования по-разному: где-то существует специальные ключевые слова для обозначения интерфейсов и классов, их реализующих, где-то это ненавязчиво реализуется средствами наследования и полиморфизма на более концептуальном уровне.
Самым наглядным языком программирования для демонстрации описания интерфейсов я считаю Java (хотя можно было бы выбрать и C#, PHP или практически любой другой по вкусу). В теории все просто:
- Ключевое слово
interface
обозначает описание интерфейса; - За ним следует название конкретного интерфейса, которое впоследствии можно будет использовать в коде при его упоминании (некоторые программисты на правах традиции начинают названия интерфейсов с заглавной буквы I, мне в свое время даже пытались объяснить зачем так надо делать, но аргументы не показались мне достаточно весомыми);
- Далее идет тело интерфейса, в котором перечисляются все заголовки методов, которые должны быть в классе, реализующем данный интерфейс (никакой реализации!);
- Впоследствии приписав к заголовку любого класса ключевое слово
implements
с последующим указанием названия интерфейса, можно обязать этот класс реализовать указанные в описания интерфейса методы. Существует небольшое исключение для абстрактных классов (то есть классов,для которых не может быть создан объект, обозначаются ключевым словомabstract
), они могут и не реализовать все методы интерфейса, но тогда эта обязанность будет переложена на их наследников.
В данной ситуации клиент, работающий с каким-либо произвольным объектом может просто-напросто проверить, реализован ли в нем заранее определенный интерфейс, что даст ему гарантию, что он может смело обращаться к необходимому набору методов.
Небольшое примечание: сами интерфейсы и методы в их теле по-умолчанию обладают свойствами abstract
и public
, так что повторно указывать эти ключевые слова не нужно.
На практике же это выглядит это примерно следующим образом:
// описание интерфейса
interface Renderable
{
// обязуем реализовать метод draw
public void draw();
}
// конкретная реализация интерфейса
class SomeText implements Renderable
{
string text;
public SomeText(string str)
{
this.text=str;
}
public void draw()
{
// вынуждены подчиниться и реализовать
System.out.println(this.text);
}
}
// класс-клиент
class Render
{
public Render(Renderable obj)
{
// можно быть уверенным, что
// метод draw реализован
obj.draw();
/*
в качестве альтернативы можно было бы написать как-то так:
if(obj instanceof Renderable)obj.draw();
то есть проверить реализован ли интерфейс
вместо использования его названия в роли типа данных
*/
}
В данном примере ситуация тривиальна: класс-клиент Render
умеет лишь визуализировать классы, которые он получает в конструктор, вызывая у них метод draw
. Для обеспечения такой возможности описан интерфейс Renderable
, который реализуется в классе SomeText
. Хоть класс Render
ничего и не знает о том, какой именно класс ему подсунут, благодаря интерфейсу он сможет вывести на экран любой объект, корректно реализующий наш интерфейс, в том числе и SomeText
.
Как я уже упоминал: альтернативой такому подходу является использование полиморфизма и наследования. Такой подход более распространен в других языках программирования, например C++, но пример я приведу все равно на Java, основываясь на предыдущем примере, чтобы читателям было проще сравнивать.
В теории такой подход еще проще: создается абстрактный класс, хоть как-то реализующий наш интерфейс (теоретически реализация может быть и пустой, просто в виде метода-заглушки), а на стороне клиента достаточно лишь просто принимать только наследников этого абстрактного класса. В нашем примере достаточно лишь изменить пару ключевых слов и все:
// теперь используем абстрактный класс
abstract class Renderable
{
// реализуем метод draw
public void draw()
{
System.out.println("Вывод на экран недоступен!");
}
}
// реализация интерфейса (на этот раз неформального)
class SomeText extends Renderable
{
// на этот раз используем extends (наследование)
// вместо implements
string text;
public SomeText(string str)
{
this.text=str;
}
public void draw()
{
// переопределяем метод draw
// но могли этого и не делать, тогда
// использовался бы метод из Renderable
System.out.println(this.text);
}
}
// класс-клиент
class Render
{
public Render(Renderable obj)
{
// можно быть уверенным, что
// метод draw реализован
obj.draw();
/*
на этот раз так как в крайнем случае
в крайнем случае вызовется хотябы
метод из класса Renderable
*/
}
Минимальные изменения - суть та же. Сразу хочу отметить, что этот процесс так прост только в Java, в других языках программирования понадобилось бы использование дополнительных модификаторов для метода draw
(например в C#: virtual
или abstract
в классе-потомке и override
в классе-наследнике, это необходимо для обеспечения возможности их переопределения).
На этом позвольте завершить данное повествование, очень надеюсь, что мне удалось изложить суть максимально прозрачно. Эта тема будет активно подниматься в дальнейших статьях по ООП, так что очень надеюсь, что она стала для Вас элементарной и очевидной. По традиции напоминаю, что не пропустить публикацию новых постов можно подписавшись на RSS.