Принцип подстановки Лискова и множественные иерархии

Этот вопрос является продолжением this. Я пытаюсь определить иерархию классов, включающую несколько пар, производных от основания. В качестве наглядного примера предположим, что у меня есть класс Animal и класс Food. Animal имеет чисто виртуальную функцию для отметки его предпочтений в еде, принимая еду в качестве параметра.

class Food
{
    public:
    virtual void printName() {
    //......
    }
};

class Animal
{
    public:
    Food *_preferredFood;
    virtual void setFoodPreference(Food *food)=0;

};

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

class ZooManager
{
    vector<Aninal*> animals;
    public:
    void setAllPreferences(vector<Food *> foods) {
        assert(animals.size() == foods.size());
        for(int i =0;i<animals.size();i++) {
            animals[i]->setFoodPreference(foods[i]);
        }
    }
};

Все идет нормально. Теперь проблема в том, что существует множество различных производных классов для Food и Animal. Food имеет производные классы Fruit и Meat, а Animal имеет производные классы Carnivore и Herbivore. Herbivore может принимать только Fruit в качестве предпочтения в еде, а Carnivore может принимать только Meat.

class Fruit : public Food
{
};
class Meat : public Food
{
};
class Carnivore: public Animal
{
    public:
    void setFoodPreference(Food *food) {
    this->_preferredFood = dynamic_cast<Meat *>(food);
    }
};
class Herbivore: public Animal
{
    public:
    void setFoodPreference(Food *food) {
    this->_preferredFood = dynamic_cast<Fruit *>(food);
    }
};

Могу ли я создать для этого иерархию классов, не нарушая принцип подстановки Лискова? Хотя в этом вопросе я использую C ++, я бы тоже приветствовал ответы, относящиеся к Java.


person SPMP    schedule 30.03.2016    source источник


Ответы (2)


Во-первых, ваш setFoodPreference должен иметь возможность сбой. Это позволяет setFoodPreference взять Food* и иметь постусловие: либо установка предпочтений в еде, либо сбой.

Динамическое приведение также может быть ошибкой LSP, но если вы сделаете инварианты типов достаточно расплывчатыми, это не будет технически ошибкой.

Как правило, dynamic_cast означает, что типа переданного аргумента и его свойств недостаточно, чтобы определить, имеет ли аргумент определенные свойства.

В принципе, setFoodPreference(Food*) следует указывать с точки зрения того, какие Food* свойства должен иметь переданный аргумент, чтобы настройка была успешной; динамический тип Food* не является свойством Food*.

Итак: LSP утверждает, что любой подкласс Food должен подчиняться всем Food инвариантам. Аналогично для Animal. Вы можете избежать нарушения LSP, сделав инварианты неопределенными, а поведение методов - непредсказуемым; в основном говоря, что "он может выйти из строя по неустановленным причинам". Это ... не очень приятно.

Теперь вы можете сделать шаг назад и решить, что динамический тип вашего Food* является частью интерфейса Food*; это делает интерфейс смехотворно широким и насмехается над LSP.

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

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

person Yakk - Adam Nevraumont    schedule 30.03.2016
comment
Мне не нравится решение enum в первую очередь потому, что оно очень часто используется для злоупотребления объектно-ориентированным программированием, по существу помещая информацию о дочернем классе в базовый класс. Я видел код, в котором Base имеет перечисление для Type со значениями CHILD1, CHILD2, CHILD3 и т. Д., Который затем побуждает кучу кода принять Base*, а затем перейти к проверке базового типа. Такой код отражает то, что кто-то думает, что может получить все преимущества полиморфизма без каких-либо недостатков. dynamic_casting прочь тоже не намного лучше. - person AndyG; 30.03.2016
comment
@AndyG Я не сказал, что это отличное решение; но, как написано выше, каждое животное заботится о том, какую пищу оно получает. По сути, абстракция, стоящая за классом Food leaks, потому что Food - довольно бесполезная абстракция; Абстрагирование от обращения с сырым мясом так же, как с сеном, - очень сомнительная вещь. - person Yakk - Adam Nevraumont; 30.03.2016
comment
Согласовано. Я думаю, ОП загнали себя в угол. Мой комментарий не был предназначен для того, чтобы не согласиться с вами, но чтобы подчеркнуть, почему решения проблемы OP нежелательны. - person AndyG; 30.03.2016
comment
Мы еще не приступили к какой-либо реализации, поэтому я хочу работать над хорошим дизайном. Проблема здесь в том, что пока я пишу базовые классы, производные классы пишутся командами по всему миру, и мы даже не знаем, сколько их у нас может быть. Но существует определенная связь между производными классами Animal и производными классами Food. Меня сейчас беспокоит то, как я мог бы написать ZooManager. - person SPMP; 30.03.2016
comment
@ user2308211 Предположим, вы пишете систему, которая позволяет клиентам подключать систему сценариев. Они предоставляют сценарии и держатели переменных значений, и вы настраиваете способ для приложения подключать сценарии, сохранять их, запускать и загружать библиотеку (возможно, из сети), ресурсы и т. Д. Все эти подробности может предоставить решение, которого не будет в абстрактном вопросе о LSP и типах продуктов питания / животных, даже если у них обоих есть проблемы с LSP. - person Yakk - Adam Nevraumont; 30.03.2016

Ваш подход к построению иерархии неверен. ОО-классы представляют собой группы тесно связанных правил, где правило - это функция, а сильносвязанные - это общие данные. ОО-классы НЕ представляют объекты реального мира. Люди ошибаются, когда так говорят.

Если вы зависите от конкретного типа еды, вы нарушаете принцип замещения Лискова. Период.

Чтобы правильно спроектировать свои классы, чтобы вы не были вынуждены нарушать LSP, как вы здесь, вам необходимо разместить правила в классе Food, которые могут использоваться классом Animal для выполнения своих собственных правил. Или решите, должна ли Еда вообще быть классом. То, что показывает ваш пример, в основном действительно плохое сравнение строк.

person Mark Murfin    schedule 30.03.2016