Об ИТ из Канады

Блог Михаила Флёнова - программист, блогер, автор нескольких скандальных книг какими-то глазами...
Когда использовать интерфейсы в программировании - Статья : блог Михаила Флёнова

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

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

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

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

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

Класс может выполнять какие-то действия и у него могут быть свойства - это все, что вы объявляете с ключевым словом 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, а когда я пишу на нем, то количество небольших косяков больше, чем обычно. Я это знаю, исправлять не собираюсь, у меня нет времени даже перечитывать статью, поэтому не нужно писать письма с просьбой исправить какую-то орфографическую ошибку. Все остальные ошибки можно присылать. 


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

О блоге

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

Внимание!

А ты уже читал мою последнюю книгу о больших сайтах и приложениях? Узнай, что это такое здесь

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

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

Пишите мне