Menu
CoddyTech

Интерфейс vs абстрактный класс

Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy. Урок 26 из 70.

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

Интерфейс определяет, что должен делать класс, не задавая реализацию. Это чистый контракт:

public interface IFlyable
{
    void Fly();
}

public class Bird : IFlyable
{
    public void Fly()
    {
        Console.WriteLine("Flapping wings");
    }
}

Абстрактный класс может предоставлять как контракт, так и общую реализацию. Это частичный шаблон:

public abstract class Vehicle
{
    public string Brand { get; set; }
    
    public void StartEngine()
    {
        Console.WriteLine("Engine started");
    }
    
    public abstract void Drive();  // Должен быть реализован
}

public class Car : Vehicle
{
    public override void Drive()
    {
        Console.WriteLine("Driving on roads");
    }
}

Ключевые различия сводятся к правилам наследования и назначению.

Класс может реализовать несколько интерфейсов, но наследовать только один абстрактный класс.

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

public class Airplane : Vehicle, IFlyable
{
    public override void Drive()
    {
        Console.WriteLine("Taxiing to the runway");
    }

    public void Fly()
    {
        Console.WriteLine("Flying above the clouds");
    }
}

Используй интерфейс, когда несвязанным классам нужно иметь общее поведение (например, IFlyable для птиц и самолётов). Используй абстрактный класс, когда родственные классы имеют общий код и состояние (например, у транспортных средств общий механизм запуска двигателя).

Абстрактные классы могут иметь конструкторы, поля и модификаторы доступа у членов класса. Традиционно интерфейсы не могли этого, хотя современные версии C# допускают реализации по умолчанию.

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

challenge icon

Задание

Легко

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

Вы распределите код по четырём файлам:

  • IPlayable.cs: Определите интерфейс с именем IPlayable в пространстве имён Media. Этот интерфейс задаёт контракт для всего, что может воспроизводить медиафайлы, — будь то телефон, телевизор или специальный музыкальный плеер. В нём следует объявить один метод Play(), который возвращает строку.
  • MediaFile.cs: Определите абстрактный класс с именем MediaFile в пространстве имён Media. Он представляет общую основу для всех медиафайлов. В нём должны быть свойство Title (строка) и свойство Duration (целое число, обозначающее длительность в секундах). Добавьте конструктор, который устанавливает оба значения. Добавьте конкретный метод с именем GetInfo(), который возвращает "{Title} ({Duration}s)". Также объявите абстрактный метод GetType(), который возвращает строку, — каждый тип медиафайла будет определять, что это за файл.
  • AudioFile.cs: Определите класс AudioFile в пространстве имён Media, который наследуется от MediaFile и реализует IPlayable. Добавьте свойство Artist (строка). Конструктор должен принимать название, длительность и имя исполнителя. Переопределите GetType(), чтобы он возвращал "Audio". Реализуйте Play(), чтобы он возвращал "Playing audio: {Title} by {Artist}".
  • Program.cs: В главном файле создайте AudioFile, используя входные значения. Продемонстрируйте обе стороны класса: вызовите GetInfo() и GetType() (унаследованные от абстрактного класса), а затем вызовите Play() (из интерфейса). Сохраните аудиофайл в переменной типа IPlayable и снова вызовите Play(), чтобы показать полиморфизм интерфейса.

Вы получите три входных значения:

  • Название песни
  • Длительность в секундах
  • Имя исполнителя

Выведите результат в следующем формате:

Info: {GetInfo() result}
Type: {GetType() result}
Direct: {Play() result}
Via Interface: {Play() result from IPlayable variable}

Например, если входные значения — Bohemian Rhapsody, 354 и Queen, результат должен быть таким:

Info: Bohemian Rhapsody (354s)
Type: Audio
Direct: Playing audio: Bohemian Rhapsody by Queen
Via Interface: Playing audio: Bohemian Rhapsody by Queen

Обратите внимание, как AudioFile использует преимущества обоих подходов: он наследует общую реализацию (GetInfo()) от абстрактного класса и одновременно выполняет контракт IPlayable. Абстрактный класс позволяет повторно использовать код для связанных типов медиафайлов, а интерфейс позволяет любому несвязанному классу быть «воспроизводимым», не входя в общую иерархию наследования.

Попробуйте сами

using System;
using Media;

class Program
{
    public static void Main(string[] args)
    {
        // Считать входные значения
        string title = Console.ReadLine();
        int duration = Convert.ToInt32(Console.ReadLine());
        string artist = Console.ReadLine();
        
        // TODO: Создать AudioFile, используя входные значения
        
        // TODO: Вывести Info:, затем результат GetInfo()
        
        // TODO: Вывести Type:, затем результат GetType()
        
        // TODO: Вывести Direct:, затем результат Play()
        
        // TODO: Сохранить аудиофайл в переменной типа IPlayable
        
        // TODO: Вывести Via Interface:, затем результат Play() для переменной типа IPlayable
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор C#