Комментарии 65
Для этой цели наследование реализаций в любом случае подходит плохо, так как ломает инкапсуляцию.
А никакого. Кто-давно ляпнул глупость (хз, то ли в Симуле, то ли в Смоллтолке — меня тогда не было) и все давай ее использовать, не смотря на проблемы с инкапсуляцией, хрупкость классов, возрастающую цикломатическую сложность, фрагментацию данных и методов, да ряд других проблем (например с производительностью, при переопределении).
Вот только наследование реализации — это и есть собственно наследование :-) То что в статье выдается за наследование (очень распространенная ошибка) — это разные механизмы реализации полиморфизма: параметрический полиморфизм (генерики и шаблоны) и полиморфизм подтипов, вот полиморфизм подтипов можно реализовать либо системой включения подтипов (интерфейсы), либо системой включения подклассов — наследованием или композицией. Ну есть еще и третий вариант — перегрузка.
Ну вот композиция для повторного использования кода куда более прозрачна, вот только вот разработка атомарных и выделение общих компонент — долгое и нудное занятие. Куда проще нашлепать public class B:A, потом C:B, потом D:C, E:D и т.д. (видел как-то калокод с иерахией в 40 классов). Потом правда кто-нибудь влезет в В или А и все сломается, но автора уже не будет и чинить будут другие люди.
Да и вообще оказывается, что нужен не просто цвет, но и фон, а ещё тень(контур). И в каждой фигуре надо прописать что должно быть каким цветом. А ещё оказывается у окружности нет «тела», которое надо бы раскрашивать — приходится раскрашивать контур в цвет фигуры. А в нашей иерархии таких бестелесных фигур несколько, в разных ветвях — надо обобщить код вне иерархии…
И непонятно в чем профит, если всё равно потрогали код всех фигур, а «общее» поведение в родительском классе подходит только половине наследников (хорошо, если большей — на самом деле «верхней»).
Вы описываете модель, где класс бизнес-логики знает как себя и на плоттере печатать, и на экран выводить, да ещё при любом чихе требует перекомпиляции иерархии. Это ровно та содомия, которую я шестиметровой палкой трогать не хочу (даже не буду спрашивать как вы это тестируете — точно что-то грустное услышу).
Отдавать данные векторизации — это не отношения «родитель-потомок», тут наследование вообще ничего не даёт.
И в Play Framework его тоже слишком любят.
Меня это очень расстраивает, потому что мешает отлаживать программу в REPL.
Часто для наследования в ООП приводят контрпример отношений между квадратом и прямоугольникомЧасто это в смысле «никогда»? Взяли неудачный пример и что? Пусть скажем ну них базовый клас Figure. Очень удобно иметь возможность обьявить некий List и пройтись по нему вызывая виртуальный метод Draw. При добавлении новой фигуры код менять не надо. Вот это круто. Вот это «частый» пример.
Это вполне удачный пример для понимания, где наследование применяется правильно, а где нет.
https://en.wikipedia.org/wiki/Circle-ellipse_problem
Об этом часто упоминают в контексте The Liskov Substitution Principle.
Честно говоря, не вижу проблемы. В наследованном классе мы переопределяем метод, в котором можно изменить, скажем, ширину и пишем, что при изменении ширины у нас новая длина должна равняться новой ширине. И все. То есть у квадрата есть грубо говоря и ширина и длина, просто они равны.
Такое кстати возможно в Common Lisp, где на уровне CLOS можно менять тип в рантайме
Да, вы предлагаете то, что и в статье в википедии:
https://en.wikipedia.org/wiki/Circle-ellipse_problem#Allow_for_a_weaker_contract_on_Ellipse
В любом случае LSP нарушается. В таком простом примере это даже может сработать, но в более сложных могут начаться проблемы с абстракциями. Например, при работе с кругом часто используют понятие радиуса, а у эллипса нет радиуса. Соответственно, код, который работает с классом круга и оперирует радиусом круга не сможет работать с классом эллипса. Другими словами в этом случае подкласс требует от вызывающего кода больше, чем базовый класс.
У нас есть класс животное.
Есть унаследованный класс класс собака.
У собаки есть метод «гав», который можно вызвать.
мояСобака = новая Собака();
мояСобака.Гав();
Я не могу вызвать метод гав у животного, потому что его (метода) нет. У животного этот метод не определен. И это разумно. Но он есть у собаки. Тем не менее, собака является частной реализацией от общего класса животное.
Почему расширенный наследованный класс не может требовать от вызывающего кода больше, чем базовый класс? Или вообще другого кода?
Потому что это и есть сам LSP в отображении на код :-)
В формулировке Саттера и Александреску: «подкласс не должен требовать от вызывающего кода больше, чем базовый класс, и не должен предоставлять вызывающему коду меньше, чем базовый класс».
Это очень сильное требование, его сложно выполнять. Многие пишут на C++/Java/C#/итп без учета этого требования, и потом у них архитектура начинает трещать по швам
Вам стоит изучить вот это: SOLID. И другие классические определения ООП на Википедии (начиная со статьи ООП, и дальше — полиморфизм, инкапсуляция, наследование)
Команда, в которой работаю, этому принципу не следует.
Многие ли следуют этому принципу, что из тех, что здесь отметились? Те, кто прошлись по моим комментариям, видимо следуют (как минимум два человека), а остальные?
Обрати внимание на тех, кто там это написал — Саттер и Александреску, создатели C++ и D. У Гослинга тоже где-то было про это, сейчас не нагуглю. Короче, дизайн этих языков специально такой, чтобы форсировать LSP. Стандартная библиотека такая. Без него начинается нездоровая фигня.
Например, явно видно как они мучались, когда не было дженериков. Вот тебе код на C#:
static void update(object[] objs)
{
objs[0] = new object();
}
string[] strs = new string[] { "hello", "world" };
update(strs);
При запуске он бросит исключение вроде ArrayTypeMismatchException, поскольку Шарп не может записать экземпляр объекта внутрь массива из стрингов. Это — прямое нарушение LSP. string[] — это подтип object[], но когда его попытались использовать в том же месте где object[], всё сломалось.
В чем проблема? Если следовать логике, то пришлось бы писать несколько одинаковых методов: static void update(string[] objs)
, static void update(int[] objs)
— и люто копипастить. Как и делают в Golang, когда не опускаются до рефлексии и кодогенерации :)) В нормальных языках для этого есть дженерики, но когда Java и C# создавались, дженериков в них не было ещё. Поэтому массивы сделали ковариантными по типу элемента. В смысле, теперь можно отправить string[] на вход методу, который принимает object[], и это работает вот так:
static void Sort(object[] objs)
{
// ...
}
string[] strs = new string[] { "hello", "world" };
Sort(strs);
Совершенно очевидно, что это жёсткий хак системы, сделанный от безысходности. Заказчики языка хотели уменьшить копипасту во что бы то ни стало — и это в точности тот вопрос, который обсуждает в ОП-посте.
ООП не предназначено для уменьшения копипасты, оно скорее запрограммировано на её увеличение. Уменьшить именно дублирование можно на уровне другого над-языка, например препроцессора шаблонов. Например, в Java такой язык обсуждается на уровне проекта Valhalla, раздел специализации дженериков.
Так что это всё замечательно, что вы не соблюдаете LSP, но готовьтесь к тому, что в большой системе через некоторое время таким кодом будет невозможно пользоваться :)))
Меня этот вопрос интересует не по причине того, что я соблюдаю или не соблюдаю LSP, а потому что это одна из основополагающих проблем, благодаря которой мир языков программирования стал таким, каким мы его знаем.
Почему расширенный наследованный класс не может требовать от вызывающего кода больше, чем базовый класс? Или вообще другого кода?
Потому, что тогда код который работает с базовым классом не сможет работать с расширенным.
Я не могу вызвать метод гав у животного, потому что его (метода) нет. У животного этот метод не определен. И это разумно. Но он есть у собаки.
Собака предоставляет вызывающему коду сделать гав, но не требует от него его вызывать. То есть в данном случае LSP соблюдается.
(востребовано, к примеру, чтобы нарезать участки садоводам по 6 соток)
Или переопределить его так, чтобы его нельзя было вызвать (хотя вот это уже плохое решение, но все равно рабочее).
Увы, я не силен в математике, но попробую описать: a^2=0, где 0 это изменение площади. Отсюда a=0.
Более простой пример: у нас есть класс List, мы от него наследуем класс DuplicateList, который добавляет элемент в список два раза. Как быть с базовым инвариантом, что после добавления объекта длина списка уеличилась на один? Что делать с методами Remove/Contains/..., должны ли мы удалить все объекты в Remove или только один?..
Более простой пример: у нас есть класс List, мы от него наследуем класс DuplicateList, который добавляет элемент в список два раза.
Тогда мы должны либо уточнить определение метода в List либо от него не наследоваться
Для решения проблемы копипасты в некоторых языках есть трейты. Что скажете насчет наследования от IList, подключения дефолтной реализации из трейта и переопределения только необходимых методов?
Тогда не используйте IList если добавление элемента дожно увеличить количество на большее — выделите из него интерфейс без этого ограничения или сделайте свой параллельно
Что мы не можем наследоваться, но очень хочется
Мы не можем назвать квадрат треуглольником, но очень хочется (ну у него же есть, по крайней мере, три угла).
Часто это в смысле «никогда»?
знакомый студент недавно пытался на ООП моделировать геометрические фигуры. По началу все шло ок, но когда дошло до определения пересечений фигур, то начался треш, угар и вопли «да кто придумал этот ооп».
Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).
Просто неверно выбран уровень абстракции. В таком случае есть смысл создать класс сторона и создать фабрику фигура которая будет моделировать любой многоугольник как ромб и квадрат так и октаидр и другие. Но если вы заходит чтобы ваше приложение обрабатывала все множество то может быть стоит уйти к истокам геометрии и рассмотреть класс точка и фабрику фигура которая будет моделировать все разнообразие имея в себе один из методов сторона.…
А теперь нам надо учебник по геометрии поддерживать.
А вот студент — возможно.
Надо либо не обобщать либо обобщать корректно.
Квадрат это прямоугольник.
Но не "Изменяемый квадрат это изменяемый прямоугольник".
Квадраты и прямоугольки могут быть вообще константами.
В-общем, в википедии ж все написано про то, что с этим делать :)
не упоминается, что экземпляр подкласса должен быть в состоянии заменить экземпляр суперкласса везде в программе
Видимо, не «заменить», а «отработать без ошибок».
Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).
А я вот вижу это абсолютно иначе — вы можете использовать квадрат везде, где используется прямоугольник (любой метод, который может работать с прямоугольником или возвращает прямоугольник, будет корректно работать и с квадратом), но нельзя использовать прямоугольник везде, где используется квадрат (по тем же причинам — методы работающие с квадратами не смогут корректно обработать произвольный прямоугольник).
Насколько я помню, UML разделяет наследование и обобщение. Обобщение — это отношение между общим и частным. Наследование это передача каких то свойств от одного к другому.
Если есть обобщение то есть и наследование (кошка наследует все свойства общие для всех живтоных). Обратное — неверно (прототипное наследование не является обобщением, например "Типичный житель Москвы обладает карими глазами. Вася — совсем как типичный житель Москвы, только глаза голубые")
Это вы про наследование при обобщении. Просто оно популярнее, потому, что привычнее и вам кажется что другого не бывает. Наследование при прототипировании это "Мне нужен халат как такой, но с перламутровыми пуговицами". То есть не "Все жители Москвы такие" а "Жители Москвы такие, если я не скажу обратного".
Не путайте «Мне нужен вот такой халат, но с перламутровыми пуговицами» и «Мне нужен халат, такой же, как этот халат сейчас, но с перламутровыми пуговицами». Второе не имеет с наследованием ничего общего, первое в программировании грозит неожиданными изменениями.
Это и есть наследование в определении UML notation guide кажется.
И такое наследование есть javascript и self
А если халат с перламутровыми пуговицами пошить после изменений халата-образца без перламутровых пуговиц, то неожиданные изменения проекта будут даже в жизни.
Это не значит что это не наследование, это значит, что таоке наследование более непредсказуемо чем наследование при обобщении.
Если сумасшедший генетик вернётся в прошлое и сделает так, чтоб млекопитающие кормили детёнышей смесью только воды, солей и белка
Нифига. Кошка будет кормить молоком или чем-то похожим. Потому, что приспособится к тем же условиям. У кошки никогда не было предка, который был "просто млекопитающее". Сначала вместо молока был питательный пот. Природа делает среднее ухо и челюсти из жабер.
Может быть несколько разных классификаций одного и того же в зависимости от целей.
Если уже есть реальный файл на диске, от зачем писать
File f = new File();? Эта конструкция тянет на создание файла, но на самом деле нет.
Если уже заведен клиент компании в базе данных, то зачем писать
Client cl = new Client();? Эта конструкция тоже претендует на создание чего-то нового, но клиент-то уже есть.
Воспользуйтесь ORM или объектной базой данных. Это проблема конкретных API а не ООП
Я недавно на более тонкую несправедливость наступил. Везде рекомендуется работать с абстракциями, а не с конкретными реализациями. Eсли есть базовый класс Animal и Cat(Animal) и Dog(Animal), то рекомендуют рабоатать именно с Animal.
Теперь попробуем сделать простую операцию — копию объекта. Эту операцию без ООП делал миллион раз.
Cat cat = new Cat();
Animal a=cat;
Animal a2 = хочу копию а.
Далее програмная порнография. Рекомендую попробовать сделать самому без опускания на классы-потомки Dog и Cat.
Animal a2 = хочу копию а.
Animal a2 = a.clone();
Что за класс у нас в переменной a и как он реализует метод clone нас не должно интересовать, а сам конкретный объект знает как вернуть свою копию.
public class testClone {
public static class Animal implements Cloneable{}
public static class Cat extends Animal{}
public static void main(String[] args) {
Cat c = new Cat();
Animal a=c;
Animal a2 = c.clone();
}
}
public class Test {
public static class Animal implements Cloneable{
public Animal clone() throws CloneNotSupportedException {
return (Animal) super.clone();
}
}
public static class Cat extends Animal{
public Cat clone() throws CloneNotSupportedException {
return (Cat) super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
Animal a1 = new Cat();
Animal a2 = a1.clone();
System.out.println(a1);
System.out.println(a2);
}
}
Почему наследование всегда было бессмысленным