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
Birdhứa rằngfly()sẽ hoạt động bình thường - Không thể substitute: Không thể thay thế
BirdbằngPenguinmộ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:¶
- Preconditions không được mạnh hóa trong subclass
- Postconditions không được yếu hóa trong subclass
- Invariants của base class phải được bảo toàn
- 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!