Рассмотрите следующий Java‑пример: class Rectangle { int width,height; void setWidth(int w){width=w;} void setHeight(int h){height=h;} } class Square extends Rectangle { @Override void setWidth(int w){ this.width=this.height=w; } @Override void setHeight(int h){ this.width=this.height=h; } } Объясните, почему такое наследование нарушает принципы проектирования (включая Liskov Substitution Principle), какие баги это вызывает в коде клиентов и как лучше спроектировать иерархию для прямоугольников и квадратов
Коротко: это наследование нарушает Liskov Substitution Principle (LSP) и принципы корректного проектирования, потому что подкласс меняет ожидаемое поведение методов базового класса (вводит дополнительный инвариант width==height) и даёт неожиданные побочные эффекты клиентам.
Почему именно нарушается LSP (и контракт методов)
Клиент, работающий с Rectangle, может ожидать, что setWidth(w) меняет только ширину, а высота остаётся прежней. Для Square этот ожиданный постусловие нарушается: setWidth(w) также меняет height. Подкласс изменяет поведение методов, т.е. не является взаимозаменяемым.В терминах предусловий/постусловий: подкласс не может ужесточать предусловия и не должен ослаблять постусловия. Square фактически меняет постусловие setWidth — недопустимо.Square вводит дополнительный инвариант (width == height), которого нет у Rectangle, — подкласс не должен накладывать новые обязательства на клиентов базового класса.
Пример проблем в коде клиентов
Ожидание: у Rectangle с начальным состоянием width=2, height=3 вызов setWidth(5) даёт площадь (5\times3=15).Для Square тот же код даст площадь (5\times5=25) — неожиданный результат для функции, принимающей Rectangle.Функции, которые по шагам изменяют width и height независимо, ломаются: например, void f(Rectangle r){ r.setWidth(5); / предполагаем height неизменным / r.setHeight(6); ... } Для Square первый вызов уже поменял height, поведение и последовательность действий несовместимы.Побочные эффекты и нарушения инвариантов могут приводить к трудноотлавливаемым багам в алгоритмах, сравнениях, коллекциях и т.д.
Как лучше спроектировать иерархию
Разделять понятия: Square — не подтип Rectangle, если Rectangle допускает независимые изменения сторон.Использовать общий интерфейс/абстракцию для общих операций (например, Shape или Rectangular) без контрактных методов, меняющих поведение, или с только читающими методами: interface Shape { int getArea(); } interface Rectangular { int getWidth(); int getHeight(); } class Rectangle implements Rectangular { ... } class Square implements Rectangular { / не extends Rectangle / ... }Сделать объекты неизменяемыми (иммутабельными): конструктор задаёт размеры, нет setWidth/setHeight. Тогда Square и Rectangle можно реализовать отдельно, оба возвращают корректные getWidth/getHeight/area.Если нужна изменяемость, использовать композицию и явные методы, не вводящие неожиданных побочных эффектов: Rectangle с setWidth/setHeight;Square с setSide(int s) либо с собственными методами; Square не наследует Rectangle.Другой подход — иметь абстрактный базовый класс без сеттеров, а конкретные реализации (ResizableRectangle, Square) реализуют интерфейсы по‑своему, но не ломают контрактов.
Резюме (правила проектирования)
Не делайте Square наследником Rectangle, если базовый класс допускает независимое изменение ширины/высоты.Предпочитайте интерфейсы и композицию; по возможности делайте объекты иммутабельными.Следите за контрактами методов (пред- и постусловия) — подкласс не должен менять ожидаемое поведение базового класса.
Коротко: это наследование нарушает Liskov Substitution Principle (LSP) и принципы корректного проектирования, потому что подкласс меняет ожидаемое поведение методов базового класса (вводит дополнительный инвариант width==height) и даёт неожиданные побочные эффекты клиентам.
Почему именно нарушается LSP (и контракт методов)
Клиент, работающий с Rectangle, может ожидать, что setWidth(w) меняет только ширину, а высота остаётся прежней. Для Square этот ожиданный постусловие нарушается: setWidth(w) также меняет height. Подкласс изменяет поведение методов, т.е. не является взаимозаменяемым.В терминах предусловий/постусловий: подкласс не может ужесточать предусловия и не должен ослаблять постусловия. Square фактически меняет постусловие setWidth — недопустимо.Square вводит дополнительный инвариант (width == height), которого нет у Rectangle, — подкласс не должен накладывать новые обязательства на клиентов базового класса.Пример проблем в коде клиентов
Ожидание: у Rectangle с начальным состоянием width=2, height=3 вызов setWidth(5) даёт площадь (5\times3=15).Для Square тот же код даст площадь (5\times5=25) — неожиданный результат для функции, принимающей Rectangle.Функции, которые по шагам изменяют width и height независимо, ломаются: например,void f(Rectangle r){ r.setWidth(5); / предполагаем height неизменным / r.setHeight(6); ... }
Для Square первый вызов уже поменял height, поведение и последовательность действий несовместимы.Побочные эффекты и нарушения инвариантов могут приводить к трудноотлавливаемым багам в алгоритмах, сравнениях, коллекциях и т.д.
Как лучше спроектировать иерархию
Разделять понятия: Square — не подтип Rectangle, если Rectangle допускает независимые изменения сторон.Использовать общий интерфейс/абстракцию для общих операций (например, Shape или Rectangular) без контрактных методов, меняющих поведение, или с только читающими методами:interface Shape { int getArea(); }
interface Rectangular { int getWidth(); int getHeight(); }
class Rectangle implements Rectangular { ... }
class Square implements Rectangular { / не extends Rectangle / ... }Сделать объекты неизменяемыми (иммутабельными): конструктор задаёт размеры, нет setWidth/setHeight. Тогда Square и Rectangle можно реализовать отдельно, оба возвращают корректные getWidth/getHeight/area.Если нужна изменяемость, использовать композицию и явные методы, не вводящие неожиданных побочных эффектов:
Rectangle с setWidth/setHeight;Square с setSide(int s) либо с собственными методами; Square не наследует Rectangle.Другой подход — иметь абстрактный базовый класс без сеттеров, а конкретные реализации (ResizableRectangle, Square) реализуют интерфейсы по‑своему, но не ломают контрактов.
Резюме (правила проектирования)
Не делайте Square наследником Rectangle, если базовый класс допускает независимое изменение ширины/высоты.Предпочитайте интерфейсы и композицию; по возможности делайте объекты иммутабельными.Следите за контрактами методов (пред- и постусловия) — подкласс не должен менять ожидаемое поведение базового класса.