Как не нарушить принцип замещения Лискова (LSP)?

Я нахожусь в ситуации, очень похожей на ту, о которой упоминал Стив МакКоннелл в Code Complete. Только то, что моя проблема основана на транспортных средствах, а трайк по закону относится к категории автомобилей. Автомобили до сих пор имели четыре колеса. В любом случае, мой домен излишне сложен, поэтому легко придерживаться приведенного ниже примера с кошками.

С подозрением относитесь к классам, которые переопределяют подпрограмму и ничего не делают внутри производной подпрограммы. Обычно это указывает на ошибку в структуре базового класса. Например, предположим, что у вас есть класс Cat и подпрограмма Scratch(), и предположим, что в конце концов вы обнаружите, что у некоторых кошек нет когтей, и они не могут царапаться. У вас может возникнуть соблазн создать производный от Cat класс с именем ScratchlessCat и переопределить процедуру Scratch(), чтобы она ничего не делала. Этот подход создает несколько проблем:

Он нарушает абстракцию (контракт интерфейса), представленную в классе Cat, изменяя семантику его интерфейса.

Этот подход быстро выходит из-под контроля, когда вы распространяете его на другие производные классы. Что будет, если найти кошку без хвоста? Или кошка, которая не ловит мышей? Или кошка, которая не пьет молоко? В конце концов вы получите производные классы, такие как ScratchlessTaillessMicelessMilklessCat.

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

Место для исправления этой проблемы находится не в базовом классе, а в исходном классе Cat. Создайте класс Claws и включите его в класс Cats. Корень проблемы заключался в предположении, что все кошки царапаются, поэтому устраняйте эту проблему в источнике, а не просто перевязывайте ее в месте назначения.

По тексту из его великой книги выше. Следовать плохо

Родительский класс не обязательно должен быть абстрактным

public abstract class Cat {
   public void scratch() {
      System.out.println("I can scratch");
   }
}

Производный класс

public class ScratchlessCat extends Cat {
   @Override
   public void scratch() {
      // do nothing
   }
}

Теперь он предлагает создать еще один класс Claws, но я не понимаю, как я могу использовать этот класс, чтобы избежать необходимости в ScratchlessCat#Scratch.


person Java Ka Baby    schedule 28.05.2012    source источник
comment
Пример с кошкой — хорошая иллюстрация LSP. Изучая LSP, я был поражен тем, сколько плохих примеров и сколько недопонимания на SO и других сайтах.   -  person SteveT    schedule 21.09.2012


Ответы (2)


У вас по-прежнему будет метод scratch(), но он не будет переопределен производными классами:

public class Cat {
  Claw claw_;
  public Cat(Claw claw) {claw = claw_;}
  public final void scratch() {
    if (claw_ != null) {
      claw_.scratch(this);
    }
  }
}

Это позволяет вам делегировать логику царапания содержащемуся объекту Claw, если он присутствует (и не царапать, если когтей нет). Классы, производные от кошки, не имеют права голоса в вопросе о том, как царапать, поэтому нет необходимости создавать теневые иерархии, основанные на способностях.

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

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

person Attila    schedule 28.05.2012

Тот факт, что не у всех кошек есть когти и они способны царапать, является важным признаком того, что Cat не следует определять общедоступный scratch метод в своем API. Первый шаг — подумать, почему вы определили scratch в первую очередь. Возможно, кошки царапаются, если могут, когда на них нападают; если нет, они шипят или убегают.

public class Cat extends Animal {
    private Claws claws;

    public void onAttacked(Animal attacker) {
        if (claws != null) {
            claws.scratch(attacker);
        }
        else {
            // Assumes all cats can hiss.
            // This may also be untrue and you need Voicebox.
            // Rinse and repeat.
            hiss();
        }
    }
}

Теперь вы можете заменить любой подкласс Cat другим, и он будет вести себя правильно в зависимости от того, есть ли у него когти или нет. Вы можете определить класс DefenseMechanism для организации различных защит, таких как Scratch, Hiss, Bite и т. д.

person David Harkness    schedule 28.05.2012