Недавно получил письмо с вопросом о том, зачем нужны интерфейсы, если это всего лишь описание функций и там нет реализации кода. Наследование на много лучше, потому что можно создать объекты с нужной реализацией и просто наследовать их.
Такой вопрос показывает, что автор письма просто не понимает, для чего нужны интерфейсы. Это нормально, потому что их смысл не в том, как и кто реализовывает действие, а в том, как можно вызвать это действие.
Интерфейсы в программировании - это тот случай, когда можно долго и нудно рассказывать теорию о том, как они работают, но читатель поймет саму суть только тогда, когда увидит результат своими глазами, то есть на практике. И вот поэтому я решил показать несколько примеров того, как и где можно получить выгоду от интерфейсов.
Но для начала все же совсем чуть-чуть теории и слов. Класс - это описание и реализация объекта. Объект - это экземпляр класса, то есть объект создается на основе описания, которое вы пишите в классе.
Класс может выполнять какие-то действия и у него могут быть свойства - это все, что вы объявляете с ключевым словом 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, а когда я пишу на нем, то количество небольших косяков больше, чем обычно. Я это знаю, исправлять не собираюсь, у меня нет времени даже перечитывать статью, поэтому не нужно писать письма с просьбой исправить какую-то орфографическую ошибку. Все остальные ошибки можно присылать.
Внимание!!! Если ты копируешь эту статью себе на сайт, то оставляй ссылку непосредственно на эту страницу. Спасибо за понимание
Лайк за статью, спустя 9 лет вы помогли с ее помощью понять для чего вообще эти ваши интерфейсы есть в природе.
Михаил, привет.
Можешь поправить начало этой статьи, может опечатка или часть текста пропала.
"Недавно получил само с вопросом о том, зачем нужны интерфейсы..."
Поправил опечатку, там должно было быть слово "письмо"