JavaRush /Java блог /Random UA /Множинне успадкування в Java. Порівняння композиції та на...
HonyaSaar
26 рівень
Москва

Множинне успадкування в Java. Порівняння композиції та наслідування

Стаття з групи Random UA
Якийсь час тому я написав кілька постів про успадкування, інтерфейси та композиції в Java. У цій статті ми розглянемо множинне спадкування, а потім дізнаємося про переваги композиції перед спадкуванням.
Множинне успадкування в Java.  Порівняння композиції та успадкування - 1

Множинне успадкування в Java

Множинне наслідування – здатність створювати класи з безліччю класів-батьків. На відміну від інших популярних об'єктно-орієнтованих мов, на кшталт С++, мова Java не підтримує успадкування класів. Не підтримує він його через ймовірність зіткнутися з «проблемою алмазу» і замість цього воліє забезпечувати комплексний підхід для його вирішення, використовуючи кращі варіанти з тих, якими ми можемо досягти аналогічний результат успадкування.

«Проблема Алмазу»

Для більш простого розуміння проблеми алмазу припустимо, що множинне успадкування підтримується Java. У цьому випадку ми можемо отримати класи з ієрархією показаної на малюнку нижче. ієрархія класів "алмаз"Припустимо, що SuperClassце абстрактний клас, який описує деякий метод, а класи ClassAі ClassBреальні класи. SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
   	public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
    @Override
 public void doSomething(){
        System.out.println("Какая-то реализация класса A");
    }
  //собственный метод класса  ClassA
    public void methodA(){
    }
}
Тепер, давайте припустимо, що клас ClassCуспадковується ClassAі ClassBодночасно, і при цьому має наступну реалізацію:
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
    public void test(){
        //вызов метода родительского класса
        doSomething();
    }
}
Зауважте, що метод test()викликає метод doSomething()батьківського класу, що призведе до невизначеності, тому що компілятор не знає про те, який метод суперкласу повинен бути викликаний. Завдяки обрисам діаграми успадкування класів у цій ситуації, що нагадує обриси гранованого алмазу, проблема отримала назву «проблема Алмаза». Це і є основна причина, чому Java не має підтримки множинного успадкування класів. Зазначимо, що зазначена проблема з множинним успадкуванням класів також може виникнути з трьома класами, що мають як мінімум один загальний метод.

Множинне успадкування та інтерфейси

Ви могли звернути увагу, що я завжди говорю: «множинне спадкування не підтримується між класами», але воно підтримується між інтерфейсами. Нижче наведено простий приклад: InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {

    public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;

public interface InterfaceB {

    public void doSomething();
}
Зауважте, що обидва інтерфейси мають метод з однаковою назвою. Тепер, припустимо, ми маємо інтерфейс, успадкований від обох інтерфейсів. InterfaceC.java
package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

    //метод, с тем же названием описан в  InterfaceA и InterfaceB
    public void doSomething();
Тут, все ідеально, оскільки інтерфейси - це лише резервування/опис методу, а реалізація самого методу буде в конкретному класі, що реалізує ці інтерфейси, таким чином немає ніякої можливості зіткнутися з невизначеністю при успадкування інтерфейсів. Ось чому, класи Java можуть успадковуватися від декількох інтерфейсів. Покажемо на прикладі нижче. InterfacesImpl.java
package com.journaldev.inheritance;

public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {

    @Override
    public void doSomething() {
        System.out.println("doSomething реализация реального класса ");
    }

    public static void main(String[] args) {
        InterfaceA objA = new InterfacesImpl();
        InterfaceB objB = new InterfacesImpl();
        InterfaceC objC = new InterfacesImpl();

        //все вызываемые ниже методы получат одинаковую реализацию конкретного класса

        objA.doSomething();
        objB.doSomething();
        objC.doSomething();
    }
}
Можливо, ви звернули увагу, що я щоразу, коли я перевизначаю метод описаний у суперкласі або в інтерфейсі, я використовую анотацію @Override. Це одна з трьох вбудованих Java-анотацій і вам слід використовувати її при перевизначенні методів.

Композиція як порятунок

Так що робити, якщо ми хочемо використовувати функцію methodA()класу ClassAі methodB()класу ClassBв ClassС? Вирішенням цього може стати композиція – переписана версія, ClassCщо реалізує обидва методи класів ClassAі ClassBтакож має реалізацію doSomething()для одного з об'єктів. ClassC.java
package com.journaldev.inheritance;

public class ClassC{

    ClassA objA = new ClassA();
    ClassB objB = new ClassB();

    public void test(){
        objA.doSomething();
    }

    public void methodA(){
        objA.methodA();
    }

    public void methodB(){
        objB.methodB();
    }
}

Композиція чи успадкування?

Хорошим тоном програмування на Java є використовувати переваги композиції перед успадкуванням. Ми розглянемо деякі аспекти на користь такого підходу.
  1. Припустимо, ми маємо наступну зв'язку класів батько-спадкоємець:

    ClassC.java

    package com.journaldev.inheritance;
    
    public class ClassC{
    
    public void methodC(){
      	}
    
    }

    ClassD.java

    package com.journaldev.inheritance;
    
    public class ClassD extends ClassC{
    
        public int test(){
            return 0;
        }
    }

    Код вище чудово компілюється та працює, але що, якщо ClassCбуде реалізовано інакше:

    package com.journaldev.inheritance;
    
    public class ClassC{
    
        public void methodC(){
        }
    
        public void test(){
        }
    }

    Зауважте, що метод test()вже існує у класі спадкоємця, але повертає результат іншого типу. Тепер ClassD, якщо ви використовуєте IDE, не буде скомпільовано. Вам буде рекомендовано змінити тип результату, що повертається в класі спадкоємця або в суперкласі.

    Тепер уявімо таку ситуацію, де є багаторівневе успадкування класів і суперклас не доступний для наших змін. Тепер, щоб позбутися помилки компіляції, ми не маємо інших варіантів, крім як змінити сигнатуру або ім'я методу підкласу. Також нам доведеться внести зміни до всіх місць, де цей метод викликався. Таким чином, успадкування робить наш код крихким.

    Проблема, описана вище, ніколи не зустрічається у разі композиції, і тому робить останню кращою перед успадкуванням.

  2. Наступна проблема з наслідуванням полягає в тому, що ми демонструємо всі методи батька клієнту. І якщо суперклас розроблений не дуже коректно і містить дірки у «безпеці». Тоді, незважаючи на те, що ми повністю подбаємо про безпеку при реалізації нашого підкласу, ми все одно залежатимемо від неповної реалізації класу-батька.

    Композиція допомагає нам у забезпеченні контрольованого доступу до методів суперкласу, тоді як спадкування не підтримує жодного контролю за його методами. Це також одна з головних переваг композиції над спадкуванням.

  3. Ще одна перевага композиції – те, що вона додає гнучкості при виклику методів. Реалізація класу ClassC, описана вище, не оптимальна і використовує раннє зв'язування з методом, що викликається. Мінімальні зміни дозволять нам зробити виклик методів гнучким та забезпечити пізнє зв'язування (зв'язування під час виконання).

    ClassC.java

    package com.journaldev.inheritance;
    public class ClassC{
        SuperClass obj = null;
        public ClassC(SuperClass o){
            this.obj = o;
        }
        public void test(){
            obj.doSomething();
        }
    
        public static void main(String args[]){
            ClassC obj1 = new ClassC(new ClassA());
            ClassC obj2 = new ClassC(new ClassB());
    
            obj1.test();
            obj2.test();
        }
    }

    Програма виведе на екран:

    doSomething implementation of A
    doSomething implementation of B

    Ця гнучкість при виклику методу не спостерігається при успадкування, що змушує використовувати композицію як найкращий підхід.

  4. Модульне тестування простіше у разі композиції, тому що ми знаємо, що для всіх методів, які використовуються в суперкласі, ми можемо поставити заглушку для тестів, у той час як у спадкуванні ми сильно залежимо від суперкласу і не знаємо, як методи класу-батька будуть використані. Таким чином, через спадкування нам доведеться тестувати всі методи суперкласу, що є зайвою роботою.

    В ідеалі, успадкування слід використовувати лише тоді, коли відношення ” is-a ” справедливе для класів батька та спадкоємця, інакше варто віддати перевагу композиції.

Оригінал статті
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ