Education

Как-то раз я написал такой твит:

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

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

Думаю, что здесь будет не лишним привести определение. 

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

Другими словами, уменьшение связанности— это процесс предельно возможного сведения зависимостей до абстракций. Если вы хотите писать хороший, чистый код, то постараетесь привязываться только к абстракциям. Такими абстракциями как раз и являются интерфейсы. Как я уже упомянул в приведённом выше твите, это очень важно для написания хорошего, легко обслуживаемого кода. 

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

Что такое интерфейс?

Большинство современных языков вроде C#, TypeScript или Java поддерживают интерфейсы. Сами же интерфейсы достаточно сложно описать, тем не менее я попытаюсь. Словарь даёт следующее определение:

Интерфейс — общая точка взаимодействия двух систем, субъектов, организаций и пр. 

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

Звучит это всё заумно, но так оно и есть. 

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

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

Интерфейсы повсюду

В нашей повседневной жизни мы встречаем интерфейсы везде. Даже розетки — это интерфейс. Например, в США они имеют два вертикальных прямоугольных паза для фазы и нейтрального контакта, а ниже круглое отверстие для заземления. 

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

Можно подключить фен или монитор — интерфейсу всё равно. В итоге эти предметы можно охарактеризовать как “подключаемые”.

Основная причина использования интерфейсов, как уже было сказано, — уменьшение связанности. Если вы программируете через интерфейсы, то можете писать код так, чтобы он никогда не связывался (не соединялся) ни с чем, кроме интерфейса. Чем менее связанным будет код, тем менее вероятно, что изменение в одной его части отразится на другой. 

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

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

При этом стоит отметить, что такая простота может усложнить понимание. Когда я впервые столкнулся с нотацией интерфейсов, то подумал: “Хмм… Зачем вообще нужны эти интерфейсы?”

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

Простой пример

Предположим, у нас есть декларация интерфейса в TypeScript:

interface IName 
{   
firstName: string;   
lastName: string; 
}

Все примеры в статье будут на TypeScript.

Интерфейсы состоят из имени (в данном случае IName) и объявления методов/свойств.

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

Интерфейс не содержит “реального” кода, реализующего функциональность. Он не может объявлять поля, переменные и константы. Он также не может определять область видимости (приватная, публичная и пр.) Он только объявляет возможности. Таким образом, каждый член интерфейса по сути является публичным.

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

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

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

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

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

Суть в том, что не имеет значения, чем являются реализующие объекты и что они делают — они просто производят имя, когда рассматриваются как IName.

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

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

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

class Name implements IName {
private _lastName: string;
private _firstName: string;

public constructor(public aFirstName: string, public aLastName: string) {
 this._firstName = aFirstName;
 this._lastName = aLastName;   
}

get firstName(): string { 
 return this._firstName;
}   
 
get lastName(): string {
 return this._lastName; 
}  
 
get fullName(): string {
 return this._firstName + ' ' + this._lastName;   
}
 }

Обратите внимание, что:

  • Класс Name объявлен как реализующий интерфейс IName.
  • Мы реализовали интерфейс немного в обход. Наш класс объявляет конструктор public, получающий имя и фамилию в виде параметров. Затем он хранит эту информацию в переменных private, которые представлены как публичные свойства только для чтения. В результате класс неизменяем.
  • При этом класс не только реализует интерфейс, но и предоставляет свойство fullName, сочетающее в себе значения имени и фамилии. Это показывает, что класс может делать что угодно помимо реализации интерфейса. Тем не менее мы не можем использовать эту функциональность, когда объявляем класс как переменную интерфейса, поскольку он не является его частью.

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

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

var person: IName;

person = new Name('Harvey', 'Wallbanger');
console.log('Hello', person.firstName + '!');

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

Заключение

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

Читайте также:


Перевод статьи Nick Hodges: Code Against Interfaces, Not Implementations