Chương 11:
Phiên bản hoá sử dụng new và override
Tác giả: Sưu tầm
Khái quát
Các dự án phần mềm hiếm khi tồn tại một phiên bản mã đơn mà không bao giờ được sửa đổi, trừ khi phần mềm không bao giờ được sinh ra. Trong hầu hết các trường hợp, người viết thư viện phần mềm muốn thay đổi một số thứ, và người sử dụng sẽ cần phải thích nghi với những thay đổi như vậy.
Việc giải quyết những vấn đề như vậy được biết đến như là sự phiên bản hoá, và nó là một trong những điều khó nhất để làm trong phần mềm. Một lý do tại sao nó lại khó khăn là vì nó đòi hỏi một kế hoạch và sự tiên đoán; những nơi mà có thể thay đổi phải được xác định, và thiết kế phải được sửa đổi để cho phép những thay đổi đó.
Một lý do khác tại sao phiên bản hoá là khó khăn là vì hầu hết các môi trường thực thi không cung cấp nhiều sự giúp đỡ cho người lập trình. Trong C++, mã được biên dịch có một hiểu biết nội tại về kích thước và cách trình bày tất cả các lớp được khắc sâu. Với sự thận trọng, vài sự sửa đổi có thể làm cho lớp mà không bắt buộc mọi người sử dụng phải biên dịch lại, nhưng những hạn chế là khá khốc liệt. Khi tính tương thích bị phá vỡ, tất cả người sử dụng cần phải biên dịch lại để sử dụng phiên bản mới. Điều này có thể không tồi tệ lắm, dù việc cài đặt một phiên bản mới của một thư viện có thể gây cho những ứng dụng khác đang sử dụng phiên bản cũ không hoạt động được.
Các môi trường được quản lý mà không trình bày thành phần lớp hay trình bày thông tin trong dữ liệu mêta là tốt hơn so với việc phiên bản hoá nhưng nó vẫn có thể viết mã mà được phiên bản nghèo nàn.
Một ví dụ phiên bản hoá
Đoạn mã sau đây thể hiện một kịch bản phiên bản hoá đơn giản. Chương trình sử dụng lớp Control, được cung cấp bởi một công ty khác.
}
public class MyControl: Control {
}
Trong phần cài đặt của MyControl, hàm ảo Foo() được thêm vào:
public class Control {
}
public class MyControl: Control {
public virtual void Foo() {}
}
Nó làm việc tốt, cho đến khi một thông báo nâng cấp đến từ các nhà cung cấp đối tượng Control. Thư viện mới bao gồm một hàm ảo Foo() trong đối tượng Control.
public class Control {
// newly added virtual
public virtual void Foo() {}
}
public class MyControl: Control {
public virtual void Foo() {}
}
Lớp Control này sử dụng Foo() như tên của một hàm chỉ là sự trùng hợp. Trong C++, trình biên dịch sẽ giả thiết rằng phiên bản của Foo() trong MyControl làm những gì mà một hàm chồng ảo của Foo() trong Control nên làm, và sẽ che lời gọi phiên bản trong MyControl.
Nó là xấu. Trong Java, điều này cũng sẽ xảy ra, nhưng những thứ có thể là khá tồi tệ; nếu một hàm ảo không có cùng tham số và kiểu trả về, trình nạp lớp sẽ cho rằng Foo() trong MyControl là một sự chồng hàm sai của hàm Foo() trong Control, và lớp sẽ lỗi khi nạp tại thời giạn chạy.
Đối với C# và .NET Runtime, một hàm được định nghĩa với virtual luôn luôn được xem là gốc của một trình gởi ảo. Nếu một hàm được giới thiệu trong một lớp cơ sở có thể được xem là một hàm ảo cơ sở của một hàm đã tồn tại, một hành vi thời gian chạy không được thay đổi.
Tuy nhiên, khi một lớp là được biên dịch tiếp theo, trình biên dịch sẽ phát sinh một cảnh báo, yêu cầu người lập trình xác định chủ ý phiên bản hoá của chúng. Trở lại với ví dụ, để chỉ rõ hành vi mặc định của việc không xem hàm là một sự chồng hàm tiếp theo, bổ từ new được thêm vào đầu hàm:
class Control {
public virtual void Foo() {}
}
class MyControl: Control {
// not an override
public new virtual void Foo() {}
}
Sự có mặt của new sẽ ngăn cản một cảnh báo.
Ngược lại, nếu một phiên bản dẫn xuất là một sự chồng hàm của một hàm trong lớp cơ sở, bổ từ override được sử dụng.
class Control {
public virtual void Foo() {}
}
class MyControl: Control {
// an override for Control.Foo()
public override void Foo() {}
}
Điều này báo cho trình biên dịch rằng hàm đó thật sự là một sự chồng hàm.
Chú ý Khoảng thời gian này, có một số người trong góc tối của căn phòng nghĩ rằng, “Tôi chỉ cần đặt new trong tất cả các hàm ảo của tôi, và sau đó tôi sẽ không bao giờ phải gặp với tình huống đó nữa”. Làm như vậy là nản chí bởi vì nó hạn chế bớt giá trị mà một chú giải mới phải có cho một số người đọc mã. Nếu new chỉ được sử dụng khi cần thiết, người đọc có thể tìm lớp cơ sở và hiểu hàm không được chồng. Nếu new được sử dụng không phân biệt, người sử dụng phải tham khảo đến lớp cơ sở thường xuyên để xem new có ý gì.