Bỏ qua

Liskov Substitution Principle (LSP)

Liskov Substitution Principle là nguyên lý thứ ba trong bộ nguyên lý SOLID, được đặt theo tên nhà khoa học máy tính Barbara Liskov. Nguyên lý này quy định:

  • Class con có thể thay thế được class cha mà không làm hỏng chương trình
  • Giống như "con trai có thể làm được những việc bố làm" - nhưng có thể làm tốt hơn hoặc theo cách riêng

Định nghĩa chính thức

Nếu S là subtype của T, thì các object của type T có thể được thay thế bằng objects của type S mà không làm thay đổi tính đúng đắn của chương trình.

Tại sao LSP quan trọng?

  • Đảm bảo tính nhất quán trong inheritance hierarchy
  • Tăng độ tin cậy của polymorphism
  • Giảm bugs do vi phạm contract của base class
  • Cải thiện maintainability và extensibility

Ví dụ vi phạm LSP

Đây là một ví dụ SAI - vi phạm nguyên lý LSP:

class Bird {
    public void fly() {
        System.out.println("Chim đang bay");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        // Chim cánh cụt không bay được!
        throw new RuntimeException("Chim cánh cụt không thể bay!");
    }
}

// Sử dụng
public static void makeBirdFly(Bird bird) {
    bird.fly(); // Sẽ lỗi nếu bird là Penguin!
}

public static void main(String[] args) {
    Bird sparrow = new Bird();
    Bird penguin = new Penguin();

    makeBirdFly(sparrow); // OK: "Chim đang bay"
    makeBirdFly(penguin); // CRASH: RuntimeException!
}

Vấn đề nghiêm trọng:

  • Vi phạm contract: Base class Bird hứa rằng fly() sẽ hoạt động bình thường
  • Không thể substitute: Không thể thay thế Bird bằng Penguin một cách an toàn
  • Runtime errors: Chương trình crash khi gặp subclass Penguin
  • Kiểm tra type: Buộc phải kiểm tra type trước khi gọi method
  • Khó maintain: Thêm bird mới có thể gây ra bugs

Giải pháp tuân thủ LSP

Thiết kế lại hierarchy để tuân thủ LSP:

Cách 1: Abstract base class với method chung

abstract class Bird {
    public abstract void move();

    // Common behavior cho tất cả birds
    public void eat() {
        System.out.println("Chim đang ăn");
    }
}

class FlyingBird extends Bird {
    @Override
    public void move() {
        fly();
    }

    public void fly() {
        System.out.println("Chim đang bay");
    }
}

class SwimmingBird extends Bird {
    @Override
    public void move() {
        swim();
    }

    public void swim() {
        System.out.println("Chim đang bơi");
    }
}

// Concrete implementations
class Sparrow extends FlyingBird {
    @Override
    public void fly() {
        System.out.println("Chim sẻ đang bay nhanh");
    }
}

class Penguin extends SwimmingBird {
    @Override
    public void swim() {
        System.out.println("Chim cánh cụt đang bơi giỏi");
    }
}

Cách 2: Interface segregation

interface Movable {
    void move();
}

interface Flyable extends Movable {
    void fly();

    default void move() {
        fly();
    }
}

interface Swimmable extends Movable {
    void swim();

    default void move() {
        swim();
    }
}

abstract class Bird implements Movable {
    public void eat() {
        System.out.println("Chim đang ăn");
    }
}

class Eagle extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Đại bàng đang bay cao");
    }
}

class Penguin extends Bird implements Swimmable {
    @Override
    public void swim() {
        System.out.println("Chim cánh cụt đang bơi");
    }
}

// Một số chim có thể vừa bay vừa bơi
class Duck extends Bird implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("Vịt đang bay");
    }

    @Override
    public void swim() {
        System.out.println("Vịt đang bơi");
    }

    @Override
    public void move() {
        // Duck có thể chọn cách di chuyển
        if (isInWater()) {
            swim();
        } else {
            fly();
        }
    }

    private boolean isInWater() {
        // Logic kiểm tra môi trường
        return true; // Simplified
    }
}

Sử dụng an toàn:

public class BirdManager {
    // Method hoạt động với tất cả birds
    public static void makeBirdMove(Bird bird) {
        bird.move(); // An toàn với mọi subclass
    }

    // Method chỉ hoạt động với birds bay được
    public static void makeBirdFly(Flyable bird) {
        bird.fly(); // Type-safe
    }

    // Method chỉ hoạt động với birds bơi được  
    public static void makeBirdSwim(Swimmable bird) {
        bird.swim(); // Type-safe
    }

    public static void main(String[] args) {
        Bird eagle = new Eagle();
        Bird penguin = new Penguin();
        Bird duck = new Duck();

        // Tất cả đều hoạt động tốt
        makeBirdMove(eagle);   // "Đại bàng đang bay cao"
        makeBirdMove(penguin); // "Chim cánh cụt đang bơi"
        makeBirdMove(duck);    // "Vịt đang bơi" hoặc "Vịt đang bay"

        // Type-safe operations
        makeBirdFly((Flyable) eagle); // OK
        makeBirdSwim((Swimmable) penguin); // OK

        // Compiler sẽ báo lỗi nếu cast sai type
        // makeBirdFly((Flyable) penguin); // Compile error!
    }
}

Nguyên tắc kiểm tra LSP

Điều kiện để tuân thủ LSP:

  1. Preconditions không được mạnh hóa trong subclass
  2. Postconditions không được yếu hóa trong subclass
  3. Invariants của base class phải được bảo toàn
  4. History constraint: Subclass không được thay đổi state mà base class không cho phép

Dấu hiệu vi phạm LSP:

  • Throw exceptions trong subclass khi base class không throw
  • Strengthen preconditions: Yêu cầu input nghiêm ngặt hơn
  • Weaken postconditions: Trả về kết quả kém chất lượng hơn
  • Cần type checking trước khi gọi method
  • Empty implementations hoặc do-nothing methods

Ví dụ thực tế khác

Rectangle vs Square Problem

Sai:

class Rectangle {
    protected int width, height;

    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = this.height = width; // Vi phạm behavior
    }

    @Override
    public void setHeight(int height) {
        this.width = this.height = height; // Vi phạm behavior  
    }
}

Đúng:

abstract class Shape {
    public abstract int getArea();
}

class Rectangle extends Shape {
    private int width, height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea() { return width * height; }
}

class Square extends Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    public int getArea() { return side * side; }
}


Lợi ích của việc tuân thủ LSP

Polymorphism đáng tin cậy

  • Có thể sử dụng subclass thay cho base class một cách an toàn
  • Không cần type checking hay special handling

Code reusability cao

  • Method hoạt động với base class sẽ hoạt động với tất cả subclass
  • Giảm duplicate code

Maintainability tốt

  • Thêm subclass mới không ảnh hưởng code hiện tại
  • Behavior consistent và predictable

Testing dễ dàng

  • Test base class behavior cũng test được subclass behavior
  • Không cần test case đặc biệt cho từng subclass

Kết luận

Liskov Substitution Principle đảm bảo rằng:

  • Inheritance hierarchy được thiết kế đúng đắn
  • Polymorphism hoạt động đáng tin cậy
  • Subclass thực sự "IS-A" relationship với base class
  • Code ổn định và dễ maintain

Nhớ rằng: "Objects of a superclass should be replaceable with objects of its subclasses without breaking the application" - đây là nền tảng cho một thiết kế OOP tốt!