#c_sharp #ооп #наследование
This question already has answers here: Как сделать общий метод двум классам C# (4 ответа) Closed 7 месяцев назад. Представим, что у нас есть два класса: прямоугольник Rectangle и квадрат Square. Как между ними правильно организовать отношение с точки зрения наследования? С одной стороны, квадрат - это частный случай прямоугольника, поэтому квадрат будет наследником, а прямоугольник - родителем (и это логично, ведь если изображать в кругах Эйлера, то круг Квадрат будет внутри круга Прямоугольник). С другой стороны, с точки зрения ООП все должно быть наоборот, т.е. прямоугольник должен наследоваться от квадрата, поскольку квадрату достаточно знать длину одной стороны, в то время как прямоугольнику - две. Чтобы не быть голословным, приведу 2 примера на языке C#. Rectangle наследует Square: public class Square { public int SideA { get; set; } public Square(int sideA) { SideA = sideA; } public virtual int Perimeter => 4 * SideA; } public class Rectangle : Square { public int SideB { get; set; } public Rectangle(int sideA, int sideB) : base(sideA) { SideB = sideB; } public override int Perimeter => 2 * (SideA + SideB); } Здесь функциональность расширяется благодаря появлению новой стороны SideB, однако пришлось переопределить свойство Perimeter. С точки зрения ООП, на мой взгляд, все хорошо (поправьте, если я не прав). Square наследует Rectangle: public class Square : Rectangle { public Square(int sideA) : base(sideA, sideA) { } } public class Rectangle { public int SideA { get; set; } public int SideB { get; set; } public Rectangle(int sideA, int sideB) { SideA = sideA; SideB = sideB; } public int Perimeter => 2 * (SideA + SideB); } Здесь, если рассматривать класс Square, для него появилось "бесполезное" свойство SideB, которое является копией SideA. Т.е. по факту, с точки зрения ООП, функциональность сузилась. Лично мне больше нравится второй вариант, он более лаконичный и правильный с точки зрения логики, но все же, как же правильно поступать в таком случае?
Ответы
Ответ 1
Имхо, нужно создать абстрактный класс Shape и от него пронаследовать Square и Rectangle. А причина проста: полиморфизм. Допустим, мы пронаследовали Rectangle от Square и создали метод public CalcSome(Square square). Мы абсолютно легитимно можем в него передать Rectangle так как он является наследником. Так вот, когда пользователь будет пользоваться этим чудо-методом, то он будет полагать, что работает с квадратом и ожидать поведения квадрата, а по факту туда может попасть Rectangle. И как следствии логично иметь абстрактный класс Shape, который не дает пользователю ложные предположения. Кстати, вот тут прямо в первом примере описывается почти ваша ситуация и говорится, что это противоречит Принцип замещения ЛисковОтвет 2
Правильно - "Square наследует Rectangle". Но не так, как у вас. У вас почему-то переопределен только конструктор, но не переопределены и не скрыты свойства SideA и SideB, поэтому квадрат, хоть и создается сначала с равными сторонами, впоследствии может обзавестись разными. Должно быть как-то так: public class Square : Rectangle { int _side; public Square(int side) : base(side, side) { } public override int SideA { get => _side; set => _side = value; } public override int SideB { get => _side; set => _side = value; } } public class Rectangle { int _SideA; int _SideB; public virtual int SideA { get { return _SideA; } set { this._SideA = value; } } public virtual int SideB { get { return _SideB; } set { this._SideB = value; } } public Rectangle(int sideA, int sideB) { SideA = sideA; SideB = sideB; } public int Perimeter => 2 * (SideA + SideB); } На самом деле, о "лишнем свойстве SideB" тут говорить несколько неправильно. Квадрат - все еще прямоугольник, у него есть обе стороны, просто они равны. Понятие "Квадрат" расширяет понятие "Прямоугольник" в данном случае дополнительным ограничением на равенство его свойств SideA и SideB. Если вас смущает, что в коде вида square.SideA = 1; square.SideB = 2 одно из значений будет "молча съедено", можно заменить один из сеттеров на throw new InvalidOperationException("Только одна сторона квадрата является изменяемой");.Ответ 3
Раньше такие темы помечались как "порождающие бесконечные прения" и закрывались безжалостно. :-) А по существу, в случае программистских классификаций надо вводить новый базовый класс - отрезок (или измерение). Тогда класс квадрат содержит один отрезок, а класс прямоугольник содержит два отрезка. Тогда сохраняются привычные круги Эйлера - прямоугольник это квадрат плюс еще один отрезок (еще одно измерение). Хотя кто сказал, что наследование в программировании должно повторять отношения вложенности множеств в математике? Если удобнее сделать как-то по-другому, то нужно делать по-другому.Ответ 4
стоит упомянуть о принципе подстановки Лисков. если взять первый пример: - прямоугольник наследуется от квадрата - допустим, у квадрата есть геометрические методы со специфичной реализацией для квадрата тогда придется переопределять их для прямоугольника во втором примере мы обходим это стороной, хоть поля и дублируются теперь мое мнение. в данном случае подойдет второй вариант, или оставить только "прямоугольник", не создавать "квадрат". но для каждого как будто похожего случая решение может быть разноеОтвет 5
Есть такая книга: "Введение в системы баз данных", К. Дж. Дейт, восьмое издание, 2005 год. В главе 20 "Наследование типов" автор высказывает свои мысли по поводу наследование Circle-Ellipse. Мысли весьма интересные и сильно отличающиеся от общепринятых. Дело в том, что автор большой специалист по реляционным базам данным и его идеи основаны именно на этом. Он с жаром отстаивает правильность модели данных, основанную на отношениях. В его мире всё должно соответствовать строгости реляционной алгебры и реляционных исчислений. Суть в том, что объект должен менять свой тип, в зависимости от выполнения ограничений, наложенных на тип. Если изначально был создан эллипс с отношением сторон, например, 5 и 6, то при изменении второго на 5, обе оси становятся одинаковы и объект должен поменять свой тип - стать окружностью. И наоборот. Насколько это осуществимо в мейнстримовых языках программирования - м-м-м, явно малоприменимо. Но мысль интересная. В комментариях дали ссылку Circle-ellipse problem (продублировал её, чтобы все увидели, т. к. комментарии не все читают). Из статьи я узнал, что мысли Дейта отнюдь не оригинальны, как я полагал, и другие авторы тоже выдвигают похожие идеи. Прочтение данной вики-статьи вполне заменит наполненную зубодробительными выкладками главу книги.
Комментариев нет:
Отправить комментарий