Страницы

Поиск по вопросам

понедельник, 9 декабря 2019 г.

Как правильно организовать отношение между квадратом и прямоугольником с точки зрения наследования? [дубликат]

#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 (продублировал её, чтобы все увидели, т. к. комментарии не все читают). Из статьи я узнал, что мысли Дейта отнюдь не оригинальны, как я полагал, и другие авторы тоже выдвигают похожие идеи. Прочтение данной вики-статьи вполне заменит наполненную зубодробительными выкладками главу книги.

Комментариев нет:

Отправить комментарий