ทำไมคำหลัก 'สุดท้าย' จึงมีประโยชน์


54

ดูเหมือนว่า Java มีอำนาจในการประกาศคลาสที่ไม่สามารถสืบทอดได้สำหรับทุกวัยและตอนนี้ C ++ ก็มีเช่นกัน อย่างไรก็ตามในแง่ของหลักการ Open / Close ใน SOLID ทำไมมันถึงมีประโยชน์? สำหรับฉันfinalคำหลักฟังดูเหมือนfriend- ถูกกฎหมาย แต่ถ้าคุณใช้งานอยู่การออกแบบส่วนใหญ่อาจผิด โปรดให้ตัวอย่างที่คลาสที่ไม่สามารถสืบทอดได้จะเป็นส่วนหนึ่งของสถาปัตยกรรมที่ดีหรือรูปแบบการออกแบบ


43
ทำไมคุณถึงคิดว่าชั้นเรียนได้รับการออกแบบอย่างไม่ถูกต้องถ้ามันมีข้อเขียนด้วยfinal? หลายคน (รวมทั้งผม) finalพบว่ามันเป็นการออกแบบที่ดีที่จะทำให้ทุกระดับไม่ใช่นามธรรม
เห็น

20
ชอบองค์ประกอบมากกว่าการสืบทอดและคุณสามารถมีคลาสนามธรรมfinalทั้งหมด
Andy

24
หลักการเปิด / ปิดนั้นเป็นความผิดสมัยสมัยศตวรรษที่ 20 เมื่อมนต์ได้ทำลำดับชั้นของชั้นเรียนที่สืบทอดมาจากชั้นเรียนที่สืบทอดมาจากชั้นเรียนอื่น นี่เป็นสิ่งที่ดีสำหรับการสอนการเขียนโปรแกรมเชิงวัตถุ แต่มันกลับกลายเป็นว่ายุ่งเหยิงยุ่งเหยิงและไม่เปลี่ยนแปลงเมื่อนำไปใช้กับปัญหาในโลกแห่งความเป็นจริง การออกแบบคลาสที่สามารถขยายได้นั้นเป็นเรื่องยาก
David Hammen

34
@DavidArno อย่าไร้สาระ การสืบทอดคือไซน์ใฐานะที่ไม่ใช่การเขียนโปรแกรมเชิงวัตถุและไม่มีที่ไหนใกล้เคียงกับความซับซ้อนหรือยุ่งเหยิงเหมือนกับบุคคลที่เชื่อฟังมากเกินไปบางคนชอบเทศนา มันเป็นเครื่องมือเหมือนกับคนอื่น ๆ และโปรแกรมเมอร์ที่ดีก็รู้วิธีใช้เครื่องมือที่เหมาะสมสำหรับงาน
Mason Wheeler

48
ในฐานะนักพัฒนาที่กำลังฟื้นตัวฉันดูเหมือนจะจำได้ว่าขั้นสุดท้ายเป็นเครื่องมือที่ยอดเยี่ยมในการป้องกันพฤติกรรมที่เป็นอันตรายจากการเข้าสู่ส่วนที่สำคัญ ฉันดูเหมือนจะจำได้ว่าการสืบทอดเป็นเครื่องมือที่ทรงพลังในหลากหลายวิธี มันเกือบจะเหมือนกับว่าเครื่องมือที่แตกต่างกันของgaspมีทั้งข้อดีและข้อเสียและเราในฐานะวิศวกรต้องรักษาสมดุลของปัจจัยเหล่านั้นเมื่อเราผลิตซอฟต์แวร์ของเรา!
corsiKa

คำตอบ:


136

final เป็นการแสดงออกถึงความตั้งใจ มันบอกผู้ใช้ของคลาสวิธีการหรือตัวแปร "องค์ประกอบนี้ไม่ควรเปลี่ยนและถ้าคุณต้องการเปลี่ยนคุณไม่เข้าใจการออกแบบที่มีอยู่"

นี่เป็นสิ่งสำคัญเนื่องจากสถาปัตยกรรมของโปรแกรมจะยากมากหากคุณต้องคาดหวังว่าทุกคลาสและทุกวิธีที่คุณเคยเขียนอาจเปลี่ยนไปทำสิ่งที่แตกต่างอย่างสิ้นเชิงโดยคลาสย่อย มันจะดีกว่ามากที่จะตัดสินใจขึ้นด้านหน้าซึ่งองค์ประกอบที่ควรจะเป็นมีการเปลี่ยนแปลงและที่ไม่ได้และการบังคับใช้ unchangeablility finalผ่าน

คุณสามารถทำได้ผ่านความคิดเห็นและเอกสารสถาปัตยกรรม แต่เป็นการดีกว่าเสมอที่จะให้คอมไพเลอร์บังคับใช้สิ่งที่ทำได้มากกว่าหวังว่าผู้ใช้ในอนาคตจะอ่านและปฏิบัติตามเอกสาร


14
ดังนั้นฉันจะ แต่ใครก็ตามที่เคยเขียนคลาสฐานที่นำกลับมาใช้ใหม่อย่างกว้างขวาง (เช่นเฟรมเวิร์กหรือไลบรารีสื่อ) รู้ดีกว่าที่คาดหวังว่าโปรแกรมเมอร์แอปพลิเคชันจะทำงานในลักษณะที่มีสติ พวกมันจะล้มล้างใช้ในทางที่ผิดและบิดเบือนสิ่งประดิษฐ์ของคุณในแบบที่คุณไม่เคยคิดว่าจะเป็นไปได้เว้นแต่คุณจะล็อคมันไว้ด้วยที่จับเหล็ก
Kilian Foth

8
@KilianFoth ตกลง แต่โดยสุจริตแล้วปัญหาของคุณคืออะไรโปรแกรมเมอร์แอปพลิเคชันทำอย่างไร
coredump

20
@coredump ผู้ที่ใช้ห้องสมุดของฉันสร้างระบบที่ไม่ดีได้ไม่ดี ระบบที่ไม่ดีก่อให้เกิดชื่อเสียงที่ไม่ดี ผู้ใช้จะไม่สามารถแยกแยะรหัสที่ยอดเยี่ยมของ Kilian ได้จากแอพที่ไม่เสถียรของ Fred Random ผลลัพธ์: ฉันเสียโอกาสในการเขียนโปรแกรมเครดิตและลูกค้า การทำให้โค้ดของคุณยากที่จะนำไปใช้ในทางที่ผิดเป็นคำถามของดอลลาร์และเซนต์
Kilian Foth

21
ข้อความว่า "องค์ประกอบนี้ไม่ควรเปลี่ยนแปลงและหากคุณต้องการเปลี่ยนแปลงคุณไม่เข้าใจการออกแบบที่มีอยู่" มันหยิ่งอย่างไม่น่าเชื่อและไม่ใช่ทัศนคติที่ฉันต้องการในฐานะเป็นส่วนหนึ่งของห้องสมุดที่ฉันทำงานด้วย ถ้าเพียง แต่ฉันมีค่าเล็กน้อยทุกครั้งที่ห้องสมุดที่ห่อหุ้มอย่างน่าขันทำให้ฉันไม่มีกลไกที่จะเปลี่ยนสถานะภายในที่ต้องเปลี่ยนเพราะผู้เขียนไม่สามารถคาดการณ์กรณีการใช้ที่สำคัญ ...
Mason Wheeler

31
@JimmyB Rule of thumb: ถ้าคุณคิดว่าคุณรู้ว่าสิ่งที่คุณสร้างจะถูกใช้เพื่ออะไรคุณก็ผิดแล้ว เบลรู้สึกของโทรศัพท์ว่าเป็นระบบ Muzak คลีนเน็กถูกคิดค้นขึ้นเพื่อวัตถุประสงค์ในการลบเครื่องสำอางได้ง่ายขึ้น Thomas Watson ประธาน IBM เคยกล่าวไว้ว่า "ฉันคิดว่ามีตลาดโลกสำหรับคอมพิวเตอร์ห้าเครื่อง" ตัวแทน Jim Sensenbrenner ซึ่งแนะนำพระราชบัญญัติความรักชาติในปี 2544 มีการบันทึกว่าเป็นการตั้งใจที่จะป้องกันไม่ให้ NSA ทำสิ่งที่พวกเขากำลังทำ และอื่น ๆ ...
Mason Wheeler

59

มันหลีกเลี่ยงเปราะบางปัญหาคลาสฐาน ทุกชั้นเรียนมาพร้อมกับชุดของการรับประกันโดยนัยหรือชัดเจนและไม่เปลี่ยนแปลง หลักการชดเชย 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
  • พวกเขาจัดให้มีการสืบทอดส่วนตัวที่ไม่ได้รับส่วนต่อประสาน แต่มีเพียงการนำไปใช้งาน
  • พวกเขาต้องการวิธีการเรียนพื้นฐานที่จะทำเครื่องหมายเป็นเสมือนจริงและต้องการให้มีการทำเครื่องหมายแทนที่ทั้งหมดเช่นกัน วิธีนี้จะช่วยหลีกเลี่ยงปัญหาที่คลาสย่อยกำหนดวิธีการใหม่ แต่มีการเพิ่มวิธีที่มีลายเซ็นเดียวกันในคลาสพื้นฐาน แต่ไม่ได้ตั้งใจให้เป็นเสมือน

3
คุณสมควรได้รับอย่างน้อย 100 สำหรับการกล่าวถึง "ปัญหาระดับพื้นฐานที่เปราะบาง" :)
David Arno

6
ฉันไม่มั่นใจกับคะแนนที่ทำที่นี่ ใช่ชั้นฐานที่เปราะบางเป็นปัญหา แต่สุดท้ายก็ไม่สามารถแก้ปัญหาทั้งหมดได้ด้วยการเปลี่ยนการใช้งาน ตัวอย่างแรกของคุณไม่ดีเพราะคุณสมมติว่าคุณรู้ทุกกรณีที่เป็นไปได้สำหรับ PasswordChecker ("ล็อคทุกคนหรืออนุญาตให้ทุกคนเข้าถึง ... ไม่เป็นไร" - บอกว่าใคร) รายการ "ที่แนะนำให้เลือกครั้งสุดท้าย" ของคุณนั้นแย่มากจริงๆแล้วคุณกำลังสนับสนุนไม่ให้ขยายและทำเครื่องหมายทุกอย่างเป็นขั้นสุดท้ายซึ่งจะเป็นการลบล้างประโยชน์ของ OOP การสืบทอดและการใช้รหัสซ้ำ
adelphus

4
ตัวอย่างแรกของคุณไม่ใช่ตัวอย่างของปัญหาระดับพื้นฐานที่เปราะบาง ในปัญหาคลาสพื้นฐานที่เปราะบางการเปลี่ยนแปลงคลาสพื้นฐานจะแบ่งคลาสย่อย แต่ในตัวอย่างนั้นคลาสย่อยของคุณไม่ปฏิบัติตามสัญญาของคลาสย่อย นี่เป็นปัญหาสองประการที่แตกต่างกัน (นอกจากนี้จริง ๆ แล้วมีเหตุผลที่ภายใต้สถานการณ์บางอย่างคุณอาจปิดการใช้งานตัวตรวจสอบรหัสผ่าน (พูดเพื่อการพัฒนา))
Winston Ewert

5
"มันหลีกเลี่ยงปัญหาระดับพื้นฐานที่เปราะบาง" - เช่นเดียวกับการฆ่าตัวตายเพื่อไม่ให้หิว
user253751

9
@immibis มันเหมือนการหลีกเลี่ยงการกินเพื่อหลีกเลี่ยงการเป็นพิษต่ออาหาร แน่นอนว่าการไม่รับประทานอาหารจะเป็นปัญหา แต่การกินเฉพาะในสถานที่ที่คุณไว้ใจนั้นสมเหตุสมผลมาก
Winston Ewert

33

ฉันประหลาดใจที่ยังไม่มีใครพูดถึงEffective Java, 2nd Editionโดย Joshua Bloch (ซึ่งควรจะต้องอ่านสำหรับนักพัฒนา Java ทุกคนอย่างน้อย) รายการที่ 17 ในหนังสือเล่มนี้กล่าวถึงรายละเอียดโดยละเอียดและมีชื่อว่า: " การออกแบบและเอกสารสำหรับการสืบทอดหรืออื่น ๆ ไม่อนุญาต "

ฉันจะไม่ทำซ้ำคำแนะนำที่ดีทั้งหมดในหนังสือ แต่ย่อหน้าเหล่านี้ดูเหมือนจะเกี่ยวข้อง:

แต่แล้วชั้นเรียนคอนกรีตทั่วไปล่ะ? ตามเนื้อผ้าพวกเขาไม่ใช่คนสุดท้ายหรือออกแบบและจัดทำเอกสารสำหรับ subclassing แต่เรื่องนี้เป็นเรื่องอันตราย ทุกครั้งที่มีการเปลี่ยนแปลงในคลาสนั้นมีโอกาสที่คลาสไคลเอ็นต์ที่ขยายคลาสจะแตก นี่ไม่ใช่แค่ปัญหาเชิงทฤษฎี ไม่ใช่เรื่องแปลกที่จะได้รับรายงานบั๊กที่เกี่ยวข้องกับคลาสย่อยหลังจากแก้ไข internals ของคลาสคอนกรีตที่ไม่ใช่ไฟนอลที่ไม่ได้ออกแบบและจัดทำเอกสารเพื่อรับมรดก

ทางออกที่ดีที่สุดสำหรับปัญหานี้คือห้ามมิให้คลาสย่อยในคลาสที่ไม่ได้ออกแบบและจัดทำเอกสารเพื่อให้คลาสย่อยปลอดภัย มีสองวิธีในการห้ามคลาสย่อย ความง่ายของทั้งสองคือการประกาศชั้นเรียนสุดท้าย ทางเลือกคือการสร้างทั้งหมดเป็นส่วนตัวหรือแพคเกจส่วนตัวและเพื่อเพิ่มโรงงานคงที่สาธารณะในสถานที่ของการก่อสร้าง ทางเลือกนี้ซึ่งให้ความยืดหยุ่นในการใช้คลาสย่อยภายในอธิบายไว้ในรายการที่ 15 ไม่ว่าจะใช้วิธีใดก็ตาม


21

หนึ่งในเหตุผลที่finalมีประโยชน์คือมันทำให้แน่ใจว่าคุณไม่สามารถคลาสย่อยในทางที่จะละเมิดสัญญาของชั้นผู้ปกครอง คลาสย่อยดังกล่าวจะเป็นการละเมิด SOLID (ส่วนใหญ่ทั้งหมด "L") และการทำให้คลาสfinalป้องกันไม่ให้เกิดขึ้น

ตัวอย่างทั่วไปอย่างหนึ่งคือการทำให้คลาสย่อยเป็นคลาสที่ไม่สามารถเปลี่ยนรูปแบบได้ซึ่งจะทำให้คลาสย่อยไม่สามารถเปลี่ยนแปลงได้ ในบางกรณีการเปลี่ยนแปลงพฤติกรรมอาจนำไปสู่ผลกระทบที่น่าประหลาดใจเช่นเมื่อคุณใช้บางสิ่งบางอย่างเป็นกุญแจในแผนที่ซึ่งคิดว่ากุญแจนั้นไม่เปลี่ยนรูปในขณะที่ในความเป็นจริงคุณกำลังใช้คลาสย่อยที่ไม่แน่นอน

ใน Java มีปัญหาด้านความปลอดภัยที่น่าสนใจมากมายที่สามารถนำมาใช้ถ้าคุณสามารถ subclass Stringและทำให้มันไม่แน่นอน (หรือทำให้มันโทรกลับบ้านเมื่อมีคนเรียกวิธีการของมันจึงอาจดึงข้อมูลที่สำคัญออกจากระบบ) เมื่อวัตถุเหล่านี้ผ่าน รอบรหัสภายในบางส่วนที่เกี่ยวข้องกับการโหลดระดับและความปลอดภัย

Final ก็มีประโยชน์ในการป้องกันความผิดพลาดอย่างง่ายเช่นการใช้ตัวแปรเดียวกันสำหรับสองสิ่งภายในเมธอด ฯลฯ ใน Scala คุณจะได้รับการสนับสนุนให้ใช้เฉพาะvalซึ่งสอดคล้องกับตัวแปรสุดท้ายใน Java และการใช้งานใด ๆvarหรือตัวแปรที่ไม่ใช่ครั้งสุดท้ายถูกมองด้วยความสงสัย

ท้ายที่สุดแล้วคอมไพเลอร์สามารถอย่างน้อยในทางทฤษฎีทำการเพิ่มประสิทธิภาพพิเศษบางอย่างเมื่อพวกเขารู้ว่าชั้นเรียนหรือวิธีการเป็นขั้นสุดท้ายเพราะเมื่อคุณเรียกวิธีการในชั้นเรียนสุดท้ายที่คุณรู้ว่าวิธีการที่จะถูกเรียกและไม่ต้องไป ผ่านตารางวิธีเสมือนเพื่อตรวจสอบการสืบทอด


6
ในที่สุดผู้เรียบเรียงสามารถอย่างน้อยก็ในทางทฤษฎี => ฉันได้ตรวจสอบรหัสผ่านแบบเสมือนจริงของ Clang และฉันยืนยันว่ามันใช้ในการปฏิบัติจริง
Matthieu M.

แต่คอมไพเลอร์ไม่สามารถบอกล่วงหน้าได้ว่าไม่มีใครเอาชนะคลาสหรือวิธีการได้ไม่ว่าจะมีการทำเครื่องหมายสุดท้ายหรือไม่
JesseTG

3
@JesseTG หากมีการเข้าถึงรหัสทั้งหมดในครั้งเดียวอาจเป็นไปได้ อย่างไรก็ตามการรวบรวมไฟล์แยกกันเป็นอย่างไร
Reinstate Monica

3
@JesseTG devirtualization หรือ (monomorphic / polymorphic) การแคชแบบอินไลน์เป็นเทคนิคทั่วไปในคอมไพเลอร์ JIT เนื่องจากระบบทราบว่าคลาสใดที่ถูกโหลดอยู่ในปัจจุบันและสามารถ deoptimize โค้ดได้หากข้อสันนิษฐานของเมธอดที่ไม่มีการแทนที่กลายเป็นเท็จ อย่างไรก็ตามคอมไพเลอร์ก่อนเวลาไม่สามารถ เมื่อฉันรวบรวมคลาส Java และรหัสหนึ่งที่ใช้คลาสนั้นฉันสามารถคอมไพล์คลาสย่อยในภายหลังและส่งอินสแตนซ์ไปยังรหัสที่ใช้ไป วิธีที่ง่ายที่สุดคือการเพิ่ม jar อื่นที่ด้านหน้าของ classpath คอมไพเลอร์ไม่สามารถรู้ทั้งหมดได้เนื่องจากสิ่งนี้เกิดขึ้นในเวลาทำงาน
amon

5
@MTilsted คุณสามารถสันนิษฐานได้SecurityManagerว่าสายจะไม่เปลี่ยนรูปเป็นกรรมวิธีสตริงผ่านการสะท้อนสามารถห้ามผ่าน โปรแกรมส่วนใหญ่ไม่ได้ใช้ แต่ก็ไม่ได้ใช้รหัสที่ไม่น่าเชื่อถือ +++ คุณต้องสมมติว่าสตริงนั้นไม่เปลี่ยนรูปแบบไม่เช่นนั้นคุณจะได้รับความปลอดภัยเป็นศูนย์และมีข้อบกพร่องมากมายเป็นโบนัส โปรแกรมเมอร์ใด ๆ ที่สมมติว่าสตริง Java สามารถเปลี่ยนรหัสได้อย่างมีประสิทธิผล
maaartinus

7

เหตุผลที่สองคือประสิทธิภาพการทำงาน เหตุผลแรกคือเนื่องจากบางคลาสมีพฤติกรรมหรือสถานะที่สำคัญที่ไม่ควรเปลี่ยนแปลงเพื่อให้ระบบทำงานได้ ตัวอย่างเช่นถ้าฉันมีคลาส "PasswordCheck" และสร้างคลาสนั้นฉันได้จ้างทีมผู้เชี่ยวชาญด้านความปลอดภัยและคลาสนี้สื่อสารกับตู้เอทีเอ็มหลายร้อยเครื่องที่มีการศึกษาดีและกำหนด procols อนุญาตให้ผู้ชายจ้างใหม่ที่สดใหม่จากมหาวิทยาลัยสร้างคลาส "TrustMePasswordCheck" ที่ขยายชั้นเรียนด้านบนอาจเป็นอันตรายต่อระบบของฉัน วิธีการเหล่านั้นไม่ควรถูกแทนที่นั่นคือมัน



JVM นั้นฉลาดพอที่จะรักษาคลาสเป็นขั้นสุดท้ายหากไม่มีคลาสย่อยแม้ว่าพวกเขาจะไม่ได้ประกาศขั้นสุดท้าย
user253751

ลดลงเนื่องจากการเรียกร้อง 'ประสิทธิภาพ' ได้รับการพิสูจน์แล้วหลายครั้ง
Paul Smith

7

เมื่อฉันต้องการเรียนฉันจะเขียนชั้นเรียน ถ้าฉันไม่ต้องการคลาสย่อยฉันไม่สนใจคลาสย่อย ฉันตรวจสอบให้แน่ใจว่าชั้นเรียนของฉันประพฤติตามที่ตั้งใจและสถานที่ที่ฉันใช้ชั้นเรียนนั้นจะถือว่าชั้นเรียนนั้นทำงานตามที่ตั้งใจไว้

หากใครต้องการซับคลาสของฉันฉันต้องการปฏิเสธความรับผิดชอบอย่างเต็มที่สำหรับสิ่งที่เกิดขึ้น ฉันประสบความสำเร็จโดยการทำให้ชั้นเรียนเป็น "ขั้นสุดท้าย" หากคุณต้องการคลาสย่อยโปรดจำไว้ว่าฉันไม่ได้นำคลาสย่อยมาพิจารณาในขณะที่ฉันเขียนคลาส ดังนั้นคุณต้องใช้รหัสที่มาเรียนเอา "สุดท้าย" และจากนั้นในสิ่งที่เกิดขึ้นเป็นอย่างเต็มที่ความรับผิดชอบของคุณ

คุณคิดว่านั่นคือ "ไม่ได้มุ่งเน้นวัตถุ"? ฉันได้รับค่าจ้างเพื่อให้ชั้นเรียนทำสิ่งที่ควรจะทำ ไม่มีใครจ่ายเงินให้ฉันสำหรับการเรียนที่สามารถซับคลาสได้ หากคุณได้รับเงินเพื่อให้ชั้นเรียนของฉันสามารถใช้งานได้อีกครั้ง เริ่มต้นด้วยการลบคำหลัก "สุดท้าย"

(นอกเหนือจากนั้น "ขั้นสุดท้าย" มักจะช่วยให้การเพิ่มประสิทธิภาพที่สำคัญตัวอย่างเช่นใน Swift "final" ในชั้นเรียนสาธารณะหรือในวิธีการของชั้นเรียนสาธารณะหมายความว่าคอมไพเลอร์สามารถรู้ได้อย่างเต็มที่ว่า และสามารถแทนที่การกระจายแบบไดนามิกด้วยการส่งแบบคงที่ (ประโยชน์เล็ก ๆ ) และมักจะแทนที่การส่งแบบคงที่ด้วยการ inline (อาจเป็นประโยชน์มาก)

adelphus: อะไรที่ยากที่จะเข้าใจเกี่ยวกับ "ถ้าคุณต้องการ subclass มันใช้ซอร์สโค้ดเอา 'สุดท้าย' และมันเป็นความรับผิดชอบของคุณ"? "ขั้นสุดท้าย" เท่ากับ "การเตือนที่ยุติธรรม"

และฉันไม่ได้รับเงินเพื่อให้ใช้รหัสซ้ำได้ ฉันได้รับเงินเพื่อเขียนโค้ดซึ่งทำในสิ่งที่ควรทำ ถ้าฉันจ่ายเงินเพื่อสร้างรหัสที่คล้ายกันสองบิตฉันจะแยกส่วนทั่วไปออกเพราะราคาถูกกว่าและฉันไม่ได้รับเงินเพื่อเสียเวลา การใช้รหัสซ้ำที่ไม่นำกลับมาใช้ใหม่ทำให้ฉันเสียเวลา

M4ks: คุณมักจะทำให้ทุกอย่างเป็นส่วนตัวที่ไม่ควรจะเข้าถึงได้จากภายนอก อีกครั้งถ้าคุณต้องการคลาสย่อยคุณใช้ซอร์สโค้ดเปลี่ยนสิ่งต่าง ๆ เป็น "ป้องกัน" ถ้าคุณต้องการและรับผิดชอบสิ่งที่คุณทำ หากคุณคิดว่าคุณจำเป็นต้องเข้าถึงสิ่งต่าง ๆ ที่ฉันทำเครื่องหมายว่าเป็นส่วนตัวคุณควรทราบว่าคุณกำลังทำอะไรอยู่

ทั้งสอง: การแบ่งคลาสย่อยเป็นส่วนเล็ก ๆ ของรหัสการใช้ซ้ำ การสร้างบล็อคที่สามารถปรับได้โดยไม่ต้อง subclassing นั้นมีประสิทธิภาพและได้รับประโยชน์อย่างมากจาก "ขั้นสุดท้าย" เพราะผู้ใช้ของบล็อกสามารถพึ่งพาสิ่งที่พวกเขาได้รับ


4
-1 คำตอบนี้อธิบายทุกสิ่งที่ผิดกับนักพัฒนาซอฟต์แวร์ หากมีคนต้องการนำคลาสของคุณกลับมาใช้ใหม่โดยการทำคลาสย่อยให้พวกเขา ทำไมมันถึงเป็นความรับผิดชอบของคุณที่พวกเขาใช้ (หรือการละเมิด) โดยพื้นฐานแล้วคุณกำลังใช้ขั้นสุดท้ายเหมือนกับคุณคุณไม่ได้ใช้คลาสของฉัน * "ไม่มีใครจ่ายเงินให้ฉันสำหรับการทำชั้นที่สามารถ subclassed เป็น" คุณจริงจังไหม นั่นเป็นเหตุผลที่วิศวกรซอฟต์แวร์ใช้ - เพื่อสร้างรหัสที่สามารถนำกลับมาใช้ใหม่ได้
adelphus

4
-1 ทำทุกอย่างให้เป็นส่วนตัวดังนั้นทุกคนจะไม่นึกถึง subclassing ..
M4ks

3
@adelphus ในขณะที่ถ้อยคำของคำตอบนี้ทื่ออยู่ติดกับที่รุนแรงมันไม่ได้เป็นมุมมองที่ "ผิด" ในความเป็นจริงมันเป็นมุมมองเดียวกันกับคำตอบส่วนใหญ่สำหรับคำถามนี้จนถึงขณะนี้มีเพียงเสียงทางคลินิกน้อยลง
NemesisX00

+1 สำหรับการกล่าวถึงว่าคุณสามารถลบ 'ขั้นสุดท้าย' ได้ มันเป็นความหยิ่งในการอ้างสิทธิ์ที่จะทราบล่วงหน้าเกี่ยวกับการใช้งานโค้ดของคุณ ถึงกระนั้นก็เป็นเรื่องที่ถ่อมใจที่จะทำให้ชัดเจนว่าคุณไม่สามารถใช้งานที่เป็นไปได้บางอย่าง
gmatht

4

ลองจินตนาการว่า SDK สำหรับแพลตฟอร์มจัดส่งคลาสต่อไปนี้:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

แอปพลิเคชันคลาสย่อยคลาสนี้:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

ทุกอย่างเรียบร้อยดี แต่มีคนที่ทำงานกับ SDK ตัดสินใจว่าการส่งเมธอดให้getโง่และทำให้อินเทอร์เฟซดีขึ้น

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

ทุกอย่างดูดีจนกระทั่งแอปพลิเคชันจากด้านบนได้รับการคอมไพล์ใหม่ด้วย SDK ใหม่ ทันใดนั้นวิธีการรับ overriden จะไม่ถูกเรียกอีกต่อไปและจะไม่ถูกนับรวม

สิ่งนี้เรียกว่าปัญหาระดับฐานที่เปราะบางเนื่องจากการเปลี่ยนแปลงที่ไร้เดียงสาดูเหมือนจะส่งผลให้เกิดการแบ่งคลาสย่อย เมื่อใดก็ตามที่มีการเปลี่ยนแปลงเมธอดที่ถูกเรียกภายในคลาสอาจทำให้คลาสย่อยแตก นั่นหมายความว่าการเปลี่ยนแปลงใด ๆ อาจทำให้คลาสย่อยแตก

ขั้นสุดท้ายป้องกันไม่ให้ใคร ๆ ทำคลาสย่อยของคุณ ด้วยวิธีนี้วิธีการใดในชั้นเรียนที่สามารถเปลี่ยนแปลงได้โดยไม่ต้องกังวลว่าจะมีใครบางคนขึ้นอยู่กับการเรียกใช้วิธีการใด


1

สุดท้ายหมายความว่าชั้นเรียนของคุณปลอดภัยที่จะเปลี่ยนแปลงในอนาคตโดยไม่ส่งผลกระทบใด ๆ ต่อชั้นเรียนที่สืบทอดจากดาวน์สตรีม (เพราะไม่มี) หรือปัญหาใด ๆ เกี่ยวกับความปลอดภัยของเธรดของชั้นเรียน (ฉันคิดว่ามีบางกรณีที่ บางกระทู้ที่มีความซวยสูง)

สุดท้ายหมายความว่าคุณมีอิสระที่จะเปลี่ยนวิธีการเรียนของคุณโดยไม่มีการเปลี่ยนแปลงที่ไม่ตั้งใจในพฤติกรรมที่คลานเข้าไปในรหัสของคนอื่นที่อาศัยคุณเป็นฐาน

ตัวอย่างเช่นฉันเขียนคลาสที่ชื่อว่า HobbitKiller ซึ่งยอดเยี่ยมเพราะฮอบบิททั้งหมดเป็นลูกเล่นและน่าจะตาย เกาว่าพวกเขาทั้งหมดต้องตายอย่างแน่นอน

คุณใช้สิ่งนี้เป็นคลาสพื้นฐานและเพิ่มวิธีใหม่ที่ยอดเยี่ยมในการใช้เครื่องพ่นสารเคมี แต่ใช้คลาสของฉันเป็นฐานเพราะฉันมีวิธีการที่ยอดเยี่ยมสำหรับการกำหนดเป้าหมายฮอบบิท (นอกเหนือจากการเป็นกลอุบายพวกมันรวดเร็ว) คุณใช้เพื่อช่วยเล็งเครื่องพ่นไฟของคุณ

สามเดือนต่อมาฉันเปลี่ยนการใช้วิธีการกำหนดเป้าหมายของฉัน ในบางจุดในอนาคตเมื่อคุณอัปเกรดไลบรารี่ของคุณไม่รู้จักคุณการใช้งานจริงในชั้นเรียนของคุณได้เปลี่ยนไปอย่างมากเนื่องจากการเปลี่ยนแปลงในวิธีการเรียนพิเศษที่คุณต้องพึ่งพา (และโดยทั่วไปจะไม่ควบคุม)

ดังนั้นสำหรับฉันที่จะเป็นนักพัฒนาที่ขยันขันแข็งและเพื่อให้แน่ใจว่าฮอบบิทตายไปสู่อนาคตโดยใช้ชั้นเรียนของฉันฉันต้องระวังอย่างมากกับการเปลี่ยนแปลงใด ๆ ที่ฉันทำในชั้นเรียนใด ๆ ที่สามารถขยายได้

โดยการลบความสามารถในการขยายออกยกเว้นในกรณีที่ฉันตั้งใจจะขยายชั้นเรียนโดยเฉพาะฉันช่วยตัวเอง (และหวังว่าคนอื่น ๆ ) จะปวดหัวมาก


2
หากคุณกำหนดเป้าหมายวิธีการจะเปลี่ยนทำไมคุณเคยทำให้มันเป็นแบบสาธารณะ? และถ้าคุณเปลี่ยนพฤติกรรมของคลาสอย่างมีนัยสำคัญคุณต้องมีเวอร์ชันอื่นมากกว่าปกป้องมากเกินไปfinal
M4ks

ฉันไม่รู้ว่าทำไมสิ่งนี้ถึงเปลี่ยนไป Hobbits เป็นนักเล่นกลและจำเป็นต้องมีการเปลี่ยนแปลง ประเด็นก็คือว่าถ้าฉันสร้างมันขึ้นมาเป็นครั้งสุดท้ายฉันจะป้องกันการสืบทอดซึ่งปกป้องผู้อื่นจากการเปลี่ยนแปลงของฉันติดรหัสของพวกเขา
Scott Taylor

0

การใช้งานfinalไม่ได้เป็นการละเมิดหลักการของ SOLID แต่อย่างใด โชคไม่ดีที่พบได้ทั่วไปในการตีความ Open / Closed Principle ("เอนทิตีซอฟต์แวร์ควรจะเปิดสำหรับการขยาย แต่ปิดสำหรับการปรับเปลี่ยน") ตามความหมาย "แทนที่จะแก้ไขคลาส, ซับคลาสมันและเพิ่มคุณสมบัติใหม่" นี่ไม่ใช่สิ่งที่ตั้งใจไว้ แต่เดิมและโดยทั่วไปแล้วจะไม่เป็นวิธีที่ดีที่สุดในการบรรลุเป้าหมาย

วิธีที่ดีที่สุดในการปฏิบัติตาม OCP คือการออกแบบจุดส่วนขยายในชั้นเรียนโดยเฉพาะการให้พฤติกรรมเชิงนามธรรมที่มีการกำหนดพารามิเตอร์โดยการฉีดการพึ่งพาวัตถุ (เช่นใช้รูปแบบการออกแบบกลยุทธ์) พฤติกรรมเหล่านี้ควรได้รับการออกแบบให้ใช้อินเทอร์เฟซเพื่อให้การใช้งานใหม่ไม่พึ่งพาการสืบทอด

อีกวิธีหนึ่งคือการใช้คลาสของคุณกับ API สาธารณะเป็นคลาสนามธรรม (หรืออินเทอร์เฟซ) จากนั้นคุณสามารถสร้างการใช้งานใหม่ทั้งหมดซึ่งสามารถเชื่อมต่อกับลูกค้ารายเดียวกัน หากอินเทอร์เฟซใหม่ของคุณต้องการลักษณะการทำงานที่ใกล้เคียงกับต้นฉบับคุณสามารถ:

  • ใช้รูปแบบการออกแบบมัณฑนากรเพื่อนำพฤติกรรมเดิมที่มีอยู่กลับมาใช้ใหม่หรือ
  • สร้างส่วนใหม่ของพฤติกรรมที่คุณต้องการเก็บไว้ในวัตถุตัวช่วยและใช้ตัวช่วยเดียวกันในการปรับใช้ใหม่ของคุณ (การเปลี่ยนโครงสร้างไม่ได้ปรับเปลี่ยน)

0

สำหรับฉันมันเป็นเรื่องของการออกแบบ

สมมติว่าฉันมีโปรแกรมที่คำนวณเงินเดือนสำหรับพนักงาน ถ้าฉันมีคลาสที่ส่งคืนจำนวนวันทำงานระหว่าง 2 วันขึ้นอยู่กับประเทศ (หนึ่งคลาสสำหรับแต่ละประเทศ) ฉันจะใส่ที่อยู่สุดท้ายนั้นและให้วิธีการสำหรับทุกองค์กรในการจัดหาวันว่างสำหรับปฏิทินเท่านั้น

ทำไม? ง่าย สมมติว่านักพัฒนาซอฟต์แวร์ต้องการสืบทอดคลาสพื้นฐาน WorkingDaysUSA ในคลาส WorkingDaysUSAmyCompany และปรับเปลี่ยนเพื่อสะท้อนให้เห็นว่าองค์กรของเขาจะถูกปิดการประท้วง / ซ่อมบำรุง / ด้วยเหตุผลใดก็ตามที่ 2 ของดาวอังคาร

การคำนวณสำหรับคำสั่งซื้อและการส่งมอบของลูกค้าจะสะท้อนให้เห็นถึงความล่าช้าและทำงานได้ตามปกติเมื่อรันไทม์พวกเขาเรียก WorkingDaysUSAmyCompany.getWorkingDays () แต่จะเกิดอะไรขึ้นเมื่อฉันคำนวณเวลาวันหยุดพักผ่อน? ฉันควรจะเพิ่มวันหยุดที่ 2 ของดาวอังคารเป็นวันหยุดสำหรับทุกคนหรือไม่ ไม่ แต่เนื่องจากโปรแกรมเมอร์ใช้มรดกและฉันไม่ได้ปกป้องชั้นเรียนสิ่งนี้อาจนำไปสู่ความสับสน

หรือสมมติว่าพวกเขารับช่วงต่อและปรับเปลี่ยนชั้นเรียนเพื่อสะท้อนว่า บริษัท นี้ไม่ทำงานวันเสาร์ซึ่งในประเทศพวกเขาทำงานครึ่งวันเสาร์ จากนั้นเกิดแผ่นดินไหวไฟฟ้าหรือเหตุการณ์บางอย่างทำให้ประธานาธิบดีประกาศ 3 วันไม่ทำงานเหมือนที่เกิดขึ้นเมื่อเร็ว ๆ นี้ในเวเนซุเอลา หากวิธีการของชั้นเรียนที่สืบทอดมาถูกลบไปแล้วในวันเสาร์ทุกครั้งการแก้ไขชั้นเรียนดั้งเดิมของฉันอาจนำไปสู่การลบในวันเดียวกันสองครั้ง ฉันจะต้องไปที่คลาสย่อยแต่ละรายการบนไคลเอนต์แต่ละแห่งและตรวจสอบว่าการเปลี่ยนแปลงทั้งหมดเข้ากันได้

วิธีการแก้? ทำให้คลาสเป็นที่สิ้นสุดและให้เมธอด addFreeDay (companyID mycompany, Date freeDay) ด้วยวิธีนี้คุณจะแน่ใจได้ว่าเมื่อคุณเรียกคลาส WorkingDaysCountry มันเป็นคลาสหลักของคุณไม่ใช่คลาสย่อย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.