Является ли переопределение конкретных методов запахом кода?

31

Правда ли, что переопределение конкретных методов - это запах кода? Потому что я думаю, что если вам нужно переопределить конкретные методы:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

это можно переписать как

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

и если B хочет повторно использовать a () в A, его можно переписать как:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

который не требует наследования для переопределения метода, это правда?

ggrr
источник
4
Проголосовав, потому что (в комментариях ниже) Теластин и Док Браун договариваются о переопределении, но дают противоположные ответы «да / нет» на главный вопрос, потому что они не согласны со значением «запах кода». Таким образом, вопрос, касающийся дизайна кода, тесно связан с жаргоном. И я думаю, что это излишне, поскольку вопрос конкретно об одной технике против другой.
Стив Джессоп
5
Вопрос не помечен как Java, но код выглядит как Java. В C # (или C ++) вам придется использовать virtualключевое слово для поддержки переопределений. Таким образом, проблема становится более (или менее) неоднозначной из-за особенностей языка.
Эрик Эйдт
1
Кажется, что почти каждая функция языка программирования неизбежно в конечном итоге классифицируется как «запах кода» по прошествии достаточного количества лет.
Брэндон
2
Я чувствую, что finalмодификатор существует именно для таких ситуаций. Если поведение метода таково, что он не предназначен для переопределения, пометьте его как final. В противном случае ожидайте, что разработчики могут переопределить его.
Мартин Тускевичюс,

Ответы:

34

Нет, это не кодовый запах.

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

В обязанности каждого класса входит тщательное рассмотрение вопроса о том, подходит ли подкласс и какие методы могут быть переопределены .

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

Обычным случаем переопределения методов является реализация по умолчанию в базовом классе, которая может быть настроена или оптимизирована в подклассе (особенно в Java 8 с появлением методов по умолчанию в интерфейсах).

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

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

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}
Питер Уолсер
источник
Я понимаю, что в реальном мире переопределение конкретного метода может быть запахом кода для некоторых. Но мне нравится этот ответ, потому что он кажется мне более специфичным.
Darkwater23
В вашем последнем примере не должны Aреализовывать DescriptionProvider?
Пятнистый
@Spotted: A может реализовывать DescriptionProviderи, следовательно, быть поставщиком описания по умолчанию. Но идея здесь заключается в разделении интересов: Aнеобходимо описание (некоторые другие классы тоже могут), в то время как другие реализации предоставляют их.
Питер Уолсер
2
Хорошо, звучит больше как шаблон стратегии .
Пятнистый
@Spotted: вы правы, пример иллюстрирует шаблон стратегии, а не шаблон декоратора (декоратор добавит поведение компоненту). Я исправлю свой ответ, спасибо.
Питер Уолсер
25

Правда ли, что переопределение конкретных методов - это запах кода?

Да, в общем, переопределение конкретных методов - это запах кода.

Поскольку с базовым методом связано поведение, которое разработчики обычно уважают, его изменение приведет к ошибкам, когда ваша реализация сделает что-то другое. Хуже того, если они изменят поведение, ваша ранее правильная реализация может усугубить проблему. И вообще, довольно сложно соблюдать принцип подстановки Лискова для нетривиальных конкретных методов.

Теперь есть случаи, когда это можно сделать, и это хорошо. Полу-тривиальные методы могут быть безопасно переопределены. Базовые методы, которые существуют только как «нормальный» тип поведения, предназначенный для переопределения, имеют хорошее применение.

Следовательно, мне это кажется запахом - иногда хорошим, обычно плохим, посмотрите, чтобы убедиться.

Telastyn
источник
29
Ваше объяснение в порядке, но я прихожу к противоположному выводу: «Нет, переопределение конкретных методов - это вообще не запах кода» - это только запах кода, когда кто-то переопределяет методы, которые не были предназначены для переопределения. ;-)
Док Браун
2
@DocBrown Точно! Я также вижу, что объяснение (и особенно заключение) противоречит первоначальному утверждению.
Терсосаврос
1
@Spotted: Самый простой контрпример - это Numberкласс с increment()методом, который устанавливает значение в следующее допустимое значение. Если вы подклассифицируете его на то, EvenNumberгде increment()добавляются два вместо одного (а установщик обеспечивает равномерность), первое предложение OP требует дублирующих реализаций каждого метода в классе, а его второе требует написания методов-оберток для вызова методов в элементе. Часть цели наследования состоит в том, чтобы детские классы прояснили, чем они отличаются от родителей, предоставляя только те методы, которые должны отличаться.
Blrfl
5
@DocBrown: запах кода не означает, что что-то обязательно плохо, только то, что это вероятно .
Миколак
3
@docbrown - для меня это относится к принципу «отдавай предпочтение композиции наследованию». Если вы перебираете конкретные методы, вы уже в крошечной земле для получения наследства. Каковы шансы, что вы перебираете что-то, чем не должны быть? Исходя из моего опыта, я думаю, что это возможно. YMMV.
Теластин
7

если вам нужно переопределить конкретные методы [...], его можно переписать как

Если это можно переписать таким образом, это означает, что у вас есть контроль над обоими классами. Но затем вы знаете, разработали ли вы класс A таким образом, и если вы это сделали, то это не запах кода.

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

Ян Худек
источник
Если бы класс А был бы разработан для получения, разве не имело бы смысла иметь абстрактный метод для четкого выражения намерения? Как тот, кто отвергает метод, может знать, что метод был разработан таким образом?
Пятнистый
@Spotted, есть много случаев, когда реализации по умолчанию могут быть предоставлены, и каждая специализация должна только переопределить некоторые из них, или для расширения функциональности или для эффективности. Экстремальный пример - посетители. Пользователь знает, как методы были разработаны из документации; это должно быть там, чтобы объяснить, что ожидается от переопределения в любом случае.
Ян Худек
Я понимаю вашу точку зрения, но все же я думаю, что опасно, что единственный способ, которым разработчик может знать, что метод может быть переопределен, - это читать документ, потому что: 1) если он должен быть переопределен, я не могу гарантировать, что будет сделано. 2) если он не должен быть переопределен, кто-то сделает это для удобства 3) не каждый читает документ. Кроме того, я никогда не сталкивался со случаем, когда мне нужно было переопределить весь метод, а только части (и я прекратил использовать шаблон).
Пятнистый
@Spotted, 1) если оно должно быть переопределено, оно должно быть abstract; но есть много случаев, когда это не должно быть. 2) если оно не должно быть переопределено, оно должно быть final(не виртуальным). Но есть еще много случаев, когда методы могут быть переопределены. 3) если нижестоящий разработчик не читает документы, он собирается выстрелить себе в ногу, но он будет делать это независимо от того, какие меры вы предпримете.
Ян Худек
Итак, наша точка зрения расхождения только в 1 слове. Вы говорите, что каждый метод должен быть абстрактным или конечным, а я говорю, что каждый метод должен быть абстрактным или конечным. :) Каковы эти случаи, когда переопределение метода необходимо (и безопасно)?
Пятнистый
3

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

Это (очевидно) очень отличается от области статически типизированных языков, таких как Java, C ++ и т. Д. А также от природы строгой типизации , используемой как в Java и C ++, так и в C # и других.


Я предполагаю, что вы пришли из Java-фона, где методы должны быть явно помечены как окончательные, если разработчик контракта намеревается их не переопределить. Однако в таких языках, как C #, по умолчанию используется обратное. Методы (без каких-либо декораций, специальных ключевых слов) « запечатаны » (версия C # final) и должны быть явно объявлены так, как virtualесли бы им было разрешено переопределять.

Если API или библиотека были разработаны на этих языках с конкретным методом, который можно переопределить, то он должен (по крайней мере, в некоторых случаях) иметь смысл для его переопределения. Следовательно, не является запахом кода переопределение незапечатанного / виртуального / не finalконкретного метода.     (Это предполагает, конечно, что разработчики API следуют соглашениям и либо помечают как virtual(C #), либо помечают как final(Java) подходящие методы для того, что они проектируют.)


Смотрите также

Tersosauros
источник
При утиной типизации достаточно ли двух классов, имеющих одинаковую сигнатуру метода, чтобы они были совместимыми по типу, или же необходимо, чтобы методы имели одинаковую семантику, так чтобы они могли быть заменены на некоторые из подразумеваемых базовых классов?
Уэйн Конрад
2

Да, это потому, что это может вызвать супер -паттерн. Он также может денатурировать начальную цель метода, вводить побочные эффекты (=> ошибки) и делать тесты неудачными. Вы бы использовали класс с юнит-тестами красного цвета (с ошибками)? Я не буду

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

  • Аннотация: вы должны представить поведение
  • Финал: вам не разрешено изменять поведение

Самый безопасный способ добавить поведение в существующий класс - это то, что показывает ваш последний пример: использование шаблона декоратора.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}
пятнистый
источник
9
Этот черно-белый подход не так уж и полезен. Подумайте о Object.toString()Java ... Если бы это было так abstract, многие вещи по умолчанию не сработали бы, и код даже не скомпилировался бы, если бы кто-то не переопределил toString()метод! Если бы это было так final, все было бы еще хуже - нет возможности разрешать преобразование объектов в строку, за исключением способа Java. Как @Peter Walser ответа переговоры «s о, по умолчанию реализации (например Object.toString()) имеют важное значение. Особенно в Java 8 по умолчанию методы.
Терсосаврос
@Tersosauros Вы правы, если бы toString()был abstractили finalэто было бы боль. Мое личное мнение таково, что этот метод не работает, и было бы лучше вообще его не иметь (например, equals и hashCode ). Первоначальная цель Java по умолчанию состоит в том, чтобы обеспечить развитие API с ретро-совместимостью.
замечено
Слово «ретро-совместимость» имеет очень много слогов.
Крейг
@Spotted Я очень разочарован общим восприятием конкретного наследования на этом сайте, я ожидал лучшего: / Ваш ответ должен иметь гораздо больше голосов
TheCatWhisperer
1
@TheCatWhisperer Я согласен, но, с другой стороны, я потратил так много времени, чтобы выяснить тонкие преимущества использования декорации по сравнению с конкретным наследованием (на самом деле стало ясно, когда я начал писать тесты первыми), что никого нельзя винить в этом , Лучше всего попытаться обучить других, пока они не откроют Истину (шутка). :)
Пятнистый