Phải thay thế được!

Nguyên tắc này nói rằng, nếu một lớp A là con của lớp B, thì chúng ta có thay thế lớp A cho lớp B mà không làm cho chương trình bị phá huỷ, và ở trong hình minh hoạ thì để di chuyển thì chúng ta có thể chọn bất cứ loại xe ô tô nào chúng ta muốn mà vẫn đảm bảo được tốc độ và khả năng vận chuyển. Và thật thú vị là nguyên tắc này được giới thiệu bởi một người phụ nữ có tên Barbara Liskov. Trên thực tế thì nguyên tắc này chúng ta vẫn tuân thủ hàng ngày mà không nhận ra, tuy nhiên cũng có trường hợp chúng ta vi phạm nguyên tắc này một cách vô ý.

Bài toán thực tế

Có lẽ trong thời đại của microservices như hiện nay thì rất có khả năng chúng ta phải lấy dữ liệu từ nhiều nguồn khác nhau, có thể là từ MongoDB hay MySQL, hãy nói chúng ta có một interface dùng chung thế này:

public interface DatabaseRepo<I, E> {

    void save(E entity);

    E findById(I id);

    long increase(String field);
}

Và chúng ta có 1 lớp sử dụng kiểu thế này:

public class UserService {

    private DatabaseRepo<Long, User> userRepo;
    private DatabaseRepo<String, IdProvider> idProvider;

    public void save(User user) {
        long userId = userRepo.increase("userId", "maxId");
        user.setId(userId);
        userRepo.save(user);
    }
}

Phân tích vấn đề

Trong đoạn chương trình ở trên, chúng ta tạo ra userId tăng dần, set nó vào đối tượng user và save xuống database. Nhưng vấn đề ở đây là với MongoDB thì chúng ta có toán tử $inc trong khi MySQL thì không, điều này dẫn đến việc nếu chúng ta tạo ra 1 lớp thế này:

public class MySQLDatabaseRepo<I, E> implements DatabaseRepo<I, E> {
    @Override
    public long increase(String id, String field) {
        throw new UnsupportedOperationException("unsupported");
    }
}

Và chúng ta set nó vào lớp UserService nó sẽ làm hỏng chương trình của chúng ta, cụ thể là nghiệp vụ save user này.

Giải pháp

Để chương trình của chúng ta hoạt động bình thường thì ở lớp MySQLDatabaseRepo hàm increase sẽ không được phép ném ra UnsupportedOperationException. Và rất may cho chúng ta là MySQL có hỗ trợ auto increment id, vậy nên chúng ta sẽ chỉ cần thay đổi lớp MySQLDatabaseRepo thế này là được:

public class MySQLDatabaseRepo<I, E> implements DatabaseRepo<I, E> {
    @Override
    public long increase(String id, String field) {
        return 0;
    }
}

Hoặc chúng ta cũng có thể tạo 1 procedure ở MySQL để tạo ra id tăng dần, lúc đó lớp MySQLDatabaseRepo sẽ kiểu thế này:

public class MySQLDatabaseRepo<I, E> implements DatabaseRepo<I, E> {
    @Override
    public long increase(String id, String field) {
        return callProcedue(id, field).getResult();
    }
}

Nhìn chung sẽ không có 1 giải pháp chung cho mọi tình huống, chính vì vậy, dựa vào logic của chương trình mà chúng ta sẽ đưa ra cài đặt phù hợp.

Tổng kết

Việc nâng cấp, thay thế hay sử dụng chung các lớp có thể sẽ xảy ra, và nhiệm vụ của chúng ta là phải đảm bảo cho mọi thứ hoạt động tốt giống như trước đó bằng cách áp dụng nguyên tắc Liskov Substitution này. Tuy nhiên để đảm bảo được nguyên tắc này hoạt động hiệu quả, chúng ta cũng sẽ cần sử dụng đên unit test và integration test để phát hiện các logic bất thường trong code của chúng ta trước khi trên khai lên các môi trường chạy thực tế.