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

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

> вынести этот метод в интерфейс Shape и прописать его имплементации в классах Square и Rectangle
То есть изменить работающий и протестированный класс Rectangle?
Как же тогда
> новая функциональность вводится путем добавления нового кода, а не изменением старого, уже работающего?
Вынос методов в интерфейс Shape никак не меняет реализацию класса Rectangle

Обычная путаница с этим прямоугольником.
Все потому, что забываете о главном принципе ООП — абстракции.
Если наши фигуры математические, то они иммутабельные. И да, квадрат это наследник прямоугольника. Без проблем.
Как только водятся методы set, то это уже не математические фигуры. Это что-то из реального мира.
И тогда "фигня в фирме прямоугольника, которая может менять размеры и пропорции" это не предок "фигни в форме квадрата, которая может менять размеры".
Для уточнения иерархии надо понять, что же у нас есть. Модель чего мы строим.
По сути весь SOLID это уточнение абстракции

А может квадрат наследник ромба без проблем?

У ромба есть «длина» и «один из углов» (проще постулировать, что он не более 90 градусов).
Так что изменяемость ромба ломает наследование.
И тут интерфейсы лучше, потому что
public Square extends Square, Rhombus
public Square extends Rectangle, Rhombus конечно
тест на площадь провалится
А может они все наследники параллелограмма?

А может геометрического места точек?

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

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

Если под интерфейсом понимать полное сохранение семантики, то например для квадрата вызов метода setSize(width, height) унаследованный от прямоугольника не может быть вызыван с отличающииися агрументами. Чуть ниже я привел пример, что у квадрата всегда есть вписанная окружность, и эта операция не всегда применима к прямогольнику.


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

Однако, для их изменения требуется объявить операции с ними. И именно в этот момент делает их разными в поведении. И запрещает наследование.


Нет никаких проблем.
public Square scale(Square, int) {}
public Rectangle scale(Rectangle, int) {}
public Rectangle scale(Rectangle, int, int) {}

Если мы меняем размер пропорционально, то квадрат останется квадратом. А иначе- всегда прямоугольник.

При этом да, интерфейсы позволяют абстрагироваться абстрактнее :)

Прямоугольник в такой модели может быть очень похожим на квадрат при определенных условиях и тогда у него может быть например метод isSquare() который ответит является ли фигура квадратом. У двух фигур может быть общий метод scale(factor). Но это будет общим свойством для фигур. Но факт остается фактом, что что у квадрата, что у прямоугольника могут быть свои специфичные сеттеры и даже геттеры. Например геттер получения вписанной окружности. (при этом я подразумеваю не просто совпадение имени но и полное сохранение семантики) не во всякий прямоугольник можно вписать окружность.

Именно что общего решения нет.
Но и проблема «наследует ли квадрат от прямоугольника»- надуманная.

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

В данном случае всё просто- наследование изначально противоречило изменчивости.
Для изменяемого прямоугольника вообще «квадрат» это свойство- он может стать на время квадратом, а потом перестать им быть. И это хорошо, что автор сделал размеры целыми, а то с double возникает проблема «с какой погрешностью наш прямоугольник можно считать квадратом» :D

И запрещает наследование

нет, просто надо переопределить наследуемую операцию

И да, квадрат это наследник прямоугольника.
Как по- по-вашему, красный прямоугольник наследник прямоугольника?

Нельзя отредактировать комментарий с мобильного. Какой же Хабр классный.

Можно. Возможно, надо было обновить страницу, или просто чуть-чуть подождать
Скорее «цветной прямоугольник» может быть наследником прямоугольника. А «красный» это просто значение свойства.
Другое дело, что математические фигуры цвета не имеют. Для построение правильной иерархии надо понимать задачу — абстракцию чего нам надо создать.

Я о том, что равенство сторон — это тоже просто значения свойства.

В целом- да. Но иногда- нет :)

Продемонстрируйте пример этого "иногда".

Ещё раз- когда фигуры у нас это математические (иммутабельные) объекты, то может оказаться полезным иметь отдельный тип (в java лучше интерфейс, в связи с отсутствием множественного наследования — в C++ или Eiffel можно и классом обойтись) «квадрат», который наследует «прямоугольник» и «ромб».
Хотя бы из-за этого наследования — квадрат можно передать туда, где ждут ромб.
>И да, квадрат это наследник прямоугольника
Нет.

>Все потому, что забываете о главном принципе ООП — абстракции
Вы сами напомнили и сами забыли ) Поэтому, по хорошему, мы делаем абстрактный класс Четырёхугольник и наследуем его. Тем самым, в иерархии квадрат и прямоугольник не будут наследоваться друг от друга.
Какая польза от лишнего класса?
Лучший код- его отсутствие.
Каждая лишняя строчка должна приносить пользу.
Где-то надо иметь интерфейсы и кассы «многоугольник», «четырёхугольник», «выпуклый четырёхугольник» «прямоугольник», «квадрат», «ромб», где-то хватит двух классов.
>Какая польза от лишнего класса?
В классе Квадрат вам будет достаточно одного поля для хранения значения стороны. Я думаю понятно, что это уменьшит расход оперативной памяти и очень драгоценного кэша ЦПУ, если у вас будет очень много квадратов. Не говоря уже о том, что будет меньше путаницы с квадратами и прямоугольниками в коде.

>Лучший код- его отсутствие.
Нет.

>Каждая лишняя строчка должна приносить пользу.
Нет. Если вы пишите на высокоуровневом ЯП, то это задача компилятора в уменьшении количества кода для ЦПУ, а не ваша. Ваша задача, как промышленного программиста, делать код понятным для другого человека, подразумевая, что он будет менее образованным, чем вы сами.

Добрый день!


  1. Почему Абстрация важнее других принципов?
  2. Почему математические фигуры автоматом иммутабельны? Квадрат с изменяемой стороной перестаёт быть квадратом в математическом смысле?
Почему Абстрация важнее других принципов?


Потому что если посмотреть на ошибки проектирования- то неверная/протекающая абстракция встречается очень и очень часто.

Почему математические фигуры автоматом иммутабельны?


В математике даже нет понятия «присвоить переменной новое значение»
Если уж пишут, что x=5 то оно всегда равно 5, а не до какой-то точки.

Квадрат с изменяемой стороной перестаёт быть квадратом в математическом смысле?


На мой взгляд- да.

Я могу принять, что абстракция и иммутабельность так выглядят так для Вас, но не могу принять это как агрументы.

Квадрат с изменяемой стороной перестаёт быть квадратом в математическом смысле?

На мой взгляд- да.

Логично

НЛО прилетело и опубликовало эту надпись здесь
его еще зовут Барбара
НЛО прилетело и опубликовало эту надпись здесь

и Анжела Меркелева...

НЛО прилетело и опубликовало эту надпись здесь
Если бы она выросла в Бердичеве, была бы Варвара Лискова. Но она ведь родилась и всю жизнь прожила в США :) Поэтому Лисков.

Вот именно ;)

Если честно, не вижу связи.

У неё в документах написано "Liskov", не никаких оснований переводить "Лискова"

Вот интересно получается с фамилиями мужчин и женщин. Ваш пример с «Рейтингом Пейджа» релевантен только потому, что он — Ларри. Будь на его месте Лариса Пейдж, язык бы не повернулся сказать «Рейтинг Пейдж-ы/евой». Думаю, так же и с Лисков не стоит искать еврейских корней.
НЛО прилетело и опубликовало эту надпись здесь
Так как класс Square находится в соотношении ISA с классом Rectangle, будет логично предположить, что тесты на Rectangle должны быть справедливы и для Square.

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

Это не просто другая сущность — это наследник, подкласс. И тесты, которые справедливы для родителя должны быть справедливы и для наследника как раз из-за Принципа подстановки.
как правильно формировать иерархию наследования и ответим на философский вопрос о том, является ли квадрат прямоугольником.

Ну что, опять?

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

Open-Close это вовсе не только наследование, лучше уже про композицию рассказывать, в большинстве случаев полезнее будет.
Spoiler header
Или, почему геттеры и сеттеры не ООП, хе-хе.

парадигме ООП, которая, в частности, говорит нам, что классы должны моделировать объекты реального мира

А автор термина говорил(раз говорил, два говорил, тут писал, тут в комментариях писал), что объекты должны быть изолированны как биологические клетки, и общаться друг с другом через посылку сообщений, а про моделирование реального мира — не говорил.
Да и кому нужно реальный мир моделировать(соседа, кстати, надо или нет моделировать?) — не понятно.
Да и кому нужно реальный мир моделировать(соседа, кстати, надо или нет моделировать?) — не понятно.

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

Хм. Да, вы правы.

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

А если пытаться спроецировать какие-то отношения из-реального мира на иерархии наследования(а-ля moderator extends user, или фигуры с геттерами, как выше пытаются) — то такого моделирования уж точно не нужно.

Для меня наследование — один способов моделирования отношений между сущностями предметной области и moderator extends user выглядит нормально на первый взгляд, если и то, и другое сущности предметной области.

Для меня наследование — один способов моделирования отношений между сущностями предметной области и moderator extends user выглядит нормально на первый взгляд, если и то, и другое сущности предметной области.

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

Во первых, если есть какой-то единый класс User в проекте — очень часто(из того что я видел/узнавал/писал) он с запахом, особенно когда кто-то пытается моделировать User'ом живого пользователя из реального мира, и кладёт туда всё, что с этим человеком связано, и логины с паролями, и профиль с форума, и банковские аккаунты, получая в итоге god-object на 100 полей(да и то чаще просто огромную анемичную модель с геттерами-сеттерами т.к. на этом месте люди разочаровываются в рич-моделях, а декомпозировать как-то не принято, не учат).
Думаю понятно что при таком раскладе уже пора решать проблемы, а не множить наследованием.

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

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

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

Вот именно потому что с технической точки зрения она довольно опасна, для технических задач типа DRY она должна использоваться только в исключительных случаях. Для моделирования же она вполне подходящая, пока а) отражает отношение "является" в контексте предметной области и б) не возникает для этого отношения проблемы множественного наследования


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

Open-Close это вовсе не только наследование, лучше уже про композицию рассказывать, в большинстве случаев полезнее будет.

Да, это вовсе не только наследование, но LSP показывает нам как его соблюсти именно в контексте наследования.
Да и кому нужно реальный мир моделировать(соседа, кстати, надо или нет моделировать?) — не понятно.

Пожалуй я слишком упрощённо сформулировал. Речь идёт о том, что мы оперируем абстракциями, причём не столько непосредственно объектов реального мира сколько понятий. У каждого понятия есть набор признаков. При написании ООП-кода мы выбираем те из них, которые являются наиболее значимыми в конкретной предметной области, то есть формируем абстракцию.
С LSP проблема, что он очень чувствителен к формулировке.
если для каждого объекта o1 типа S существует такой объект o2 типа T, что для всех программ P, определенных в терминах T, поведение программы P не изменяется, если вместо o2 подставить o1, то S — подтип T.
если S является подтипом T, тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.


То есть во втором и третьем случае требуется только совместимость, в то время как первая формулировка, если её понимать максимально строго, фактически запрещает полиморфизм.

В то время, как в оригинале:
Let phi(x) be a property provable about objects x of type T. Then phi(y) should be true for objects y of type S where S is a subtype of T.
вакуум, сферичнее некуда, и никаких конкретных указаний там нет. Под него больше всего подходит как раз 2-й вариант.

Ну насколько красивее оригинальная формулировка

Все становится гораздо интереснее, когда вы узнаете про pattern matching
Первый вариант лучше отрефакторить через полиморфизм? Внести метод отрисовки в класс?
Если фигуры приходят из сторонней библиотеки — мы не можем их изменять. А отрисовать в нашем приложении мы можем без проблем.

За пример с квадрато-прямоугольником — прямо мега, гига, супер респект. Невероятно визуально и интуитивно.

Так а в чем проблема прямоугольник унаследовать от квадрата?
Rectangle extends Square
extends — перев. расширять
Square — прямоугольник с наложенными на него ограничениями (т.е. прямоугольника «шире»)
А треугольник это частный случай четырехугольника, а четырехугольник частный случай шестиугольника у которого 2 угла 180 градусов.
(С) Ваш К.О.

Я бы еще отметил, что наследование классов — впринципе очень опасная операция, с которой надо обращаться аккуратно. Когда вы наследуюте один класс от другого, то вы подразумеваете, что наследуете то, как там ваш родитель "работает под капотом". Иначе говоря вы наследуете не только семантику, но еще и реализацию этой семантики.


Выражать конструкции вида "квадрат это прямоугольник" путем наследования класса Square от класса Rectangle — опасно (это как раз то, что приводит к тому, что класс Square в реализации в статье работает не оптимально и требует в два раза больше памяти чем нужно).


Правильнее определить два интерфейса (я думаю не стоит упоминать, что интерфейсы еще и позволяют Square унаследовать дополнительно от Rhombus...):


interface Rectangle {
  int getHeight();
  int getWidth();
  int perimeter();
}
interface Square extends Rectangle {
  int getSideLength();
  default int getHeight() { return getSideLength(); }
  default int getWidth() { return getSideLength(); }
}

И после этого писать реализации RectangleIml и SquareImpl под каждый. Причем на практике допустимо реализовать SquareImpl путем наследования от RectangleIml, это обычный инженерный trade-off между "написать поменьше кода" и "покушать меньше памяти". Просто академически это неправильно.


Ну а про мутабельность Rectangle и Square сверху умные люди уже написали. В данном случае иммьютабельность — это не просто поклон моде, а вынужденная необходимость, чтобы Rectangle и Square оставались прямоугольником и квадратом в математическом определении соответственно. Если есть желание по существующему объекту интерфейса Rectangle получить другой Rectangle, с измененной высотой, то это можно делать имьютабельно путем добавления метода в интерфейс Rectangle:


  /**
   * Creates new Rectangle which inherits width from {@code this} Rectangle but has {@code newWidth}
   */
  Rectangle rectWithWidth(int newWidth);
Известно, что наследование реализует отношение «является» (ISA).

Я не думаю, что много программистов на практике придерживаются этого. В основном это вопрос компоновки кода, а не взаимосвзяей. Т.е. с помощью ООП пытаются реализовать более древнюю мантру "не повторяй себя".

Ну почему опять на этом примере? Такое чувство, что принцип был придуман только ради квадрата-прямоугольника.
Я занимаюсь разработкой на Java в Tinkoff.ru

И не нашлось примеров из реальной работы?
Если нет, то либо сами не применяете принцип либо негде применять в реальности:)

Как же у меня полыхает, когда из SOLID делают заповеди. Заповедь L гласит: "И не унаследует же никто квадрат от прямоугольника, ибо не будет тому прощения отныне и во веки веков".


  1. Что будет нарушено, если абстракции фигур не будут требовать операций нахождения периметра и площади и прочих нарушающих L в SOLID?
  2. Может всё-таки воздержаться от утверждений "A нельзя моделировать как наследник B"?
  3. Может быть всё-таки всё дело в конкретной выбранной абстрактной модели под конректную задачу?
Проблема, думаю, в том, что у людей после нынешних статей «про ООП» в голове не объекты с поведением, а мутабельные структурки данных(поле+геттер+сеттер), которые меняют отдельные процедуры.

На геттеры/сеттеры давно пора начать крестовый(++) поход! OOP VULT!

Геттеры норм для сущностей, пока сущности не выходят за пределы слоя бизнес-логики

На геттеры/сеттеры давно пора начать крестовый(++) поход! OOP VULT!

А в самой лишь войне против них не много смысла. Если воинственно отстаивать их «ненужность», вы будете либо вызывать презрение и недоверие, либо просто один карго культ замените другим.

Нужно развивать понимание, а не сеять смуту.
В реализацию квадрата и теста закралась маленькая неточность, которая создает иллюзию «опровержения принципа» и хрупкости механизма наследования реализации.

Итак, квадрат — это прямугольник, у которого стороны равны, тем не менее, код квадрата:
а/ вместо выдачи ошибки почему-то позволяет задавать разные длины сторон и
б/ тайно «под капотом» их меняет, т.е. set несимметричен с get

Тест на непонятных основаниях считает, что:
а/ у прямоугольника set-ы детерминированы (не меняют состояние других элементов) и
б/ не отлавливает ошибку задания длины
А если задать нулевую или отрицательную? А если завтра наледником будет не просто Square, а Square4x4?

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

объект класса Square — это определенно не объект класса Rectangle

да ладно

public class Rectangle
{
    public double Width {  get; set; }
    public double Height {  get; set; }

    public double Perimeter => 2 * (Width + Height);
}

public class Square : Rectangle
{
    public new double Width 
    {  
        get => base.Width;
        set => base.Width = base.Height = value;
    }
    public new double Height
    {
        get => base.Height;
        set => base.Width = base.Height = value;
    }

    public Square(double side) => Width = Height = side;
}

public class Program
{
    public static void Main()
    {
        Square s = new(5);
        Console.WriteLine(s.Height);
        Console.WriteLine(s.Perimeter);
        Console.ReadLine();
    }
}

Точнее так (с учетом всего прочитанного тут) - только вот реализацию периметра не удается унаследовать о прямоугольника, а может и не нужно



public interface IQuadrilateral
{
    public double Perimetr();
}

public class Quadrilateral:IQuadrilateral
{
    private double[] _sides;
    public Quadrilateral() { _sides = new double[4]; }
    public double Perimetr() => _sides.Sum();
}

public interface IRombus : IQuadrilateral
{
}
public interface IParalellipiped : IQuadrilateral
{
}

public class Paralellipiped: IParalellipiped
{
    private double[] _sides;
    public Paralellipiped() { _sides = new double[4]; }
    public double Perimetr() => _sides.Sum();

}
public class Rombus:IRombus
{
    private double _size;
    private double _angle;
    public double Perimetr() => 4 * _size;
    public Rombus(double size, double angle)
    {
        _size = size;
        _angle = angle;
    }
}

public interface IRectangle: IParalellipiped
{
    public double GetHeight();
    public double GetWidth();
}

public class Rectangle:IRectangle
{
    private double _height;
    private double _width;
    public Rectangle(double height, double width)
    {
        _height = height; _width = width;
    }
    public double GetHeight() => _height;
    public double GetWidth() => _width;
    public double Perimetr() => 2 * (_height + _width);
}

public class Square: IRectangle,IRombus
{
    private double _side;
    public Square(double side)
    { _side = side; }
    public double GetHeight() { return _side; }
    public double GetWidth() { return _side; }
    public double Perimetr() { return 4*_side; }

}

public class Program
{
    public static void Main()
    {
        Square s = new(5);
        Console.WriteLine(s.GetHeight());
        Console.WriteLine(s.Perimetr());
        Console.ReadLine();
    }
}

через расширения удобней

public class Diamond
{
    protected double _side;
    protected double _diagonal;
    public Diamond(double side, double diagonal)  {_side = side; _diagonal = diagonal;}
    public double Side { get => _side; set => Side = value; }
    public double Diagonal { get => _diagonal; set => Diagonal = value; }
    public void Display() => Console.WriteLine($"Side: {_side}");
    public static implicit operator Diamond(Square square) => new Diamond(square.Side, square.Side*Math.Sqrt(2));
}
public class Rectangle
{
    protected double _width;
    protected double _height;
    public Rectangle(double height, double width) { _height = height; _width = width; }
    public double Width { get => _width; set => _width = value; }
    public void Display() => Console.WriteLine($"Width: {_width}, Height: {_height}");
    public static implicit operator Rectangle(Square square) => new Rectangle(square.Side, square.Side);
}
public class Square
{
    protected double side;
    public Square(double side) => this.side = side;
    public double Side { get => side; set => side = value; }
    public void Display() => Console.WriteLine($"Side: {side}");
}
class Program
{
    static void Main(string[] args)
    {
        Square square = new Square(5);
        square.Display();
        Console.WriteLine();
        Rectangle rectangle = square;
        rectangle.Display();
        Console.ReadLine();
    }
}

Зарегистрируйтесь на Хабре , чтобы оставить комментарий