Включите исполнение JavaScript в браузере, чтобы запустить приложение.

Абстрактные классы и интерфейсы в ООП: что такое абстракции в объектно-ориентированном программировании

Поговорим об абстракциях в ООП, используя наглядные примеры. Расскажем простыми словами, что такое абстрактные классы и интерфейсы в объектно-ориентированном программировании, как с ними работать и когда лучше использовать абстрактный класс, а когда — интерфейс.
  1. Абстрактные классы и интерфейсы в ООП: что такое абстракции в объектно-ориентированном программировании
  2. Что такое абстракция в ООП
  3. Абстрактные классы
  4. Интерфейсы
  5. Отличия абстрактного класса и интерфейса
  6. Когда использовать абстрактный класс, а когда — интерфейс
  7. Заключение

Полиморфизм, наследование и инкапсуляция — три кита, на которых строится объектно-ориентированное программирование. Разбираемся, как устроены классы, что они собой представляют, чем отличаются от интерфейсов, когда и что использовать при разработке.

Что такое абстракция в ООП

Абстрагирование в жизни — это фокус на тех свойствах системы, которые важны в текущий момент времени. Абстракция в ООП — использование только определения характеристик объекта без детальной реализации. У объекта есть набор методов, но мы не предоставляем их конкретную логику: ее реализуют потомки.

Абстракция в ООП скрывает от разработчика реализацию. Мы создаем класс и говорим: «Сделай методы, которые будут реализованы в дочерних классах (потомках)». Классу A наследует класс B. В последнем мы указываем abstract и прописываем, что метод должен быть реализован в дочернем классе. Дочерний класс может отнаследоваться и реализовать указанный метод «по своим правилам». При этом наследование возможно от одного класса: у потомка может быть только один родитель (исключая ЯП, в которых допускается множественное наследование).

При этом базовый abstract может содержать стопку уже реализованных методов. Это значит, в нем же могут быть прописаны и реализованные дефолтные методы, которые автоматически «переедут» в потомка (за исключением приватных private). Такая особенность языка программирования значительно оптимизирует код: нам не нужно переписывать один и тот же код методов в потомках. Достаточно использовать extend, чтобы перетянуть методы.

Абстрактные классы

Разберем, как работают абстрактные классы в Java. 

К примеру, у нас есть класс «кошачьи». У всех кошек есть общие признаки и функции: все они млекопитающие, ходят на четырех лапах и виляют хвостом. Но способы добычи еды, т.е. реализация условного метода findEat(), могут быть разными: 

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

В таком случае мы можем объявить общий для всех кошачьих class Cat с методом findEat():

class Cat {

findEat()

}

От родительского класса будут наследоваться дочерние class HomeCat, class CatFisherMan, class Tiger. Дочерние классы (потомки) перенимают от родителя характеристики и методы, но получают возможность изменять реализацию. 

Объявляем родительский class с характеристиками (=методами), которые будут наследовать потомки:

class Cat {

    public void walkOnFourLegs() {

        System.out.println("Я хожу на четырех лапах");

    }

    public void wagTail() {

        System.out.println("Я умею вилять хвостом");

    }

    public abstract void findEat();

}

Создаем потомка, который унаследует родительские свойства и реализует объявленный в нем метод findEat(), но по-своему:

class HomeCat extends Cat {

    public void findEat() {

        System.out.println("Я домашняя кошка, поэтому сижу и жду, пока меня покормит хозяин");

    }

}

При этом наш потомок HomeCat унаследует от родителя четыре лапы walkOnFourLegs() и способность вилять хвостом wagTail(). Разработчик не увидит эти методы в коде class HomeCat, но они есть. 

В этом можно убедиться, если видоизменить код и попытаться вызвать у HomeCat метод wagTail(), которого нет в коде.

abstract class Cat {

    public void walkOnFourLegs() {

        System.out.println("Я хожу на четырех лапах");

    }

    public void wagTail() {

        System.out.println("Я умею вилять хвостом");

    }

       public abstract void findEat();

}

class HomeCat extends Cat {

    public void findEat() {

        System.out.println("Я домашняя кошка, поэтому сижу и жду, пока меня покормит хозяин");

    }

}

public class MyClass {

    public static void main(String args[]) {

     HomeCat cat = new HomeCat();

     cat.wagTail();

    }

}

В public class MyClass мы создаем экземпляр класса HomeCat, обращаемся к нему и вызываем метод, которого не видно в коде, но который существует, поскольку предоставлен родителем. Результат вызова cat.wagTail() — надпись «Я умею вилять хвостом» в консоли. 

Казалось, что наш class HomeCat умеет только ждать еду от хозяина. А он, оказывается, унаследовал от родителя способность вилять хвостом (и ходить на четырех лапах, к слову, тоже).

https://gv02-blog-obs01.obs.ru-moscow-1.hc.sbercloud.ru/image17e1d2946dcee9168b078f3adb24a1ee6e1c6e42.jpeg

Если упростить, то абстрактные классы позволяют нам создавать потомков, похожих на родителей. Например, родительский class — «Кошачьи», а его потомки — «Домашний кот», «Дикий кот», «Кот-рыболов».

Как происходит наследование
Как происходит наследование

При этом наследование может быть более сложным. У классов все почти как у людей: иногда есть не только родитель, но дедушка, прадедушка и т.д. В таком случае цепочка будет выглядеть, например, так:

child <— abstract parent1 <— abstract parent 2

Потомок будет наследовать то, что ему передали и «дедушка», и «отец». При этом мы можем указать, что родитель не передает ребенку часть методов. Для этого используется private. Свойства и методы, обозначенные так, наследоваться не будут. Родитель сможет передать потомку только public и protected.

Interface же позволяет имплементировать методы разных интерфейсов и создать кота-«франкенштейна». 

Например, у interface Bird наш класс возьмет метод «летать», у interface Human — «говорить», у interface Fish — «дышать под водой».

Как происходит имплементация интерфейса
Как происходит имплементация интерфейса

Интерфейсы

Преимущество интерфейсов — в том, что один класс может получить и реализовать методы нескольких интерфейсов (как в кейсе выше, с котом-франкенштейном). 

Интерфейс на Java может выглядеть таким образом:

interface Drawable {

    void draw(); 

    void resize(int percentage); 

}

В void draw() мы указали метод для рисования, а в void resize — задали метод для изменения размера. Теперь любые классы смогут «подписать контракт» и использовать методы.

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

Как работают контракты в интерфейсе? Например, в Interface мы можем прописать метод «Ходить на работу к 8.00», а в классе реализуем метод: ходить на автобусе, на метро, пешком, на велосипеде. Так и происходит реализация контракта. 

Вернемся к кошкам. Мы «создаем контракт», т.е. объявляем методы:

interface Cat {

    void walkOnFourLegs();

    void wagTail();

    void findEat();

}

Класс, который будет реализовывать наши методы, как бы «подписывает контракт» (в примере — OtherCat).

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

interface Cat {

    void walkOnFourLegs();

    void wagTail();

    void findEat();

}

class HomeCat implements Cat {

    public void walkOnFourLegs() {

        System.out.println("Кошка ходит на четырех лапах");

    }

    public void wagTail() {

        System.out.println("Я умею вилять хвостом");

    }

    public void findEat() {

        System.out.println("Я домашняя кошка, поэтому сижу и жду, пока меня покормит хозяин");

    }

}

class OtherCat implements Cat {

    public void walkOnFourLegs() {

        System.out.println("Кошка ходит на четырех лапах");

    }

    public void wagTail() {

        System.out.println("Я умею вилять хвостом");

    }

    public void findEat() {

        System.out.println("Я other кошка, поэтому сижу и жду, пока меня покормит хозяин");

    }

}

public class Main {

    public static void main(String args[]) {

        HomeCat cat = new HomeCat();

        OtherCat otherCat = new OtherCat();

        printCat(cat);

        printCat(otherCat);

    }

    public static void printCat(Cat cat) {

        cat.findEat();

    }

}

Пример реализации методов интерфейса
Пример реализации методов интерфейса

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

interface CatFromFairytale {

    void talking();

    void drinking();

    void whistling();

Методы сможет реализовать наш класс OtherCat или HomeCat. В случае с абстрактным классом-родителем class HomeCat был ограничен: он получал только те методы, которые ему передавал родитель. Благодаря интерфейсам класс HomeCat становится «более свободным» и может выбирать, чьи методы ему имплементировать.

Пример с кошками выше достаточно условный. Он помогает понять общие принципы классов и интерфейсов. Но теперь мы можем решить более приближенную к реальности задачу. Пускай нам нужно подключить к сервису три способа платежа: кошелек YooMoney, банковскую карту МИР и мобильный телефон Beeline.

Мы можем создать один интерфейс, в котором будет метод pay(). Он будет передавать условные clientID и amount. Для всех способов платежа эти данные будут одинаковыми. Хэш будет считаться внутри каждой реализации отдельно (это важно, чтобы подписать сигнатуру и т.д.). Дальше создаем три class:

  • class YooMoney;
  • class BankCard;
  • class Beeline.

Они будут реализовывать интерфейс. В каждом классе мы описываем метод pay() и прописываем конкретную реализацию. Например, для class YooMoney будем делать POST-запрос на условный yoomoney.ru, подписывать сигнатуру таким-то образом, response обрабатывать таким-то образом.

Отличия абстрактного класса и интерфейса

В таблице ниже постараемся привести основные особенности. Разница между abstract class и interface может зависеть от конкретного ЯП.

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

Критерий сравненияАбстрактный классИнтерфейс
Какие методы содержитКак абстрактные, так и с реализациейАбстрактные, без реализации
СинтаксисЗависит от языка. Для объявления родителя используют abstract class, для наследования потомками — extendsДля объявления используется зарезервированное слово interface. Для реализации — implements
Для чего используетсяСоздать общий родительский класс с теми дефолтными возможностями, которые унаследуют все потомкиВыкупить долю полиморфизма, декомпозировать систему на более стройные модули
Работа с методамиМожно создать несколько дефолтных методов, которые переедут в потомкаТолько декларация контракта (interface), реализация в дочерних классах
Имеет переменные конструктора или экземпляраМожет Не может 
Как происходят наследование и реализацияКласс может отнаследоваться только от одного абстрактного класса (в некоторых ЯП возможна реализация множественного наследования как в С++)Класс может реализовать несколько интерфейсов (множественное наследование). Интерфейсов можно имплементировать в класс сколько угодно: 1, 5, 10 и так далее

Когда использовать абстрактный класс, а когда — интерфейс

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

Интерфейс стоит использовать, когда программист хочет выйти в сторону полиморфизма. Мы можем создавать разные классы, которые реализуют («подписали») один и тот же интерфейсный контракт. Но при этом у всех разное поведение, как в нашем случае с платежами. Также interface подходит для ситуаций, когда разработчику нужно сгруппировать все классы под одно поведение и «накрыть» одним интерфейсом.

В некоторых языках программирования можно наследоваться только от одного класса. Interface решает эту проблему: он позволяет имплементить несколько интерфейсов.

Заключение

На простых примерах и базовом уровне мы разобрали, как работают абстрактные классы и интерфейсы в объектно-ориентированном программировании. В ООП интерфейс и абстрактный класс — одни из ключевых элементов.