Когда использовать интерфейсы в программировании

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

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

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

Но для начала все же совсем чуть-чуть теории и слов. Класс - это описание и реализация объекта. Объект - это экземпляр класса, то есть объект создается на основе описания, которое вы пишите в классе.  

Класс может выполнять какие-то действия и у него могут быть свойства - это все, что вы объявляете с ключевым словом static (в C подобных языках). Такие методы вызываются без дополнительной инициализации, а свойства существуют в единственном экземпляре.  

Объекты - это экземпляры классов. Вы можете создать два объекта одного класса и они будут обладать одним и тем же набором возможностей (методов) и свойств, только свойства у каждого свои. Изменяя значения свойства одного объекта, вы не трогаете другой. Свойства и методы объектов - это все, что вы объявляете не статичным.  

Если вы это знаете и понимаете разницу, то это просто отлично.  

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

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

Интерфейсы для плагинов 

Самый простой пример - plug-in. Помню, до появления интерфейсов в Delphi приходилось извращаться и одно из таких извращений я показывал в первой книге Delphi глазами хакера. Тогда Delphi не поддерживал интерфейсов, когда я писал тот пример.  

Что такое plug-in? Это какой-то объект, который выполняет отделенные действия. Основной программе (основному коду) плевать, как выполняется действие и возможно, что даже по барабану, что там происходит, наша задача всего лишь предоставить возможность плагину зарегистрироваться у нас, и нам нужно знать, как можно запустить плагин на выполнение. 

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

interface IInterface 

 void getTest(); 

Сразу же хочу извинится за возможные отпечатки и ошибки в коде. Я пишу эту заметку на iPad в OneNote, у которого нет компиляторе и проверки на ошибки в C# коде.  

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

Теперь мы можем объявить класс, который будет описывать дом и этот дом может реализовывать наш протокол: 

class Home : Interface  

 public void GetTest() 

 { 

  // вот тут находится реализация интерфейсы 

 } 

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

Теперь мы можем создать интерфейс класса Home: 

IInterface test = new Home(); 

Так как дом реализует наш протокол, то такая операция абсолютно легальна.  

Пока никакой выгоды особо не видно, но теперь мы подошли к тому моменту, когда когда уже можно увидеть выгоду. Дело в том, что в C# двойное расследование запрещено. А что, если наш plugin должен наследоваться от какого-то класса? Если вы хотите реализовать расширения в виде абстрактного базового класса, то люди, которые будут писать расширения не смогут объявить класс, который будет наследовать ваш класс и класс, который им нужен. Нужно будет использовать извращения, которые не стоят выделки.  

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

Посмотрим на полноценный код возможного примера:  

using System; // классика 

using System.Collections.Generic; // нам понадобится List 

// возможно я здесь забыл что-то еще подключить и код не скомпилируется 

// ну ничего, тем, кто любит искать ошибки, будет чем заняться  

 

namespace OurApplication 

    // объявляем интерфейс 

    interface IInterface 

    { 

        void getTest(); 

    } 

 

    // объявляем дом 

    class Home : IInterface 

    { 

        public void getTest() 

        { 

        } 

    } 

 

    // еще один класс утка, который реализует интерфейс 

    class Duck : IInterface 

    { 

        public void getTest() 

        { 

        } 

    } 

 

    // это началась наша программа 

    class Program 

    { 

        static void Main(string[] args) 

        { 

           // создаем список расширений 

            Listtests = new List(); 

 

            // добавляем в него объекты 

            tests.Add(new Home()); 

            tests.Add(new Duck()); 

 

           // запускаем каждый объект на выполнение 

            foreach (IInterface test in tests) 

                test.getTest(); 

        } 

    } 

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

В своей программе мы можем создать список из интерфейсов:  

Listtests = new List(); 

Это список, который состоит из объектов любого класса, но все они реализуют интерфейс IInterface.  

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

Абстракция 

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

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

interface IFtpInterface  

 bool UploadFile(string fileName); 

 

class FtpClient : IFtpInterface 

 public bool UploadFile(string fileName); 

 

class program  

 static void Main(string[] args) { 

    // тут выполняем какие-то действия и сохраняем их в файл 

    // этот файл мы хотим загрузить на сервер 

    IFtpInterface client = new FtpClient();  

    client.UploadFile("c:\file.txt"); 

 } 

В данном случае у нас только один метод у FtpFlient, поэтому трудно представить себе выгоду, но представьте себе, что класса десятки методов. Что если вы решили не поддерживать самостоятельно код FTP клиента, а решили перейти на стороннюю разработку? У этой сторонней разработки могут быть другие методы и вот тут вы попадаете в полную задницу, если используйте объекты напрямую.  

При использовании интерфейсов, вам достаточно всего лишь реализовать класс, который реализует IFtpInterface и поменять одну строку инициализации. Все магическим образом заработает.  

Я не использую интерфейсы везде, где попало, но стараюсь их использовать там, где работаю со сторонним кодом. Если бы в Delphi работа с базами данных была выполнена через интерфейсы, то переход с BDE на ADO.NET или DBExpress был бы простым. Но так как этого нет, я бы на вашем месте обращался к базе через интерфейсы.  

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

Тестирование кода 

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

- Мы знаем, что компонент FtpClient работает и для его тестирования у с могут быть отдельные тесты.  

- Юнит тесты должны быть максимально простыми, и проверять работоспособность чего-то одного, а не всего подряд.  

- Тестировать метод, который загружает что-то на сервер, не очень быстро (зависит от размера файла и скорости доступа к FTP) и неудобно, потому что после выполнения метода придется его скачать с FTP и проверить содержимое.  

Все это решается использованием интерфейсов.  

interface IFtpInterface { 

 bool UploadFile(string fileName); 

 }  

 

class FtpClient : IFtpInterface { 

 public bool UploadFile(string fileName) { 

  Здесь мы загружаем файл на FTP сервер  

 }  

 

class UnitTestFtpClient : IFtpInterface { 

  public bool UploadFile(string fileName) { 

     Не загружаем никуда файлы, а просто сохраняем где-нибудь 

     Чтобы UnitTest мог проверить результат 

  } 

 

class program { 

 static void boo(IFtpInterface client) { 

 // тут выполняем какие-то действия и сохраняем их в файл 

 // этот файл мы хотим загрузить на сервер 

 

 client.UploadFile("c:\file.txt"); 

 }  

Так мы будем вызывать метод в реальности: 

boo(new FtpClient());  

А вот так в Unit тестах.  

boo(new UnitTestFtpClient());  

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

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

Неужели в каждом классе нужно писать свою реализацию метода UploadFile? Это же в каждом классе будет куча одинакового кода инициализации FTP соединения и передачи данных.  

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

class UnitTestFtpClient : IFtpInterface { 

  public bool UploadFile(string fileName) { 

   (new RealFtpClient()).UploadFile(fileName); 

  } 

}  

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

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



Внимание!!! Если ты копируешь эту статью себе на сайт, то оставляй ссылку непосредственно на эту страницу. Спасибо за понимание

Комментарии

Максим123

04 Января 2022

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


Андрей Ше

12 Января 2024

Михаил, привет.
Можешь поправить начало этой статьи, может опечатка или часть текста пропала.
"Недавно получил само с вопросом о том, зачем нужны интерфейсы..."


Михаил Фленов

12 Января 2024

Поправил опечатку, там должно было быть слово "письмо"


Добавить Комментарий

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

Без проблем вступаю в неразборчивые разговоры по e-mail. Стараюсь отвечать на письма всех читателей вне зависимости от страны проживания, вероисповедания, на русском или английском языке.

Пишите мне