Chúng ta có hai class như sau:
class Foo {
String value = "foo";
public String getValue() {
return value;
}
}
class Bar extends Foo {
String value = "bar";
@Override
public String getValue() {
return value;
}
}
Hãy quan sát sự khác nhau giữa hai lời gọi truy cập sau đây:
Foo foo = new Bar();
System.out.println(foo.getValue()); // bar
System.out.println(foo.value); // foo
Chúng ta có hiểu biết thường thức rằng phương thức getValue() của class con sẽ ghi đè phương thức tương ứng của class cha, và chúng ta sẽ cho rằng điều tương tự sẽ xảy ra với thuộc tính của instance. Nhưng sự thực đã không như thế.
Chúng ta nhận được giá trị thuộc tính dựa theo kiểu của tham chiếu, không phải theo kiểu của instance. Nhưng tại sao?
Đa hình chỉ áp dựng cho phương thức, không áp dụng cho thuộc tính
Tại thời điểm compile, bytecode của các lời gọi phương thức sẽ là bytecode của class của tham chiếu. Tới thời điểm thực thi chương trình tất cả các bytecode đó sẽ được JVM thay thế bằng bytecode của class của instance. Chúng ta gọi cơ chế này là đa hình tại thời điểm thực thi (runtime).
Tương tự như vậy, tại thời điểm compile tất cả các lời gọi truy cập thuộc tính đều sẽ là bytecode của kiểu tham chiếu. Vấn đề là tại runtime… không có thêm điều gì xảy ra với các bytecode đó cả. Chúng ta gọi cơ chế này là đa hình chỉ áp dụng cho phương thức, không áp dụng cho thuộc tính.
Nhưng tại sao lại thế? Hay rõ hơn, tại sao các nhà thiết kế Java lại định hình như thế?
- Thứ nhất, việc tồn tại cơ chế ghi đè bytecode truy cập thuộc tính sẽ khiến các phương thức mà class con được kế thừa từ class cha bị “hỏng”, chẳng hạn trong trường hợp class con khai báo lại thuộc tính thành một kiểu dữ liệu khác.
- Thứ hai, ghi đè thuộc tính sẽ phế bỏ Nguyên tắc thay thế Liskov.
- Thứ ba, việc thiếu vắng khả năng ghi đè thuộc tính còn giúp chúng ta tạo ra các thiết kế phần mềm tốt hơn so với khi không có.
Kết luận
Thông thường không ai lại đi khai báo lại thuộc tính với cùng một tên. Ít khi có nhu cầu thực tế nào bắt buộc chúng ta phải làm như thế khi thiết kế cây kế thừa. Việc khiến thuộc tính bị trùng tên cũng sẽ dễ gây nhầm lẫn. Và cuối cùng, kể cả khi diều đó có xảy ra, chúng ta cũng sẽ không bao giờ gặp phải kết quả khó lường như trong ví dụ ở đầu bài viết này nếu chúng ta tận dụng triệt để tính chất bao gói. Cụ thể, giấu tất cả các thuộc tính ra khỏi khả năng truy cập trực tiếp từ bên ngoài và thay vào đó sử dụng các getter/setter sẽ giúp chúng ta viết ra phần mềm chạy giống như nó được viết hơn, dễ debug hơn, thiết kế đẹp hơn và “khỏe” hơn.
0 Lời bình