Как бороться с полиморфизмом в классе

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

Я попытаюсь объяснить мой вопрос на простом примере: предположим, что язык с динамической типизацией (например, ECMAScript) и следующая структура классов:

диаграмма

class A{
    private idA;
    public A(){
        idA=0;
    }
    public foo(){
        update();
        if (this.idA!=3) Throws new Exception(" What is happening? ");
    }
    private update(){
        this.idA = 3;
    }
}
class B extends A{
    private idB;
    public B(){
        super();
        idB=0;
    }
    public foo(){
        super.foo();
        // Any operation between A::update and B::update()
        if (this.idB!=0) Throws new Exception("hmmm, that could not happend!");
        update();
    }
    private update(){
        this.idB = 5;
    }
}

В этом очень простом примере, когда я создаю объект класса B, B::foo() вызывает родительский объект A::foo(), который вызывает "update". Объект является экземпляром B, поэтому вызываемыми функциями "update" является B::update, после чего в B:: foo снова вызывается функция update (B::update). В результате A::update никогда не вызывается, а idA по-прежнему 0.

Класс A работает правильно, когда используется один, но после расширения его с помощью B функция foo () завершается ошибкой.

Каково правильное решение этой проблемы:

1) Заставить класс A вызвать A::update, что означает ужасный код при каждом вызове его собственной функции (защита суперкласса):

A::foo(){
    A::update();
    if (this.idA!=3) Throws new Exception(" What is happening? "); 
}

2) B:: update является расширением A::update, поэтому B:: update должен вызывать себя родительскую функцию (подготовить подкласс и решить проблемы):

B::foo(){
    super.foo();
    ... // Any operation that must be performed between A::update and B::update
}
B::update(){
    super.update();
    this.idB = 5;
}

Но в этом случае A:: foo вызывает обновление, а не B::foo. Это означает, что другие проблемы.

3) Любое другое решение.

Как резюме:

Как защитить код суперкласса от полиморфизма?

  • Добавьте защиту в суперкласс.
  • Разобраться с этими проблемами, создав дочерний класс
  • Язык должен сделать это! (не знаю, возможно ли это с динамически типизированными языками)

Я ищу очень теоретическое / каноническое решение этого вопроса.

РЕДАКТИРОВАНИЕ: убрать проблему из конструктора и уточнить некоторые моменты.

4 ответа

Решение

Базовый класс должен защищать себя от вредных переопределений. В соответствии с принципом открытия / закрытия он должен быть открыт для расширения, но закрыт для модификации. Переопределение update является вредной модификацией предполагаемого поведения базового класса. В вашем примере нет преимущества в переопределении update потому что оба A::update а также B::update являются частными методами, которые имеют дело с частными переменными. Даже не ожидается, что они должны быть выполнены вместе, судя по вашему исключению в B::foo, Если B::update был назван по-другому, не было бы ничего плохого в вашей реализации. В любом случае, возможно, все будет в порядке: поскольку ни один из известных мне языков не позволит вам переопределить закрытый метод, B::update мог скрыть A::update вместо того, чтобы переопределить это.

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

tl; dr: Дети не имеют дела с частными лицами своих родителей.

Обычно считается очень плохой практикой вызывать методы экземпляра, и особенноvirtual методы экземпляра из конструктора именно по этой причине (но также по той причине, что объект еще не завершен, будучи "инициализированным").

3) Любое другое решение.

Док, мне больно, когда я это делаю.

Тогда не делай этого!

Серьезно, если вам нужно установить IdA в конструкторе Aне звоните update, сделать это, явно установив значение IdA в конструкторе для A,

Тебе, вероятно, не понравится мой ответ, но: условность и дисциплина.

Установить конвенции для

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

Документируйте эти соглашения и придерживайтесь их. Вероятно, они должны быть частью вашего кода; либо в форме комментариев или соглашений об именах (все, что работает для вас). Я мог бы придумать что-то вроде этого:

/*
 * @final
 */
function shouldNotBeOverridden() {
}

/*
 * @overridable
 * @call-super
 */
function canBeOverriddenButShouldBeCalledFromChildClasses() {
}

/*
 * @overridable
 */
function canBeOverridenWithoutBeingCalledFromChildClasses() {
}

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

И если кто-то все еще отменяет ваш @final методы, у вас, надеюсь, есть тщательное тестирование;)


Мне нравится этот ответ на несколько похожий вопрос относительно Python:

Вы можете добавить туда комментарий:

# We'll fire you if you override this method.

Если язык позволяет одному классу вызывать закрытый метод другого класса таким образом, программист должен понимать и жить с этим. Если я понимаю вашу цель, foo и update должны быть переопределены, а обновление должно быть защищено. Затем они будут вызывать метод в родительском классе, когда это необходимо. Производный метод foo не должен вызывать update, потому что вызов foo в родительском классе позаботится об этом. Код может работать так:

class A{
    private idA;
    public A(){
        idA=0;
    }
    public foo(){
        update();
        if (this.idA!=3) Throws new Exception("idA not set by update");
    }
    protected update(){
        this.idA = 3;
    }
}
class B extends A{
    private idB;
    public B(){
        super();
        idB=0;
    }
    @Override
    public foo(){
        super.foo();
        // Any operation between A::update and B::update()
        if (this.idB!=5) Throws new Exception("idB not set by super.foo");
    }
    @Override
    protected update(){
        super.Update()
        this.idB = 5;
    }
}

Я изменил исключения, чтобы соответствовать ожиданиям.

Другие вопросы по тегам