Loading content...
Having thoroughly analyzed why Square extends Rectangle violates LSP, we now face the practical question: How should we actually design these types?
There is no single 'correct' answer—the best design depends on your requirements, constraints, and the behaviors your system needs. This page presents multiple alternative designs, each with distinct trade-offs. By understanding this spectrum of solutions, you'll develop the judgment to choose appropriately for specific situations.
The solutions range from simple (don't use inheritance at all) to sophisticated (use immutability and factory patterns). Each teaches a different lesson about object-oriented design.
By the end of this page, you will know five distinct approaches to solving the Rectangle-Square problem, understand the trade-offs of each approach, and be able to choose the right design for your specific requirements. You'll also extract general principles applicable to similar design dilemmas.
Before diving into each solution, let's survey the landscape of approaches to fixing the Rectangle-Square problem:
| Approach | Core Idea | Complexity | When to Use |
|---|---|---|---|
| 1. No Inheritance | Square and Rectangle as separate, unrelated types | Simple | When you don't need polymorphism between them |
| 2. Common Interface | Both implement a shared Shape interface | Simple | When you need polymorphism for read-only operations |
| 3. Immutability | Make both types immutable; factory methods return new objects | Moderate | When immutability fits your architecture |
| 4. Remove Setters | Rectangle has no setters; dimensions set only at construction | Simple | When you don't need mutable shapes |
| 5. Rethink Hierarchy | Rectangle extends Square (inverted), or both extend abstract Shape | Moderate | When you need inheritance but want correctness |
Each approach addresses the LSP violation differently. Some avoid the problematic inheritance entirely; others preserve a type relationship while eliminating the behavioral incompatibility.
Let's examine each in detail.
The simplest solution is often the best: don't create an inheritance relationship at all.
Square and Rectangle are independent types. They share some conceptual properties (they're both quadrilaterals), but if the behavioral relationship is problematic, simply don't model it in code.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
/** * Solution 1: No Inheritance * * Rectangle and Square are completely separate types. * No shared type relationship beyond Object. */ public class Rectangle { private double width; private double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double getWidth() { return width; } public double getHeight() { return height; } public void setWidth(double w) { this.width = w; } public void setHeight(double h) { this.height = h; } public double getArea() { return width * height; } public double getPerimeter() { return 2 * (width + height); }} public class Square { private double side; public Square(double side) { this.side = side; } public double getSide() { return side; } public void setSide(double s) { this.side = s; } public double getArea() { return side * side; } public double getPerimeter() { return 4 * side; } // Optional: conversion method if needed public Rectangle toRectangle() { return new Rectangle(side, side); }} // Usage:// Rectangle and Square are used as distinct types.// No polymorphism, but no LSP violation either.Use this approach when you don't actually need to treat rectangles and squares polymorphically. Ask yourself: 'Do I ever need to write code that accepts both Rectangle and Square through a common interface?' If not, separate types are the cleanest solution.
If you need polymorphism for reading shape properties (getArea, getPerimeter, etc.) but not for mutating them, define a common read-only interface that both types implement.
The key insight: LSP violations occur in mutable operations. Read-only interfaces typically don't have this problem.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
/** * Solution 2: Common Read-Only Interface * * Both types implement a shared interface for read operations. * Mutation is handled type-specifically, not polymorphically. */ // Shared interface for read-only operationspublic interface Shape { double getArea(); double getPerimeter();} // Extension for shapes with width and heightpublic interface QuadrilateralShape extends Shape { double getWidth(); double getHeight();} // Rectangle implements the interfaces and adds its own setterspublic class Rectangle implements QuadrilateralShape { private double width; private double height; public Rectangle(double w, double h) { this.width = w; this.height = h; } @Override public double getWidth() { return width; } @Override public double getHeight() { return height; } @Override public double getArea() { return width * height; } @Override public double getPerimeter() { return 2 * (width + height); } // Mutators are NOT part of the interface public void setWidth(double w) { this.width = w; } public void setHeight(double h) { this.height = h; }} // Square implements the same interfacespublic class Square implements QuadrilateralShape { private double side; public Square(double s) { this.side = s; } @Override public double getWidth() { return side; } @Override public double getHeight() { return side; } @Override public double getArea() { return side * side; } @Override public double getPerimeter() { return 4 * side; } // Square-specific mutator public void setSide(double s) { this.side = s; }} // Now polymorphism works for reading:public class ShapeProcessor { public void printShapeInfo(Shape shape) { System.out.println("Area: " + shape.getArea()); System.out.println("Perimeter: " + shape.getPerimeter()); // Works with both Rectangle and Square! } public void compareShapes(QuadrilateralShape a, QuadrilateralShape b) { System.out.println("A dimensions: " + a.getWidth() + " x " + a.getHeight()); System.out.println("B dimensions: " + b.getWidth() + " x " + b.getHeight()); // Works with both Rectangle and Square! }}Why this works:
The shared interface only contains read-only operations. For read-only operations, Square behaves exactly like a Rectangle—it returns width, height, area, and perimeter correctly. The LSP violation was in the setters, which are now not part of the shared interface.
Mutation is done through type-specific methods (setWidth/setHeight for Rectangle, setSide for Square), so there's no possibility of using the wrong setter polymorphically.
This solution uses Interface Segregation Principle (ISP): separate the read interface from the write interface. Clients that only need to read shapes can use the Shape interface. Clients that need to modify shapes must work with specific types—but this is appropriate since the modification behavior differs.
The LSP violation stems from mutable setters with incompatible contracts. Remove mutability, and the problem disappears. With immutable types, 'modification' returns a new object rather than changing the existing one.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
/** * Solution 3: Immutable Types with Factory Methods * * All types are immutable. "Modification" creates new instances. * This eliminates the behavioral incompatibility. */ // Immutable interfacepublic interface ImmutableShape { double getArea(); double getPerimeter();} // Immutable Rectanglepublic final class ImmutableRectangle implements ImmutableShape { private final double width; private final double height; public ImmutableRectangle(double width, double height) { this.width = width; this.height = height; } public double getWidth() { return width; } public double getHeight() { return height; } public double getArea() { return width * height; } public double getPerimeter() { return 2 * (width + height); } // Factory methods return NEW objects with modified values // Note: Return type is ImmutableRectangle, NOT ImmutableShape public ImmutableRectangle withWidth(double newWidth) { return new ImmutableRectangle(newWidth, this.height); } public ImmutableRectangle withHeight(double newHeight) { return new ImmutableRectangle(this.width, newHeight); }} // Immutable Square - CAN extend ImmutableRectangle!public final class ImmutableSquare extends ImmutableRectangle { public ImmutableSquare(double side) { super(side, side); } public double getSide() { return getWidth(); } // Factory method for square-specific modification public ImmutableSquare withSide(double newSide) { return new ImmutableSquare(newSide); } // Override inherited methods - they now return Rectangle (not Square) // because modifying one dimension breaks the "squareness" @Override public ImmutableRectangle withWidth(double newWidth) { // Changing just width makes it no longer a square return new ImmutableRectangle(newWidth, getHeight()); } @Override public ImmutableRectangle withHeight(double newHeight) { // Changing just height makes it no longer a square return new ImmutableRectangle(getWidth(), newHeight); }} // Usage:public class ImmutableDemo { public static void main(String[] args) { ImmutableSquare square = new ImmutableSquare(5); // Using square as ImmutableRectangle - no LSP violation! // withWidth returns an ImmutableRectangle, which is correct ImmutableRectangle modified = square.withWidth(10); // modified is a 10x5 ImmutableRectangle (no longer square) System.out.println("Width: " + modified.getWidth()); // 10 System.out.println("Height: " + modified.getHeight()); // 5 // Original square unchanged System.out.println("Square: " + square.getSide()); // 5 }}Why immutability solves the problem:
ImmutableSquare.withWidth() returns ImmutableRectangle, not ImmutableSquare. This is semantically correct: modifying one dimension of a square makes it a rectangle.withWidth(10) returns a new rectangle with width=10. It doesn't claim the original object's height is unchanged—because the original is immutable and there's no 'after' state to check.ImmutableSquare anywhere ImmutableRectangle is expected. All inherited methods work correctly.When inheritance hierarchies have behavioral incompatibilities for mutators, immutability is often the cleanest solution. It sidesteps the problem by removing mutation entirely. Factory methods that return new objects can have any return type appropriate to the operation's semantics.
If immutability feels like overkill, a simpler variant is to simply remove the problematic setters from the base class. Rectangle can still be mutable, but the setters aren't part of its public contract.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
/** * Solution 4: Base Class Has No Setters * * The base class only exposes read operations. * Concrete types have their own modification APIs. */ // Base class with read-only public interfacepublic abstract class Shape { public abstract double getArea(); public abstract double getPerimeter();} // Rectangle is concrete with its own setterspublic class Rectangle extends Shape { private double width; private double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double getWidth() { return width; } public double getHeight() { return height; } @Override public double getArea() { return width * height; } @Override public double getPerimeter() { return 2 * (width + height); } // Rectangle-specific setters public void setWidth(double w) { this.width = w; } public void setHeight(double h) { this.height = h; }} // Square is also concrete with its own setterpublic class Square extends Shape { private double side; public Square(double side) { this.side = side; } public double getSide() { return side; } @Override public double getArea() { return side * side; } @Override public double getPerimeter() { return 4 * side; } // Square-specific setter public void setSide(double s) { this.side = s; }} // Note: Square does NOT extend Rectangle!// Both extend Shape, which has no setters.// No LSP violation because the modification operations// are not part of the shared contract (Shape). // Polymorphism still works for read operations:void processShape(Shape shape) { System.out.println("Area: " + shape.getArea()); // Works with both Rectangle and Square}Key distinction from Solution 2:
In Solution 2, we used interface for shared behavior. Here, we use an abstract class hierarchy. The effect is similar: the shared type (Shape) has no setters, so there's no behavioral incompatibility.
Notice that Square extends Shape directly, not Rectangle. This is intentional—we're not claiming Square IS-A Rectangle, because that claim is the source of the problem.
If you find that setters are causing LSP problems, ask whether they need to be part of the base class's contract at all. Often, mutation is specific to concrete types and shouldn't be in the shared abstraction. Push setters down to concrete types.
What if we really, truly need an inheritance relationship between mutable Rectangle and Square? One provocative solution is to invert the hierarchy: Rectangle extends Square.
This sounds backwards mathematically, but let's analyze it behaviorally:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
/** * Solution 5A: Inverted Hierarchy (Rectangle extends Square) * * Counterintuitive, but let's analyze the behavioral implications. */ // Square is the base classpublic class Square { protected double side; public Square(double side) { this.side = side; } public double getSide() { return side; } public void setSide(double s) { this.side = s; } public double getArea() { return side * side; } public double getPerimeter() { return 4 * side; }} // Rectangle extends Square, adding independent width/heightpublic class Rectangle extends Square { private double additionalWidth; // Extra width beyond the "square" dimension public Rectangle(double width, double height) { super(height); // The "side" is the height this.additionalWidth = width - height; } public double getWidth() { return side + additionalWidth; } public double getHeight() { return side; } public void setWidth(double w) { this.additionalWidth = w - side; } public void setHeight(double h) { double currentWidth = getWidth(); this.side = h; this.additionalWidth = currentWidth - h; } @Override public double getArea() { return getWidth() * getHeight(); } @Override public double getPerimeter() { return 2 * (getWidth() + getHeight()); }} // But wait... does this work?// Test: Can Rectangle substitute for Square? void clientCode(Square s) { s.setSide(10); double area = s.getArea(); // Expects 100 // For a Rectangle, getArea() returns width * height // which could be different from side * side // LSP VIOLATED AGAIN!}Analysis:
The inverted hierarchy also violates LSP! Here's why:
setSide(s) contract: After setSide(s), getArea() should return s * ssetSide(s), getArea() returns width * height, which isn't necessarily s * sThis shows that the problem isn't the direction of inheritance—it's that these two types have fundamentally incompatible behaviors regardless of which way you inherit.
Rectangle extends Square also violates LSP. The behavioral incompatibility is symmetric—neither type can substitute for the other. This reinforces that the real solution is to avoid inheritance between these incompatible types.
A better approach for Solution 5: If you truly need both types to share mutable behavior through inheritance, abstract the common parts into a parent class that neither can substitute for:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
/** * Solution 5B: Abstract Base Class * * Both types extend an abstract class, but neither extends the other. */ public abstract class Quadrilateral { public abstract double getArea(); public abstract double getPerimeter(); public abstract double getWidth(); public abstract double getHeight(); // No setters in the abstract class! // Each subclass defines its own mutation strategy.} public class Rectangle extends Quadrilateral { private double width, height; public Rectangle(double w, double h) { this.width = w; this.height = h; } @Override public double getWidth() { return width; } @Override public double getHeight() { return height; } @Override public double getArea() { return width * height; } @Override public double getPerimeter() { return 2 * (width + height); } // Rectangle-specific setters public void setWidth(double w) { this.width = w; } public void setHeight(double h) { this.height = h; }} public class Square extends Quadrilateral { private double side; public Square(double s) { this.side = s; } @Override public double getWidth() { return side; } @Override public double getHeight() { return side; } @Override public double getArea() { return side * side; } @Override public double getPerimeter() { return 4 * side; } // Square-specific setter public void setSide(double s) { this.side = s; }} // Now both can be used as Quadrilateral for read operations.// Mutation must be done through specific types.// No LSP violation because Quadrilateral has no mutation contract.With five different approaches available, how do you choose the right one for your situation? Here's a decision framework:
| Requirement | Recommended Solution |
|---|---|
| Don't need polymorphism between them | Solution 1: No Inheritance — Simplest, cleanest |
| Need polymorphism for reading only | Solution 2: Common Interface — Targeted polymorphism |
| System already uses immutability extensively | Solution 3: Immutability — Fits existing patterns |
| Shapes are set once and rarely/never modified | Solution 4: Remove Setters — Simplifies the contract |
| Need class hierarchy for other reasons | Solution 5B: Abstract Base — Hierarchy without LSP issues |
There's no universally 'correct' solution. The Rectangle-Square problem is a design choice, not a puzzle with one answer. Understand your requirements, apply the appropriate pattern, and document your reasoning for future maintainers.
We've explored five distinct approaches to resolving the Rectangle-Square LSP violation. Let's consolidate the key lessons:
What's next:
With this module complete, you now have a thorough understanding of the Rectangle-Square problem—the canonical LSP violation. You understand what the problem is, why it occurs (the clash between mathematical and behavioral inheritance), and how to solve it through various alternative designs.
The next modules in the LSP chapter will dive into preconditions and postconditions in more depth, exploring how to reason formally about behavioral contracts and how to design inheritance hierarchies that honor these contracts from the start.
You've mastered the Rectangle-Square problem—the most famous LSP violation in object-oriented programming. You understand the problem, the underlying theory, and practical solutions. You can now recognize similar patterns in your own code and apply appropriate design alternatives.