Квадрат — это прямоугольник?
В геометрии квадрат — это особый случай прямоугольника, в котором высота и ширина фигуры равны. При моделировании квадратов и прямоугольников в объектно-ориентированных языках программирования определение этой связи может привести к неожиданным ошибкам.
Отношение IS A (подчинения)
Отношение между подклассом и его базовым классом часто называют отношением «IS A», поскольку подкласс является версией суперкласса. Например, вы можете разработать приложение, которое хранит данные о членах команды в организации. Вы можете создать класс TeamMember, который является базовым типом для класса SoftwareDeveloper. Поскольку вы можете заменить объекты SoftwareDeveloper там, где ожидается TeamMember, можно сказать, что SoftwareDeveloper является TeamMember.
Понятие отношения «IS A» может вводить в заблуждение. При определении того, должен ли один тип наследовать другой, важно помнить, что вы имеете дело с представлениями реальных объектов, а не с самими объектами. В приведенном выше отношении физический программист является физическим членом команды. Поведение объекта программиста также является расширением поведения члена команды. Поэтому отношение наследования является подходящим. Существует много ситуаций, в которых отношение между двумя физическими объектами подходит для отношения «IS A», но их поведение не подходит.
Квадраты и прямоугольники
Одной из ситуаций, в которых отношения между физическими объектами и их представлениями различаются, являются отношения между квадратами и прямоугольниками. С геометрической точки зрения квадрат является прямоугольником. Оба объекта соответствуют требованиям прямоугольника: они имеют четыре стороны, все углы которых являются прямыми. Квадраты имеют дополнительное свойство: все их стороны имеют одинаковую длину.
Физическое отношение «IS A» между квадратом и прямоугольником может подтолкнуть вас к разработке иерархии классов, в которой Square является подклассом Rectangle. Вы можете посчитать, что физическое отношение можно напрямую отобразить в коде как отношение наследования. Однако это не так из-за некоторых тонких, но значительных проблем, связанных с принципами SOLID, в частности с принципом замещения Лисков.
Мы можем продемонстрировать несовершенство отношения наследования, создав классы Rectangle и Square в C# и посмотрев, где возникают проблемы. Пример кода будет упрощенным, чтобы показать, что проблемы начинают возникать быстро. Они будут усугубляться по мере добавления новых функций. Для начала создайте класс Rectangle со свойствами Height и Width и методом для вычисления площади фигуры:
class Rectangle
{
public virtual int Height { get; set; }
public virtual int Width { get; set; }
public int CalculateArea()
{
return Height * Width;
}
}
Далее создайте класс Square, используя Rectangle в качестве базового типа:
class Square : Rectangle
{
}
Вышеуказанный тип Square не является идеальным, поскольку позволяет определять квадраты без соответствующей высоты и ширины. В настоящее время Square — это не что иное, как Rectangle с новым именем. Один из способов обновить класс, чтобы гарантировать, что представленные объекты действительно являются квадратами, — это переопределить установщики свойств. Когда устанавливается одно измерение, другое может быть обновлено, чтобы соответствовать ему. Пересмотренный код показан ниже:
class Square : Rectangle
{
public override int Height
{
get { return base.Height; }
set { base.Height = base.Width = value; }
}
public override int Width
{
get { return base.Height; }
set { base.Height = base.Width = value; }
}
}
Проблемы
При рассмотрении в отдельности класс Square теперь функционирует правильно. Невозможно создать объект Square с несовпадающими размерами. Однако мы нарушили принцип замещения Лисков (LSP) и потенциально внесли ошибки в клиенты класса Rectangle. Наиболее очевидным нарушением LSP является проблема с инвариантами. Клиенты класса Rectangle знают, что при изменении свойства Height свойство Width остается неизменным. С введением класса Square это правило было нарушено.
Рассмотрим следующий код. Создаются два объекта Rectangle и задаются их высота и ширина. В первом случае высота задается перед шириной, а во втором — ширина задается первой. Это не имеет значения, поскольку оба свойства являются инвариантными при изменении другого. Последняя строка кода сравнивает два прямоугольника и определяет, что они имеют совпадающие размеры.
Rectangle r1 = new Rectangle(); r1.Height = 4; r1.Width = 5; Rectangle r2 = new Rectangle(); r2.Width = 5; r2.Height = 4; bool match = r1.Height == r2.Height && r1.Width == r2.Width; // match = TrueLSP гласит, что замена объекта подкласса не должна изменять поведение или правильность программы. Однако, если мы заменим прямоугольники квадратами, результат сравнения будет ложным. Это связано с тем, что изменение одного из размеров квадрата влияет на другой, и порядок, в котором указываются ширина и высота, становится важным:
Rectangle r1 = new Square(); r1.Height = 4; r1.Width = 5; Rectangle r2 = new Square(); r2.Width = 5; r2.Height = 4; bool match = r1.Height == r2.Height && r1.Width == r2.Width; // match = FalseЭта, казалось бы, безобидная связь наследования вызвала проблемы, хотя Rectangle и Square все еще являются простыми классами. По мере расширения их функциональности проблемы могут усугубляться. Например, классы могут использоваться для представления фигур в пакете автоматизированного проектирования (CAD). Это программное обеспечение может требовать, чтобы прямоугольник был растянут, чтобы поместиться в заданном пространстве, сохраняя при этом соотношение сторон. Замена квадрата может привести к тому, что в результате растяжения получится слишком большой или слишком маленький квадрат. Если пакет позволяет добавлять фиксированное значение к высоте и ширине, квадраты могут неправильно получить это значение дважды.
Решением проблемы является исключение отношений наследования. Альтернативой может служить базовый класс «Shape» или интерфейс «IShape», общий для прямоугольников и квадратов. Базовый класс не будет включать свойства Height и Width, вместо этого размеры различных фигур будут обрабатываться их реализациями. Подкласс Rectangle будет иметь свойства Height и Width, а Square может иметь одно значение Size. Дальнейшие классы, такие как Pentagon, могут быть определены с использованием нескольких свойств длины и угла.
Другой альтернативой решению проблемы Square и Rectangle может быть полное удаление класса Square. Вышеупомянутый тип Square не расширяет Rectangle путем добавления новых свойств или методов, поэтому может быть совершенно ненужным. Вместо этого можно просто использовать прямоугольники, размеры которых случайно совпадают.
Заключение
Принцип подстановки Лисков говорит нам, что отношения наследования должны основываться на внешнем поведении типов, а не на характеристиках реальных объектов, которые они могут представлять. Хотя квадрат является прямоугольником, внешнее поведение этих двух представлений несовместимо, поэтому наследование недействительно. Эта проблема применима к квадратам и прямоугольникам, кругам и эллипсам, а также ко многим другим физическим объектам, которые вы можете захотеть смоделировать. Наследование — мощный инструмент для объектно-ориентированного разработчика, но перед его применением необходимо тщательно продумать внешнее поведение.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.
Программы на заказ
Отзывы
Контакты