Композиция и наследование

116
21 декабря 2020, 03:10

Что такое композиция, и как она связана с наследованием? Какие преимущества она имеет перед последним? Какие подводные камни имеет этот приём?

Answer 1

Наследование - расширение одного класса другим. Когда следует использовать наследование:
1.Когда программисты знают детальное устройство всей ветки, от которой они наследуются.
ИЛИ
2.Когда расширяемые классы специально созданы и документированы для последующего расширения. То есть в документации должны быть отражены все условия, при которых класс может вызвать переопределяемый метод, какие его методы вызывают друг друга.

НО ОБЯЗАТЕЛЬНО

  1. Если вы можете ответить на вопрос — Является ли подкласс суперклассом? Часто суперкласс оказывается просто частью, необходимой для реализации подкласса. Например — стек не является вектором. Значит и наследовать вектор он не должен.

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

class InstrumentedHashSet extends HashSet{
    private int addCount = 0;
    //Пропуск конструктора. Перейдем к сути
    //…
    public boolean add(Object o){
        addCount++;
        super.add(o);
    }
    public boolean addAll(Collection c){
        addCount+=c.size();
        super.addAll(c);
    }   
    //… Другие методы
}

Когда мы вызываем метод addAll InstrumentedHashSet-а, то мы добавляем размер коллекции и вызываем метод addAll() HashSet, который содержит внутри метод add(). Он, в свою очередь, вызывает переписанный метод! В итоге нам отдадут число, которое в 2 раза больше ожидаемого. Такое «использование самого себя» является деталью реализации, и нет гарантии, что она не поменяется от одной версии к другой. Это можно поменять, просто поставив цикл с вызовом add(), но это является повторением кода, и вообще - можно что-то упустить. Этот вариант сложен, трудоемок и подвержен ошибкам.

  1. Суперкласс в новой версии может обзавестись некоторыми методами. Предположим, что у нас есть система безопасности. У нас есть подкласс, который контролирует добавление новых элементов(переопределяет методы, в них проверяет соответствие элементов каким-то нормам). Выходит новый метод, который имеет некую строку, которая просто добавляет строку напрямую. Итог: защита имеет брешь, систему можно взломать.

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

  3. Все дефекты суперкласса перейдут в API подкласса, в то время как композиция позволяет разработать новый API, который скрывает эти недостатки. Поэтому вы обязаны хорошо продумать реализацию, структуру, так как есть только один шанс что-то изменить. Нужно пробовать разные варианты структур, реализаций.

  4. Мы можем случайно наследовать класс, который вообще для этого не заточен, который либо не документирован для наследования, либо вообще не документирован. Решение — делать такие классы final, чтобы от него нельзя было наследоваться. Это, кстати, даст небольшой прирост в скорости.

Правила проектирования наследования:
1 Конструкторы суперкласса не должны вызывать переопределяемые методы непосредственно или опосредованно. Нарушение влечет аварийное завершение программы.

class Subclass extends ParentNew{
    Date date;
    Subclass(){
        date = new Date();
        parentMethodThatOverrides();//вызывает переписанный метод, все в порядке
    }
    //…
    @Override
    public void parentMethodThatOverrides(){
        System.out.println("date in incorrect method: "+date);
    }
}
class ParentNew{
    ParentNew(){
        parentMethodThatOverrides();//вызывает не свой, а переписанный метод!
    }
public void parentMethodThatOverrides(){}
} 

С реализацией интерфейсов Cloneable или Serializable в классе, предназначенном для наследования, нужно понять, что, поскольку методы clone() и readObject() в значительной степени работают как конструкторы, к ним применимо то же самое ограничение: ни методу clone(), ни методу readObject() нельзя разрешать вызывать переопределяемый метод, непосредственно или опосредованно. При реализации Serializable в классе-наследнике нужно методы readResolve или writeReplace сделать не закрытыми, а защищенными, чтобы подклассы(класса-наследника) не игнорировали эти методы.

  1. Полностью исключить использование переопределяемых методов самим классом. Так переопределение не будет влиять на результат действий других методов.

Но есть лекарство от этих проблем. Имя ему — композиция

Композиция — создание private(!) объекта «суперкласса» в подклассе. Этот подкласс называется классом-оболочкой Передача вызова -- каждый экземпляр метода в новом классе вызывает соответствующий метод содержащегося здесь же экземпляра прежнего класса, а затем возвращает полученный результат. Соответствующие методы нового класса носят название методов переадресации.

class InstrumentedHashSet{
    private int addCount = 0;
    HashSet hashSet;
    //…
    InstrumentedHashSet(Collection c){
        hashSet = new HashSet(c);
    }
    //...
    public boolean add(Object o){
        addCount++;
        return hashSet.add(o);
    }
    public boolean addAll(Collection c){
        addCount+=c.size();
        return hashSet.addAll(c);
    }   
    //… Другие методы
}

Преимущества:
Полученный класс будет прочен, как скала: он не будет зависеть от деталей реализации прежнего класса. Даже если к имевшемуся прежде классу будут добавлены новые методы, на новый класс это не повлияет. Данная реализация не только устойчива, но и чрезвычайно гибка. Мы просто преобразуем один класс(HashSet) в другой, добавляя некоторый функционал.

Недостаток:
Классы-оболочки обладают проблемой самоидентификации(SELF problem). Классы-оболочки не приспособлены для использования в схемах с обратным вызовом(callback framework), где один объект передает другому объекту ссылку на самого себя для последующего вызова. Так как объекту все равно на свою оболочку он входит в этот метод и его вызовы, переписанные в классе-оболочке, минуют перепись в классе-оболочки, и, как результат, используются непереписанные методы.

class Wrapper implements SomethingWithCallback {
    private final WrappedObject wrappedObject;
    //...
    @Override
    public void doSomething() {
        wrappedObject.doSomething();
    }
    @Override
    public void call() {
        System.out.println("Wrapper callback!");
    }
    //...
}
class WrappedObject implements SomethingWithCallback {
    private final SomeService service;
    //...
    @Override
    public void doSomething() {
        service.callSelf(this); // Комментарий
    }
    @Override
    public void call() {
        System.out.println("WrappedObject callback!");
    }
    //...
}
class SomeService{
    void callSelf(WrappedObject Object){
        Object.call();
    }
}

Комментарий - здесь он дает ссылку на СЕБЯ(для метода это объект WrappedObject, а не объект WrappedObject, завернутый в оболочку подкласса!). Естественно вызывется call() WrappedObject-а, так как теперь его ничто не переписывает.

READ ALSO
Как с генерировать исходный код из байтов?

Как с генерировать исходный код из байтов?

Имеется массив из байтов одной Java программыКак мне получить её исходники в нормальном Java виде, с помощью ASM может как-то ?

122
Login Filter вопрос

Login Filter вопрос

Написал Логин Фильтр, который не будем пропускать не авторизованного юзера :

118
POST PACH в 1С из JAVA/Аndroid

POST PACH в 1С из JAVA/Аndroid

Пишу небольшое приложение под android для инвенторизации

132