Зачем придумали интерфейсы?

90
26 февраля 2022, 06:40

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

interface IMovable
{
    void Move();
}

Реализация в классе:

// применение интерфейса в классе
class Person : IMovable
{
    public void Move()
    {
        Console.WriteLine("Человек идет");
    }
}
// применение интерфейса в структуре
struct Car : IMovable
{
    public void Move()
    {
        Console.WriteLine("Машина едет");
    }
}

Но если я просто удалю интерфейс и сделаю вот так

class Person
{
    public void Move()
    {
        Console.WriteLine("Человек идет");
    }
}

Код по прежнему будет работать.
Вот и вопрос зачем все это нужно?
Я просто не могу представить себе ситуацию где бы было необходимо использовать интерфейсы.
В C#8 добавили реализацию метода по умолчанию в интерфейсе тем самым сделав из интерфейса недо-класс.
Смотрю видео на ютубе и там все используют интерфейсы но не говорят зачем, просто как будто так и надо.
Не спроста же все их используют? Но без понимания зачем все это нужно у меня просто не получается найти смысл их применения и понять как работает чужой код когда в нем есть интерфейсы.
Единственный смысл в них вижу в том что не нужно писать документацию к классам, просто наследуем от интерфейса а дальше пусть уже другой программист лезет в код и смотрит чего там и как.
Ни в коем случае не хочу сказать что я тут самый умный а все остальные ошибались, просто хочу разобраться почему придумали интерфейсы если и без них все хорошо работает?
Извините что так сумбурно написал, сам на эмоциях, неделю уже читаю статьи по теме и ни как не могу осилить.

Answer 1

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

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

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

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

В такой ситуации вам поможет интерфейс. Например, у вас есть класс, который вам надо сохранить.

public class MyGameState
{
    //... my game data
}

Напишем для него интерфейс сохранения

public interface IGameStore
{
    void SaveGame(MyGameState state);
    MyGameState Load();
}

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

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

Но тут вы можете возразить мне и сказать - что вы с таким же успехом можете определить класс для сохранения, например

public absract class GameStore
{
    public abstract void SaveGame(MyGameState state);
    public abstract MyGameState Load();
}

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

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

public absract class GameStore
{   
    protected string PlayerName{get; protected set;}
    public GameStore(string playerName)
    {
        PlayerName = playerName;
    }
    public abstract void SaveGame(MyGameState state);
    public abstract MyGameState Load();
}

Теперь посмотрите что произошло. Мы не только заставляем классы для сохранения игры наследоваться от нашего абстрактного класса, но ещё и накладываем на них определенные огреничения. Интересны ли эти ограничения остальной части игры? Думаю, нет. В таком случае, зачем остальной части игры знать об этих огреничениях? Нет никаких причин для этого. Таким образом, Используя интерфейс, вы декларируете желаемое поведение. Используя класс - вы декларируете детали реализации. То есть, когда у вас был интерфейс в игре, вы декларировали, что "не важно что тут будет за объект, не важно как он будет написан, но мне надо чтобы он мог сохранять и загружать состояние игры" - то есть вам было важно, что объект может делать, при этом не важно, как он это делает. В этом суть инкапсуляции. Когда же вы используете класс, особенно если класс содержит какие либо посторонние детали, вы уже покажываете, что вам важно не только что объект может делать, но и также кем объект является и какие у объекта есть детали реализации.

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

  1. Если вы хотите держать свои модули слабо связанными (а вы должны хотеть), используйте для связи интерфейсы
  2. При использовании интерфейов для связи, на них ложится бремя совместимости - интерфейсы должны меняться как можно реже
  3. Так как интерфейсы должны меняться как можно реже, написание интерфейса становится задачай более ответственной, чем написание класса. Класс, закрытый интерфейсом, всегда можно переписать. Интерфейс же, используемый во многих компонентах, переписать возможно не всегда, при этом есть большой риск потери обратной совместимости с уже написанным до этого кодом.
Answer 2

Классы выражают основную иерархию и часто определяют внутреннее поведение наследников.

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

Например, может быть большая иерархия живых организмов – царства, семейства, виды из биологии. Это всё много разных абстрактных и финальных классов. При этом, можно выделить дополнительные иерархии – например, способность передвигаться (IMovable), которая может наследоваться в способность ходить (IWalking), летать (IFlying), плавать (ISwimming). Что немаловажно, те же интерфейсы могут быть реализованы классами из совсем другой иерархии вроде техники – автомобили, танки, самолёты, корабли и лодки.

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

Например, ISwimming может предоставлять абстрактный метод void swim(float speed); и конкретные методы вроде void swimSlowly() { self.swim(0.3); }.

Пример не реалистичный, но на деле всё то же самое, только вместо всем понятных животных и техники чаще абстрактные структуры данных и хитроумные манипуляции с ними. Из наиболее популярных это возможности сериализации (ISerializable), отображения с форматированием, конвертирования в какие-то форматы и т.п. И могут быть большие функции или даже библиотеки, которые будут принимать объекты таких интерфейсов для передачи по сети, сохранения в файл или красивого отображения. А до основного поведения класса, будь то бинарное дерево, граф вычислений, картинка или персонаж игры, им дела нет.

Почему придумали интерфейсы если и без них все хорошо работает?

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

READ ALSO
Сравнение 2-х текстовых файлов

Сравнение 2-х текстовых файлов

Сравниваю 1 и 2 текстовый файл, если в 1 нету тех строк, которые во 2 текстовом файле, то он создает третий текстовый файл и записывает в негоКак...

133
Размер картинок для смарфонов

Размер картинок для смарфонов

У меня на сайте есть галерея картинокКартинки в довольно хорошем качестве, где-то 2000х1500

173
Viber не видит Open Graph разметку

Viber не видит Open Graph разметку

Есть сайт, когда ссылку на страницу сайта кидаю, например, в skype, то с сайта подтягивается картинка, тайтл и описание, которые находятся в мета-тегах...

144