Разберите этот Java‑пример: class Rectangle { int width, height; int area() { return width*height; } } class Square extends Rectangle { void setSide(int s){width=height=s;} } Обсудите, нарушает ли наследование Liskov Substitution Principle и как лучше спроектировать иерархию
Коротко: да — в типичном (изменяемом) варианте это нарушает Liskov Substitution Principle (LSP). Почему и как лучше спроектировать — ниже. Почему нарушает LSP - У Rectangle клиент может ожидать возможность менять ширину и высоту независимо (например методы setWidth, setHeight или прямой доступ к полям). - Square вынужденно поддерживает инвариант width == height, поэтому поведение при независимом изменении сторон меняется и нарушает ожидания клиента. Пример нарушения (иллюстрация поведения): - Ожидание клиента: если сделать - setWidth(222), затем setHeight(333), то area должна быть 2⋅3=62\cdot 3=62⋅3=6. - Для Square (который при каждом сетте ставит обе стороны равными) реальный результат будет, например, 3⋅3=93\cdot 3=93⋅3=9 (или 2⋅2=42\cdot 2=42⋅2=4 в зависимости от порядка). Это несоответствие ожиданию — LSP нарушен. Как лучше спроектировать 1) Отделить интерфейс поведения от конкретных реализаций - Ввести интерфейс Shape (или Rectangular) с методом area(): - class Rectangle implements Shape { ... } - class Square implements Shape { ... } - Тогда Square не наследует мутабельное поведение Rectangle и не ломает ожидания. 2) Сделать объекты неизменяемыми (value objects) - Сделать Rectangle immutable (только конструктор + геттеры, без setWidth/setHeight). Тогда Square может наследовать Rectangle безопасно: - class Rectangle { final int width,height; Rectangle(int w,int h){...} int area(){ return width*height; } } - class Square extends Rectangle { Square(int s){ super(s,s); } } - При отсутствии мутаторов подкласс не ломает контракт — LSP соблюдается. 3) Композиция/фабрики вместо наследования - Если нужно представлять «прямоугольник, который может быть квадратом» — использовать фабрику: Rectangle.ofSquare(side) возвращает Rectangle с равными сторонами, но не наследовать Square от Rectangle. Резюме (рекомендация) - Если класс Rectangle предоставляет мутабельные методы изменения ширины/высоты — не делать Square наследником Rectangle. Используйте отдельные реализации интерфейса Shape или композицию. - Если нужен подкласс Square, делайте объекты immutable (тогда наследование безопасно).
Почему нарушает LSP
- У Rectangle клиент может ожидать возможность менять ширину и высоту независимо (например методы setWidth, setHeight или прямой доступ к полям).
- Square вынужденно поддерживает инвариант width == height, поэтому поведение при независимом изменении сторон меняется и нарушает ожидания клиента.
Пример нарушения (иллюстрация поведения):
- Ожидание клиента: если сделать
- setWidth(222), затем setHeight(333), то area должна быть 2⋅3=62\cdot 3=62⋅3=6.
- Для Square (который при каждом сетте ставит обе стороны равными) реальный результат будет, например, 3⋅3=93\cdot 3=93⋅3=9 (или 2⋅2=42\cdot 2=42⋅2=4 в зависимости от порядка). Это несоответствие ожиданию — LSP нарушен.
Как лучше спроектировать
1) Отделить интерфейс поведения от конкретных реализаций
- Ввести интерфейс Shape (или Rectangular) с методом area():
- class Rectangle implements Shape { ... }
- class Square implements Shape { ... }
- Тогда Square не наследует мутабельное поведение Rectangle и не ломает ожидания.
2) Сделать объекты неизменяемыми (value objects)
- Сделать Rectangle immutable (только конструктор + геттеры, без setWidth/setHeight). Тогда Square может наследовать Rectangle безопасно:
- class Rectangle { final int width,height; Rectangle(int w,int h){...} int area(){ return width*height; } }
- class Square extends Rectangle { Square(int s){ super(s,s); } }
- При отсутствии мутаторов подкласс не ломает контракт — LSP соблюдается.
3) Композиция/фабрики вместо наследования
- Если нужно представлять «прямоугольник, который может быть квадратом» — использовать фабрику: Rectangle.ofSquare(side) возвращает Rectangle с равными сторонами, но не наследовать Square от Rectangle.
Резюме (рекомендация)
- Если класс Rectangle предоставляет мутабельные методы изменения ширины/высоты — не делать Square наследником Rectangle. Используйте отдельные реализации интерфейса Shape или композицию.
- Если нужен подкласс Square, делайте объекты immutable (тогда наследование безопасно).