--- id: solid-lsp-contracts title: SOLID - Liskov Substitution (Contracts) category: solid-principles priority: critical tags: [SOLID, LSP, liskov-substitution, contracts] related: [solid-lsp-preconditions, solid-ocp-abstraction, core-composition] --- # Liskov Substitution Principle - Contracts Subtypes must be substitutable for their base types without altering the correctness of the program. Derived classes must honor the contracts established by their base classes. ## Bad Example ```typescript // Anti-pattern: Subclass violates the contract of the base class class Rectangle { protected _width: number; protected _height: number; constructor(width: number, height: number) { this._width = width; this._height = height; } get width(): number { return this._width; } set width(value: number) { this._width = value; } get height(): number { return this._height; } set height(value: number) { this._height = value; } getArea(): number { return this._width * this._height; } } // Square violates LSP - it changes the behavior of setters class Square extends Rectangle { constructor(side: number) { super(side, side); } // Violates LSP: setter has different behavior than parent set width(value: number) { this._width = value; this._height = value; // Unexpected side effect! } set height(value: number) { this._width = value; // Unexpected side effect! this._height = value; } } // This code works with Rectangle but breaks with Square function resizeRectangle(rect: Rectangle): void { rect.width = 10; rect.height = 5; // Expects area to be 50, but Square gives 25! console.assert(rect.getArea() === 50, 'Area should be 50'); } const rectangle = new Rectangle(4, 4); resizeRectangle(rectangle); // Works: area is 50 const square = new Square(4); resizeRectangle(square); // Fails: area is 25, not 50 ``` ## Good Example ```typescript // Correct approach: Use composition and proper abstractions // Define what shapes can do interface Shape { getArea(): number; getPerimeter(): number; } // Define what resizable shapes can do interface ResizableShape extends Shape { scale(factor: number): void; } // Immutable rectangle - no setters that could be violated class Rectangle implements Shape { constructor( readonly width: number, readonly height: number ) { if (width <= 0 || height <= 0) { throw new Error('Dimensions must be positive'); } } getArea(): number { return this.width * this.height; } getPerimeter(): number { return 2 * (this.width + this.height); } // Return new instance instead of mutating withWidth(width: number): Rectangle { return new Rectangle(width, this.height); } withHeight(height: number): Rectangle { return new Rectangle(this.width, height); } scale(factor: number): Rectangle { return new Rectangle(this.width * factor, this.height * factor); } } // Square is its own shape, not a subtype of Rectangle class Square implements Shape { constructor(readonly side: number) { if (side <= 0) { throw new Error('Side must be positive'); } } getArea(): number { return this.side * this.side; } getPerimeter(): number { return 4 * this.side; } withSide(side: number): Square { return new Square(side); } scale(factor: number): Square { return new Square(this.side * factor); } } // Functions work with the Shape interface function printShapeInfo(shape: Shape): void { console.log(`Area: ${shape.getArea()}`); console.log(`Perimeter: ${shape.getPerimeter()}`); } // This works correctly with both Rectangle and Square const rect = new Rectangle(10, 5); printShapeInfo(rect); // Area: 50, Perimeter: 30 const square = new Square(5); printShapeInfo(square); // Area: 25, Perimeter: 20 // For operations specific to rectangles, use Rectangle type function createBanner(width: number, height: number): Rectangle { return new Rectangle(width, height); } // For operations that work with any shape, use Shape interface function calculateTotalArea(shapes: Shape[]): number { return shapes.reduce((total, shape) => total + shape.getArea(), 0); } const shapes: Shape[] = [ new Rectangle(10, 5), new Square(4), new Rectangle(3, 7) ]; console.log(calculateTotalArea(shapes)); // 50 + 16 + 21 = 87 ``` ## Why 1. **Predictable Behavior**: Code using the base type works correctly with any subtype. No surprises. 2. **Safe Polymorphism**: You can pass any `Shape` to functions expecting `Shape` without checking the concrete type. 3. **Contract Honoring**: Each class fully honors its interface contract - `getArea()` always returns the correct area. 4. **Immutability Benefits**: By making shapes immutable, we avoid the setter problem entirely. 5. **Proper Modeling**: Square and Rectangle are separate concepts that happen to share behavior (Shape), not a parent-child relationship. 6. **Easier Testing**: Tests for `Shape` work for all implementations. No special cases needed. 7. **Design Clarity**: The inheritance hierarchy reflects true "is-a" relationships, not just code reuse.