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