มันหลีกเลี่ยงเปราะบางปัญหาคลาสฐาน ทุกชั้นเรียนมาพร้อมกับชุดของการรับประกันโดยนัยหรือชัดเจนและไม่เปลี่ยนแปลง หลักการชดเชย Liskov สั่งว่าประเภทย่อยทั้งหมดของชั้นนั้นจะต้องให้การค้ำประกันเหล่านี้ทั้งหมด final
แต่ก็เป็นเรื่องง่ายที่จะละเมิดนี้ถ้าเราไม่ได้ใช้ ตัวอย่างเช่นมีตัวตรวจสอบรหัสผ่าน:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
หากเราอนุญาตให้คลาสนั้นถูกแทนที่การใช้งานอย่างหนึ่งอาจปิดกั้นทุกคนและอีกอันอาจให้ทุกคนเข้าถึงได้:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
สิ่งนี้มักจะไม่ตกลงเนื่องจากคลาสย่อยมีพฤติกรรมที่เข้ากันไม่ได้กับต้นฉบับ ถ้าเราตั้งใจให้ชั้นเรียนขยายออกไปพร้อมกับพฤติกรรมอื่น ๆ โซ่แห่งความรับผิดชอบจะดีกว่า:
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;
}
}
ปัญหาจะชัดเจนขึ้นเมื่อชนชั้นที่ซับซ้อนเรียกวิธีการของตนเองและวิธีการเหล่านั้นสามารถแทนที่ได้ บางครั้งฉันพบสิ่งนี้เมื่อพิมพ์โครงสร้างข้อมูลหรือเขียน HTML แต่ละวิธีมีหน้าที่รับผิดชอบวิดเจ็ตบางอย่าง
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>");
}
...
}
ตอนนี้ฉันสร้างคลาสย่อยที่เพิ่มการกำหนดสไตล์อีกเล็กน้อย:
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>");
}
}
ตอนนี้ไม่สนใจสักครู่ว่านี่ไม่ใช่วิธีที่ดีในการสร้างหน้า HTML เกิดอะไรขึ้นถ้าฉันต้องการเปลี่ยนเค้าโครงอีกครั้ง ฉันต้องสร้างSpiffyPage
คลาสย่อยที่มีเนื้อหาอย่างใดอย่างหนึ่งล้อม สิ่งที่เราสามารถดูที่นี่เป็นอุบัติเหตุการประยุกต์ใช้รูปแบบวิธีการแม่แบบ วิธีการเทมเพลตเป็นจุดส่วนขยายที่กำหนดไว้อย่างดีในคลาสฐานที่ตั้งใจจะเขียนทับ
และจะเกิดอะไรขึ้นถ้าคลาสพื้นฐานเปลี่ยนไป? หากเนื้อหา HTML เปลี่ยนแปลงมากเกินไปสิ่งนี้อาจทำให้เค้าโครงย่อยที่คลาสย่อยจัดเตรียมไว้ให้ ดังนั้นจึงไม่ปลอดภัยที่จะเปลี่ยนคลาสฐานหลังจากนั้น สิ่งนี้ไม่ชัดเจนหากคลาสทั้งหมดของคุณอยู่ในโครงการเดียวกัน แต่จะสังเกตได้ชัดเจนมากถ้าคลาสพื้นฐานเป็นส่วนหนึ่งของซอฟต์แวร์ที่เผยแพร่ซึ่งผู้อื่นสร้างขึ้น
หากมีการใช้กลยุทธ์ส่วนขยายนี้เราสามารถอนุญาตให้ผู้ใช้สลับวิธีสร้างแต่ละส่วนได้ อาจมีกลยุทธ์สำหรับแต่ละบล็อกที่สามารถให้ได้จากภายนอก หรือเราอาจทำรังมัณฑนากร สิ่งนี้จะเทียบเท่ากับโค้ดด้านบน แต่มีความชัดเจนและยืดหยุ่นมากกว่า:
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
พารามิเตอร์เพิ่มเติมมีความจำเป็นเพื่อให้แน่ใจว่าการโทรwriteMainContent
ผ่านด้านบนของห่วงโซ่มัณฑนากรนี่เป็นการจำลองคุณลักษณะของคลาสย่อยที่เรียกว่าการเรียกซ้ำแบบเปิด )
หากเรามีนักตกแต่งหลายคนเราสามารถผสมได้อย่างอิสระมากขึ้น
บ่อยกว่าความปรารถนาที่จะปรับฟังก์ชั่นที่มีอยู่เล็กน้อยคือความปรารถนาที่จะนำส่วนหนึ่งของคลาสที่มีอยู่กลับมาใช้ใหม่ ฉันได้เห็นกรณีที่มีคนต้องการชั้นเรียนที่คุณสามารถเพิ่มรายการและทำซ้ำได้ทั้งหมด ทางออกที่ถูกต้องคือ:
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
}
แต่พวกเขาสร้างคลาสย่อยแทน:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
ซึ่งหมายความว่าอินเทอร์เฟซทั้งหมดของArrayList
กลายเป็นส่วนหนึ่งของอินเทอร์เฟซของเรา ผู้ใช้สามารถทำremove()
สิ่งต่าง ๆ หรือget()
สิ่งต่าง ๆ ที่ดัชนีเฉพาะ นี่มันตั้งใจอย่างนั้นเหรอ? ตกลง. แต่บ่อยครั้งเราไม่ได้คิดอย่างรอบคอบถึงผลที่จะตามมาทั้งหมด
ดังนั้นจึงแนะนำให้
- ไม่เคย
extend
เรียนโดยไม่ต้องคิดอย่างระมัดระวัง
- ทำเครื่องหมายชั้นเรียนของคุณเสมอว่า
final
ถ้าคุณต้องการให้วิธีการใด ๆ ถูกแทนที่
- สร้างอินเตอร์เฟสที่คุณต้องการสลับการใช้งานเช่นสำหรับการทดสอบหน่วย
มีตัวอย่างมากมายที่“ กฎ” นี้จะต้องถูกทำลาย แต่โดยปกติแล้วจะแนะนำคุณถึงการออกแบบที่ดีมีความยืดหยุ่นและหลีกเลี่ยงข้อผิดพลาดเนื่องจากการเปลี่ยนแปลงที่ไม่ได้ตั้งใจในคลาสฐาน (หรือการใช้คลาสย่อยโดยไม่ได้ตั้งใจ) )
บางภาษามีกลไกการบังคับใช้ที่เข้มงวด:
- วิธีการทั้งหมดเป็นที่สิ้นสุดโดยค่าเริ่มต้นและจะต้องมีการทำเครื่องหมายอย่างชัดเจนว่า
virtual
- พวกเขาจัดให้มีการสืบทอดส่วนตัวที่ไม่ได้รับส่วนต่อประสาน แต่มีเพียงการนำไปใช้งาน
- พวกเขาต้องการวิธีการเรียนพื้นฐานที่จะทำเครื่องหมายเป็นเสมือนจริงและต้องการให้มีการทำเครื่องหมายแทนที่ทั้งหมดเช่นกัน วิธีนี้จะช่วยหลีกเลี่ยงปัญหาที่คลาสย่อยกำหนดวิธีการใหม่ แต่มีการเพิ่มวิธีที่มีลายเซ็นเดียวกันในคลาสพื้นฐาน แต่ไม่ได้ตั้งใจให้เป็นเสมือน
final
? หลายคน (รวมทั้งผม)final
พบว่ามันเป็นการออกแบบที่ดีที่จะทำให้ทุกระดับไม่ใช่นามธรรม