Как стать автором
Обновить

Комментарии 65

А какой смысл в применении наследования для повторного использования кода?
Для этой цели наследование реализаций в любом случае подходит плохо, так как ломает инкапсуляцию.

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


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


Ну вот композиция для повторного использования кода куда более прозрачна, вот только вот разработка атомарных и выделение общих компонент — долгое и нудное занятие. Куда проще нашлепать public class B:A, потом C:B, потом D:C, E:D и т.д. (видел как-то калокод с иерахией в 40 классов). Потом правда кто-нибудь влезет в В или А и все сломается, но автора уже не будет и чинить будут другие люди.

Для добавления свойств и поведения уже написанному коду достаточно их добавить в предка — корень иерархии. И волшебство произойдёт за счёт полиморфизма (позднего связывания кода). Например, захотели добавить свойство цвет ко всем фигурам — добавили его в их общего предка, пусть даже абстрактного, реализовали методы для работы с этим свойством; и автоматически цветность приобретут все унаследованные фигуры, даже уже написанные.
Звучит красиво, но на практике «добавить свойство цвет ко всем фигурам» скорее всего есть нарушение SRP, со всеми вытекающими.

Да и вообще оказывается, что нужен не просто цвет, но и фон, а ещё тень(контур). И в каждой фигуре надо прописать что должно быть каким цветом. А ещё оказывается у окружности нет «тела», которое надо бы раскрашивать — приходится раскрашивать контур в цвет фигуры. А в нашей иерархии таких бестелесных фигур несколько, в разных ветвях — надо обобщить код вне иерархии…

И непонятно в чем профит, если всё равно потрогали код всех фигур, а «общее» поведение в родительском классе подходит только половине наследников (хорошо, если большей — на самом деле «верхней»).
SRP (принцип единственной ответственности) распространяется на класс или объект, а не иерархию. Ведь в иерархии самый старый древний класс, от которого все наследуются, может тоже меняться, не влияя на реализацию потомков. Пример: в классе-предке реализуется черчение фигур на графопостроителе, а классы фигур-потомков занимаются исключительно представлением себя (векторизацией) и используют методы вывода на то инкапсулированное векторное устройство, которое подраумевает класс-предок, отдавая ему данные векторизации. Затем, с развитием технологий, появились векторные дисплеи, класс-предок переписали так, чтобы использовать выбор: отображать фигуры на графопостроителе или векторном дисплее — в зависимости от положения механического переключателя. И программу изменили только в части класса-предка, а остальную иерархию классов не трогали — все ранее написанные фигуры продолжали считать, что они отображают себя на некоем векторном устройстве вывода, возвращая лишь данные для своего построения. Затем появились растровые устройства отображения, класс-предок тоже пришлось переписать, не затрагивая код классов-потомков, таким образом, чтобы данные векторного представления фигур в нём растеризовались и отсылались на принтер или дисплей…
Так я не понял, какая одна ответственность у вашего предка?

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

Отдавать данные векторизации — это не отношения «родитель-потомок», тут наследование вообще ничего не даёт.
Мои коллеги очень любят такое наследование. Говорият, что один extend заменяет кучу import.
И в 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. И другие классические определения ООП на Википедии (начиная со статьи ООП, и дальше — полиморфизм, инкапсуляция, наследование)

Ну как бы вам сказать. Тогда наследование вообще убогое получается, если следовать LSP, и, как желтушно кричит заголовок, «бесмыссленно» (отчасти). Но следовать LSP или не следовать — дело договоренности внутри команды. Если команда следует, это одно, если не следует — это другое.
Команда, в которой работаю, этому принципу не следует.
Многие ли следуют этому принципу, что из тех, что здесь отметились? Те, кто прошлись по моим комментариям, видимо следуют (как минимум два человека), а остальные?

Обрати внимание на тех, кто там это написал — Саттер и Александреску, создатели 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 соток)
Этот метод вернет только одно решение, оно будет равно 0.
Или переопределить его так, чтобы его нельзя было вызвать (хотя вот это уже плохое решение, но все равно рабочее).
Кодировать ошибки магическими числами тоже — так себе решение.
Это не магическое число. Это как раз-таки ответ. На что можно изменить ширину, чтобы площадь осталась неизменной? На 0.
Увы, я не силен в математике, но попробую описать: a^2=0, где 0 это изменение площади. Отсюда a=0.
Окей, как насчет метода changeWidth, который меняет просто ширину? Как быть тестам, которые проверяют, что после изменения ширины длина не изменилась?

Более простой пример: у нас есть класс List, мы от него наследуем класс DuplicateList, который добавляет элемент в список два раза. Как быть с базовым инвариантом, что после добавления объекта длина списка уеличилась на один? Что делать с методами Remove/Contains/..., должны ли мы удалить все объекты в Remove или только один?..
Более простой пример: у нас есть класс List, мы от него наследуем класс DuplicateList, который добавляет элемент в список два раза.

Тогда мы должны либо уточнить определение метода в List либо от него не наследоваться

Вы не можете не наследоваться. Ну даже если не от класса, то от интерфейса IList придется, иначе этим классом невозможно будет пользоваться, потому что для него придется копипастить реализацию всех методов. В этом и проблема. Что мы не можем наследоваться, но очень хочется. Если мы не наследуемся, то просто копипастим все реализации из текущего класса List, поправляя инварианты по ходу дела.

Для решения проблемы копипасты в некоторых языках есть трейты. Что скажете насчет наследования от IList, подключения дефолтной реализации из трейта и переопределения только необходимых методов?

Возвращаемся к проблеме с которой начали — вполне логично от интерфейса IList ожидать, что добавление элемента увеличит количество его элементов ровно на один.

Тогда не используйте IList если добавление элемента дожно увеличить количество на большее — выделите из него интерфейс без этого ограничения или сделайте свой параллельно

Тут есть еще хотя бы та проблема, что «унаследованный» таким образом квадрат будет занимать в 2 раза больше места, потому что вместо того, чтобы хранить просто свою сторону, он будет хранить (длина, ширина).
В определение прямоугольника вообще не входит хранение его величин. Он вообще может вычисляться формулой и быть неизменным.
С практической точки зрения нас это все же интересует. Мы работаем не с машиной Тьюринга с бесконечной памятью, и рост х2-х3 по памяти просто из-за использованных паттернов вряд ли оценят даже в Java-мире, где совет «докупите ОЗУ, дешевле выйдет» самый частоиспользуемый.
Что мы не можем наследоваться, но очень хочется

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

Часто это в смысле «никогда»?


знакомый студент недавно пытался на ООП моделировать геометрические фигуры. По началу все шло ок, но когда дошло до определения пересечений фигур, то начался треш, угар и вопли «да кто придумал этот ооп».
Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).

Просто неверно выбран уровень абстракции. В таком случае есть смысл создать класс сторона и создать фабрику фигура которая будет моделировать любой многоугольник как ромб и квадрат так и октаидр и другие. Но если вы заходит чтобы ваше приложение обрабатывала все множество то может быть стоит уйти к истокам геометрии и рассмотреть класс точка и фабрику фигура которая будет моделировать все разнообразие имея в себе один из методов сторона.…
Только надо нам было просто создать прямоугольник и квадрат и нарисовать их методом .draw()
А теперь нам надо учебник по геометрии поддерживать.
В этом отличие студента и профессионала. Студент создаст класс прямоугольник и будет им рисовать квадрат. Профессионал все унаследует от точки и будет строить фигуры используя метод line прежде чем рисовать методом draw или render что лучше оставляя возможность рисовать различные фигуры которые захочется нарисовать в будущем. Таким образом у студента получаяется консольное приложение которое в качестве лабы идет в зачет, у профессионала получается САПР которые со временем расширяется и выростает до мощностей AutoCAD. Ничего особенного.
По-моему наоборот — профессионал с опытом разработки сначала посмотрит на задачу, и если там действительно нужно только рисовать прямоугольники и квадраты — он не будет делать САПР.
А вот студент — возможно.
Профессионал, который будет наследовать фигуры от точки может выкинуть свой диплом в помойку, ибо явно не понимает разницы между композицией и наследованием.

Надо либо не обобщать либо обобщать корректно.
Квадрат это прямоугольник.
Но не "Изменяемый квадрат это изменяемый прямоугольник".
Квадраты и прямоугольки могут быть вообще константами.


В-общем, в википедии ж все написано про то, что с этим делать :)

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

Видимо, не «заменить», а «отработать без ошибок».
Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).

А я вот вижу это абсолютно иначе — вы можете использовать квадрат везде, где используется прямоугольник (любой метод, который может работать с прямоугольником или возвращает прямоугольник, будет корректно работать и с квадратом), но нельзя использовать прямоугольник везде, где используется квадрат (по тем же причинам — методы работающие с квадратами не смогут корректно обработать произвольный прямоугольник).
Еще наследование иногда используется для эмуляции типов-сумм (variants, tagged unions, discriminated unions). В таком случае родитель выступает в роле маркера. Он, как-бы, говорит — мои наследники — это разные представлени одного класса, и каждый, кто хочет с ним работать, должен уметь работать с каждым из моих наследников.
НЛО прилетело и опубликовало эту надпись здесь

Насколько я помню, 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);
        }
}
P.S. Не обращайте внимания на громозкие конструкции clone, это один из недостатков java, во многих аналогичных ООП языках (даже на JVM) клонирование делается автоматически. Но суть одинаковая, о копии нужно запрашивать сам экземпляр объекта, для этого не обязательно знать какой конкретный тип он реализует.
Спасибо большое!
Пока не отпускает ощущение, что простые вещи делаются сложно. Я через рефлексию сделал, количество кода получилось примерно такое же. Благо, что проект тестовый, а не реальный.
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории