Nó tránh được vấn đề Lớp cơ sở mong manh . Mỗi lớp đi kèm với một tập hợp các bảo đảm và bất biến rõ ràng hoặc rõ ràng. Nguyên tắc thay thế Liskov quy định rằng tất cả các kiểu con của lớp đó cũng phải cung cấp tất cả các bảo đảm này. Tuy nhiên, rất dễ vi phạm điều này nếu chúng ta không sử dụng final
. Ví dụ: hãy kiểm tra mật khẩu:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
Nếu chúng ta cho phép lớp đó bị ghi đè, một triển khai có thể khóa tất cả mọi người, một lớp khác có thể cấp cho mọi người quyền truy cập:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
Điều này thường không ổn, vì các lớp con bây giờ có hành vi rất không tương thích với bản gốc. Nếu chúng tôi thực sự có ý định mở rộng lớp học với các hành vi khác, Chuỗi trách nhiệm sẽ tốt hơn:
PasswordChecker passwordChecker =
new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
new DefaultPasswordChecker(
new OpenDoor(null)
);
public interface PasswordChecker {
boolean passwordIsOk(String password);
}
public final class DefaultPasswordChecker implements PasswordChecker {
private PasswordChecker next;
public DefaultPasswordChecker(PasswordChecker next) {
this.next = next;
}
@Override
public boolean passwordIsOk(String password) {
if ("s3cret".equals(password)) return true;
if (next != null) return next.passwordIsOk(password);
return false;
}
}
public final class OpenDoor implements PasswordChecker {
private PasswordChecker next;
public OpenDoor(PasswordChecker next) {
this.next = next;
}
@Override
public boolean passwordIsOk(String password) {
return true;
}
}
Vấn đề trở nên rõ ràng hơn khi một lớp phức tạp hơn gọi các phương thức của chính nó và các phương thức đó có thể được ghi đè. Đôi khi tôi gặp phải điều này khi in một cấu trúc dữ liệu hoặc viết HTML. Mỗi phương pháp chịu trách nhiệm cho một số widget.
public class Page {
...;
@Override
public String toString() {
PrintWriter out = ...;
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("</head>");
out.print("<body>");
writeHeader(out);
writeMainContent(out);
writeMainFooter(out);
out.print("</body>");
out.print("</html>");
...
}
void writeMainContent(PrintWriter out) {
out.print("<div class='article'>");
out.print(htmlEscapedContent);
out.print("</div>");
}
...
}
Bây giờ tôi tạo một lớp con có thêm một chút kiểu dáng:
class SpiffyPage extends Page {
...;
@Override
void writeMainContent(PrintWriter out) {
out.print("<div class='row'>");
out.print("<div class='col-md-8'>");
super.writeMainContent(out);
out.print("</div>");
out.print("<div class='col-md-4'>");
out.print("<h4>About the Author</h4>");
out.print(htmlEscapedAuthorInfo);
out.print("</div>");
out.print("</div>");
}
}
Bây giờ bỏ qua một chút rằng đây không phải là một cách rất tốt để tạo các trang HTML, điều gì xảy ra nếu tôi muốn thay đổi bố cục một lần nữa? Tôi phải tạo một SpiffyPage
lớp con bằng cách nào đó kết thúc nội dung đó. Những gì chúng ta có thể thấy ở đây là một ứng dụng tình cờ của mẫu phương thức mẫu. Các phương thức mẫu là các điểm mở rộng được xác định rõ trong một lớp cơ sở được dự định ghi đè.
Và điều gì xảy ra nếu lớp cơ sở thay đổi? Nếu nội dung HTML thay đổi quá nhiều, điều này có thể phá vỡ bố cục được cung cấp bởi các lớp con. Do đó, không thực sự an toàn để thay đổi lớp cơ sở sau đó. Điều này không rõ ràng nếu tất cả các lớp của bạn nằm trong cùng một dự án, nhưng rất đáng chú ý nếu lớp cơ sở là một phần của một số phần mềm được xuất bản mà người khác xây dựng.
Nếu chiến lược mở rộng này được dự định, chúng tôi có thể cho phép người dùng trao đổi cách thức mỗi phần được tạo. Hoặc, có thể có một Chiến lược cho mỗi khối có thể được cung cấp bên ngoài. Hoặc, chúng ta có thể làm tổ trang trí. Điều này sẽ tương đương với mã trên, nhưng rõ ràng hơn và linh hoạt hơn nhiều:
Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());
public interface PageLayout {
void writePage(PrintWriter out, PageLayout top);
void writeMainContent(PrintWriter out, PageLayout top);
...
}
public final class Page {
private PageLayout layout = new DefaultPageLayout();
public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
layout = wrapper.apply(layout);
}
...
@Override public String toString() {
PrintWriter out = ...;
layout.writePage(out, layout);
...
}
}
public final class DefaultPageLayout implements PageLayout {
@Override public void writeLayout(PrintWriter out, PageLayout top) {
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("</head>");
out.print("<body>");
top.writeHeader(out, top);
top.writeMainContent(out, top);
top.writeMainFooter(out, top);
out.print("</body>");
out.print("</html>");
}
@Override public void writeMainContent(PrintWriter out, PageLayout top) {
... /* as above*/
}
}
public final class SpiffyPageDecorator implements PageLayout {
private PageLayout inner;
public SpiffyPageDecorator(PageLayout inner) {
this.inner = inner;
}
@Override
void writePage(PrintWriter out, PageLayout top) {
inner.writePage(out, top);
}
@Override
void writeMainContent(PrintWriter out, PageLayout top) {
...
inner.writeMainContent(out, top);
...
}
}
( top
Tham số bổ sung là cần thiết để đảm bảo rằng các lệnh gọi writeMainContent
đi qua đầu chuỗi trang trí. Điều này mô phỏng một tính năng của phân lớp được gọi là đệ quy mở .)
Nếu chúng ta có nhiều trang trí, bây giờ chúng ta có thể trộn chúng tự do hơn.
Thường xuyên hơn nhiều so với mong muốn điều chỉnh một chút chức năng hiện có là mong muốn sử dụng lại một phần của một lớp hiện có. Tôi đã thấy một trường hợp ai đó muốn một lớp học nơi bạn có thể thêm các mục và lặp lại trên tất cả chúng. Giải pháp chính xác sẽ là:
final class Thingies implements Iterable<Thing> {
private ArrayList<Thing> thingList = new ArrayList<>();
@Override public Iterator<Thing> iterator() {
return thingList.iterator();
}
public void add(Thing thing) {
thingList.add(thing);
}
... // custom methods
}
Thay vào đó, họ đã tạo một lớp con:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
Điều này đột nhiên có nghĩa là toàn bộ giao diện ArrayList
đã trở thành một phần của giao diện của chúng tôi . Người dùng có thể remove()
mọi thứ, hoặc get()
những thứ tại các chỉ số cụ thể. Điều này đã được dự định theo cách đó? ĐỒNG Ý. Nhưng thông thường, chúng ta không cẩn thận suy nghĩ về tất cả các hậu quả.
Do đó, nên
- không bao giờ
extend
một lớp học mà không suy nghĩ cẩn thận.
- luôn luôn đánh dấu các lớp của bạn là
final
ngoại trừ nếu bạn có ý định cho bất kỳ phương thức nào bị ghi đè.
- tạo giao diện nơi bạn muốn trao đổi một triển khai, ví dụ để thử nghiệm đơn vị.
Có rất nhiều ví dụ về quy tắc này mà Bẻ khóa này phải bị phá vỡ, nhưng nó thường hướng dẫn bạn một thiết kế linh hoạt, tốt và tránh các lỗi do những thay đổi ngoài ý muốn trong các lớp cơ sở (hoặc sử dụng vô ý của lớp con làm ví dụ của lớp cơ sở ).
Một số ngôn ngữ có cơ chế thực thi chặt chẽ hơn:
- Tất cả các phương thức là cuối cùng theo mặc định và phải được đánh dấu rõ ràng là
virtual
- Họ cung cấp quyền thừa kế riêng không kế thừa giao diện mà chỉ thực hiện.
- Chúng yêu cầu các phương thức lớp cơ sở được đánh dấu là ảo và yêu cầu tất cả các phần ghi đè cũng được đánh dấu. Điều này tránh các vấn đề trong đó một lớp con xác định một phương thức mới, nhưng một phương thức có cùng chữ ký sau đó đã được thêm vào lớp cơ sở nhưng không nhằm mục đích ảo.
final
? Nhiều người (bao gồm cả tôi) thấy rằng đó là một thiết kế tốt để tạo ra mọi lớp không trừu tượngfinal
.