Tại sao từ khóa 'cuối cùng' sẽ hữu ích?


54

Có vẻ như Java đã có quyền tuyên bố các lớp không có nguồn gốc từ lâu đời và bây giờ C ++ cũng có nó. Tuy nhiên, theo nguyên tắc Mở / Đóng trong RẮN, tại sao điều đó lại hữu ích? Đối với tôi, finaltừ khóa nghe có vẻ giống như friend- nó là hợp pháp, nhưng nếu bạn đang sử dụng nó, rất có thể thiết kế đã sai. Vui lòng cung cấp một số ví dụ trong đó một lớp không có nguồn gốc sẽ là một phần của kiến ​​trúc hoặc mẫu thiết kế tuyệt vời.


43
Tại sao bạn nghĩ rằng một lớp được thiết kế sai nếu nó được chú thích 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ượng final.
Phát hiện

20
Thành phần ủng hộ kế thừa và bạn có thể có mọi lớp không trừu tượng final.
Andy

24
Nguyên tắc mở / đóng theo nghĩa là lỗi thời từ thế kỷ 20, khi câu thần chú là tạo ra một hệ thống phân cấp các lớp được thừa hưởng từ các lớp được thừa hưởng từ các lớp khác. Điều này là tốt cho việc dạy lập trình hướng đối tượng nhưng hóa ra lại tạo ra những mớ hỗn độn, không thể nhầm lẫn khi áp dụng vào các vấn đề trong thế giới thực. Thiết kế một lớp để có thể mở rộng là khó.
David Hammen

34
@DavidArno Đừng nực cười. Kế thừa là điều kiện thiết yếu của lập trình hướng đối tượng và không nơi nào phức tạp hay lộn xộn như những cá nhân giáo điều quá mức nhất định muốn rao giảng. Đó là một công cụ, giống như bất kỳ công cụ nào khác, và một lập trình viên giỏi biết cách sử dụng công cụ phù hợp cho công việc.
Mason Wheeler

48
Là một nhà phát triển đang phục hồi, tôi dường như nhớ lại cuối cùng là một công cụ tuyệt vời để ngăn chặn hành vi có hại xâm nhập vào các phần quan trọng. Tôi dường như cũng nhớ lại sự kế thừa là một công cụ mạnh mẽ theo nhiều cách khác nhau. Gần như thể các công cụ khác nhau thở hổn hển có những ưu và nhược điểm và chúng tôi là các kỹ sư phải cân bằng các yếu tố đó khi chúng tôi sản xuất phần mềm của mình!
corsiKa

Câu trả lời:


136

final bày tỏ ý định . Nó nói với người dùng của một lớp, phương thức hoặc biến "Phần tử này không có nghĩa vụ phải thay đổi và nếu bạn muốn thay đổi nó, bạn đã không hiểu thiết kế hiện có."

Điều này rất quan trọng vì kiến ​​trúc chương trình sẽ thực sự rất khó nếu bạn phải dự đoán rằng mọi lớp và mọi phương thức bạn từng viết có thể được thay đổi để làm một cái gì đó hoàn toàn khác bởi một lớp con. Sẽ tốt hơn nhiều khi quyết định trước những yếu tố nào được cho là có thể thay đổi và yếu tố nào không, và để thực thi tính không thay đổi thông qua final.

Bạn cũng có thể làm điều này thông qua các bình luận và tài liệu kiến ​​trúc, nhưng tốt hơn hết là hãy để trình biên dịch thực thi những thứ có thể hơn là hy vọng rằng người dùng trong tương lai sẽ đọc và tuân theo tài liệu.


14
Tôi cũng vậy. Nhưng bất cứ ai từng viết một lớp cơ sở được sử dụng lại rộng rãi (như khung hoặc thư viện phương tiện) đều biết rõ hơn là mong đợi các lập trình viên ứng dụng hành xử theo cách lành mạnh. Họ sẽ lật đổ, lạm dụng và bóp méo phát minh của bạn theo những cách mà bạn thậm chí không nghĩ là có thể trừ khi bạn khóa nó bằng một cái kẹp sắt.
Kilian Foth

8
@KilianFoth Ok, nhưng thành thật mà nói, vấn đề của các lập trình viên ứng dụng là gì?
coredump

20
@coredump Mọi người sử dụng thư viện của tôi tạo ra các hệ thống xấu. Hệ thống xấu sinh danh tiếng xấu. Người dùng sẽ không thể phân biệt mã tuyệt vời của Kilian với ứng dụng không ổn định khủng khiếp của Fred Random. Kết quả: Tôi mất tín dụng lập trình và khách hàng. Làm cho mã của bạn khó sử dụng sai là một câu hỏi về đô la và xu.
Kilian Foth

21
Câu lệnh "Yếu tố này không được phép thay đổi và nếu bạn muốn thay đổi nó, bạn đã không hiểu thiết kế hiện có." là vô cùng kiêu ngạo và không phải là thái độ tôi muốn như là một phần của bất kỳ thư viện nào tôi làm việc cùng. Giá như mỗi lần tôi có một thư viện bị đóng gói quá mức một cách lố bịch khiến tôi không có cơ chế thay đổi một số trạng thái bên trong cần phải thay đổi vì tác giả đã không lường trước được một trường hợp sử dụng quan trọng ...
Mason Wheeler

31
@JimmyB Quy tắc ngón tay cái: Nếu bạn nghĩ rằng bạn biết sáng tạo của bạn sẽ được sử dụng cho mục đích gì, bạn đã sai. Bell quan niệm điện thoại thực chất là một hệ thống Muzak. Kleenex được phát minh với mục đích tẩy trang thuận tiện hơn. Thomas Watson, chủ tịch của IBM, đã từng nói "Tôi nghĩ rằng có một thị trường thế giới cho khoảng năm máy tính." Đại diện Jim Sensenbrenner, người đã giới thiệu Đạo luật PATRIOT vào năm 2001, có tên trong hồ sơ nói rằng nó nhằm mục đích đặc biệt là ngăn NSA thực hiện những việc họ đang làm "với cơ quan hành động của PATRIOT." Và cứ thế ...
Mason Wheeler

59

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 SpiffyPagelớ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);
    ...
  }
}

( topTham 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ờ extendmộ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à finalngoạ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.

3
Bạn xứng đáng ít nhất +100 khi đề cập đến "vấn đề lớp cơ sở mong manh". :)
David Arno

6
Tôi không bị thuyết phục bởi những điểm được thực hiện ở đây. Đúng, lớp cơ sở mong manh là một vấn đề, nhưng cuối cùng không giải quyết được tất cả các vấn đề với việc thay đổi một cách thực hiện. Ví dụ đầu tiên của bạn rất tệ vì bạn cho rằng bạn biết tất cả các trường hợp sử dụng có thể có cho PasswordChecker ("khóa tất cả mọi người hoặc cho phép mọi người truy cập ... không ổn" - nói ai?). Danh sách "do đó nên ..." cuối cùng của bạn thực sự rất tệ - về cơ bản, bạn chủ trương không mở rộng bất cứ điều gì và đánh dấu mọi thứ là cuối cùng - điều này hoàn toàn xóa sạch tính hữu dụng của OOP, kế thừa và tái sử dụng mã.
adelphus

4
Ví dụ đầu tiên của bạn không phải là một ví dụ về vấn đề lớp cơ sở mong manh. Trong bài toán lớp cơ sở dễ vỡ, các thay đổi thành lớp cơ sở phá vỡ một lớp con. Nhưng trong ví dụ đó, lớp con của bạn không tuân theo hợp đồng của lớp con. Đây là hai vấn đề khác nhau. (Ngoài ra, thực sự hợp lý là trong một số trường hợp nhất định, bạn có thể vô hiệu hóa trình kiểm tra mật khẩu (nói để phát triển))
Winston Ewert

5
"Nó tránh được vấn đề lớp cơ sở mong manh" - giống như cách tự giết mình để tránh bị đói.
dùng253751

9
@immibis, nó giống như tránh ăn để tránh bị ngộ độc thực phẩm. Chắc chắn, không bao giờ ăn sẽ là một vấn đề. Nhưng chỉ ăn ở những nơi mà bạn tin tưởng có rất nhiều ý nghĩa.
Winston Ewert

33

Tôi ngạc nhiên khi chưa có ai đề cập đến Java hiệu quả, Ấn bản thứ 2 của Joshua Bloch (ít nhất nên đọc cho mọi nhà phát triển Java). Mục 17 trong cuốn sách thảo luận chi tiết về vấn đề này và có tiêu đề: " Thiết kế và tài liệu để thừa kế hoặc nếu không thì cấm ".

Tôi sẽ không lặp lại tất cả những lời khuyên tốt trong cuốn sách, nhưng những đoạn đặc biệt này có vẻ phù hợp:

Nhưng những gì về các lớp bê tông thông thường? Theo truyền thống, chúng không phải là cuối cùng cũng không được thiết kế và ghi lại cho phân lớp, nhưng tình trạng này là nguy hiểm. Mỗi lần thay đổi được thực hiện trong một lớp như vậy, có khả năng các lớp khách mở rộng lớp sẽ bị phá vỡ. Đây không chỉ là một vấn đề lý thuyết. Không có gì lạ khi nhận được các báo cáo lỗi liên quan đến phân lớp sau khi sửa đổi các phần bên trong của một lớp bê tông không phải là không được thiết kế và ghi lại cho kế thừa.

Giải pháp tốt nhất cho vấn đề này là cấm phân lớp trong các lớp không được thiết kế và ghi lại để được phân lớp an toàn. Có hai cách để cấm phân lớp. Việc hai người dễ dàng hơn là tuyên bố trận chung kết của lớp. Cách khác là làm cho tất cả các nhà xây dựng là riêng tư hoặc gói riêng tư và thêm các nhà máy tĩnh công cộng thay cho các nhà xây dựng. Sự thay thế này, cung cấp tính linh hoạt để sử dụng các lớp con bên trong, được thảo luận trong Mục 15. Cách tiếp cận nào cũng được chấp nhận.


21

Một trong những lý do finalrất hữu ích là nó đảm bảo bạn không thể phân lớp một lớp theo cách vi phạm hợp đồng của lớp cha. Việc phân lớp như vậy sẽ vi phạm RẮN (hầu hết tất cả "L") và làm cho một lớp finalngăn chặn nó.

Một ví dụ điển hình là làm cho không thể phân lớp một lớp bất biến theo cách làm cho lớp con có thể thay đổi. Trong một số trường hợp, việc thay đổi hành vi như vậy có thể dẫn đến những hiệu ứng rất đáng ngạc nhiên, ví dụ như khi bạn sử dụng thứ gì đó làm chìa khóa trong bản đồ, nghĩ rằng khóa là bất biến trong khi thực tế bạn đang sử dụng một lớp con có thể thay đổi.

Trong Java, rất nhiều vấn đề bảo mật thú vị có thể được đưa ra nếu bạn có thể phân lớp Stringvà làm cho nó có thể thay đổi (hoặc khiến nó gọi về nhà khi ai đó gọi phương thức của nó, do đó có thể rút dữ liệu nhạy cảm ra khỏi hệ thống) khi các đối tượng này được thông qua xung quanh một số mã nội bộ liên quan đến tải lớp và bảo mật.

Final cũng đôi khi hữu ích trong việc ngăn ngừa các lỗi đơn giản như sử dụng lại cùng một biến cho hai điều trong một phương thức, v.v. Trong Scala, bạn được khuyến khích chỉ sử dụng valtương ứng với các biến cuối cùng trong Java và thực sự sử dụng một biến varhoặc biến không phải là cuối cùng được xem xét với sự nghi ngờ.

Cuối cùng, các trình biên dịch có thể, ít nhất là về mặt lý thuyết, thực hiện một số tối ưu hóa bổ sung khi chúng biết rằng một lớp hoặc phương thức là cuối cùng, vì khi bạn gọi một phương thức trên một lớp cuối cùng, bạn biết chính xác phương thức nào sẽ được gọi và không phải đi thông qua bảng phương thức ảo để kiểm tra tính kế thừa.


6
Cuối cùng, các trình biên dịch có thể, ít nhất là trên lý thuyết => Cá nhân tôi đã xem xét việc vượt qua ảo ảnh của Clang và tôi xác nhận nó được sử dụng trong thực tế.
Matthieu M.

Nhưng trình biên dịch không thể nói trước rằng không ai ghi đè một lớp hoặc phương thức bất kể nó có được đánh dấu cuối cùng hay không?
JesseTG

3
@JesseTG Nếu nó có quyền truy cập vào tất cả các mã cùng một lúc, có lẽ. Những gì về biên dịch tập tin riêng biệt, mặc dù?
Phục hồi Monica

3
@JlieTG devirtualization hoặc bộ nhớ đệm nội tuyến (đơn hình / đa hình) là một kỹ thuật phổ biến trong trình biên dịch JIT, vì hệ thống biết các lớp nào hiện đang được tải và có thể mở rộng mã nếu giả định không có phương thức ghi đè nào là sai. Tuy nhiên, một trình biên dịch trước thời gian không thể. Khi tôi biên dịch một lớp Java và mã sử dụng lớp đó, sau này tôi có thể biên dịch một lớp con và chuyển một thể hiện cho mã tiêu thụ. Cách đơn giản nhất là thêm một jar khác vào phía trước của classpath. Trình biên dịch không thể biết về tất cả điều đó, vì điều này xảy ra vào thời gian chạy.
amon

5
@MTilsted Bạn có thể giả sử rằng các chuỗi là bất biến vì các chuỗi đột biến thông qua phản xạ có thể bị cấm thông qua a SecurityManager. Hầu hết các chương trình không sử dụng nó, nhưng họ cũng không chạy bất kỳ mã không đáng tin cậy nào. +++ Bạn phải giả sử rằng các chuỗi là bất biến vì nếu không, bạn nhận được bảo mật bằng không và một loạt các lỗi tùy ý làm phần thưởng. Bất kỳ lập trình viên nào giả định các chuỗi Java có thể thay đổi mã sản xuất chắc chắn là điên rồ.
maaartinus

7

Lý do thứ hai là hiệu suất . Lý do đầu tiên là vì một số lớp có các hành vi hoặc trạng thái quan trọng không được phép thay đổi để cho phép hệ thống hoạt động. Ví dụ: nếu tôi có một lớp "PasswordCheck" và để xây dựng lớp đó, tôi đã thuê một nhóm chuyên gia bảo mật và lớp này giao tiếp với hàng trăm máy ATM với các công cụ được nghiên cứu và xác định rõ ràng. Cho phép một anh chàng mới được tuyển dụng mới ra khỏi trường đại học, tạo một lớp "TrustMePasswordCheck" mở rộng lớp trên có thể rất có hại cho hệ thống của tôi; những phương pháp đó không được ghi đè, đó là nó.



JVM đủ thông minh để coi các lớp là cuối cùng nếu chúng không có các lớp con, ngay cả khi chúng không được khai báo là cuối cùng.
dùng253751

Bị từ chối vì yêu cầu 'hiệu suất' đã bị từ chối rất nhiều lần.
Paul Smith

7

Khi tôi cần một lớp học, tôi sẽ viết một lớp. Nếu tôi không cần các lớp con, tôi không quan tâm đến các lớp con. Tôi chắc chắn rằng lớp của tôi cư xử như dự định và những nơi tôi sử dụng lớp cho rằng lớp đó cư xử như dự định.

Nếu bất cứ ai muốn phân lớp của tôi, tôi muốn từ chối hoàn toàn mọi trách nhiệm cho những gì xảy ra. Tôi đạt được điều đó bằng cách làm cho lớp "cuối cùng". Nếu bạn muốn phân lớp nó, hãy nhớ rằng tôi đã không tính đến phân lớp trong khi tôi viết lớp. Vì vậy, bạn phải lấy mã nguồn lớp, loại bỏ "cuối cùng" và từ đó, bất cứ điều gì xảy ra đều thuộc trách nhiệm của bạn .

Bạn nghĩ rằng "không hướng đối tượng"? Tôi đã được trả tiền để tạo ra một lớp học làm những gì nó phải làm. Không ai trả tiền cho tôi để tạo một lớp có thể được phân lớp. Nếu bạn được trả tiền để làm cho lớp học của tôi có thể tái sử dụng, bạn có thể làm điều đó. Bắt đầu bằng cách loại bỏ từ khóa "cuối cùng".

. và có thể thay thế công văn động bằng công văn tĩnh (lợi ích nhỏ) và thường thay thế công văn tĩnh bằng nội tuyến (có thể là lợi ích rất lớn)).

adelphus: Điều gì quá khó hiểu về "nếu bạn muốn phân lớp nó, lấy mã nguồn, xóa 'cuối cùng', và đó là trách nhiệm của bạn"? "Cuối cùng" bằng "cảnh báo công bằng".

Và tôi không được trả tiền để tạo mã có thể tái sử dụng. Tôi được trả tiền để viết mã làm những gì nó phải làm. Nếu tôi được trả tiền để tạo hai bit mã tương tự nhau, tôi trích xuất các phần phổ biến vì rẻ hơn và tôi không phải trả tiền để lãng phí thời gian của mình. Làm cho mã có thể tái sử dụng mà không được sử dụng lại là một sự lãng phí thời gian của tôi.

M4ks: Bạn luôn đặt mọi thứ riêng tư mà không cần phải truy cập từ bên ngoài. Một lần nữa, nếu bạn muốn phân lớp, bạn lấy mã nguồn, thay đổi mọi thứ thành "được bảo vệ" nếu bạn cần và chịu trách nhiệm về những gì bạn làm. Nếu bạn nghĩ rằng bạn cần truy cập vào những thứ mà tôi đánh dấu là riêng tư, bạn nên biết rõ hơn những gì bạn đang làm.

Cả hai: Phân lớp là một phần rất nhỏ của mã tái sử dụng. Tạo các khối xây dựng có thể được điều chỉnh mà không cần phân lớp sẽ mạnh hơn rất nhiều và mang lại lợi ích lớn từ "cuối cùng" vì người dùng của các khối có thể dựa vào những gì họ nhận được.


4
-1 Câu trả lời này mô tả mọi thứ sai với các nhà phát triển phần mềm. Nếu ai đó muốn sử dụng lại lớp của bạn bằng cách phân lớp, hãy để họ. Tại sao trách nhiệm của họ là họ sử dụng (hoặc lạm dụng) nó như thế nào? Về cơ bản, bạn đang sử dụng cuối cùng như bạn, bạn không sử dụng lớp của tôi. * "Không ai trả tiền cho tôi để tạo một lớp có thể được phân lớp" . Bạn nghiêm túc chứ? Đó chính xác là lý do tại sao các kỹ sư phần mềm được sử dụng - để tạo mã vững chắc, có thể tái sử dụng.
adelphus

4
-1 Chỉ cần đặt mọi thứ ở chế độ riêng tư, vì vậy mọi người thậm chí sẽ không nghĩ đến việc phân lớp ..
M4ks 13/05/2016

3
@adelphus Mặc dù từ ngữ của câu trả lời này là thẳng thừng, giáp với sự khắc nghiệt, nhưng đó không phải là một quan điểm "sai". Trong thực tế, đó là quan điểm tương tự như phần lớn các câu trả lời cho câu hỏi này cho đến nay chỉ với một giọng điệu lâm sàng ít hơn.
NemesisX00

+1 để đề cập rằng bạn có thể xóa 'trận chung kết'. Thật là kiêu ngạo khi tuyên bố biết trước tất cả các cách sử dụng mã của bạn. Tuy nhiên, thật khiêm tốn để làm rõ rằng bạn không thể duy trì một số sử dụng có thể, và những sử dụng đó sẽ yêu cầu duy trì một ngã ba.
gmatht

4

Chúng ta hãy tưởng tượng rằng SDK cho một nền tảng vận chuyển lớp sau:

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

Một lớp con ứng dụng lớp này:

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

Tất cả đều ổn và tốt, nhưng ai đó làm việc trên SDK quyết định rằng việc truyền một phương thức getlà ngớ ngẩn và làm cho giao diện tốt hơn đảm bảo thực thi khả năng tương thích ngược.

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);
}

Mọi thứ có vẻ ổn, cho đến khi ứng dụng từ phía trên được biên dịch lại với SDK mới. Đột nhiên, phương thức get overriden không được gọi nữa và yêu cầu không được tính.

Đây được gọi là vấn đề lớp cơ sở mong manh, bởi vì một sự thay đổi dường như vô hại dẫn đến việc phá vỡ lớp con. Bất cứ lúc nào thay đổi phương thức nào được gọi bên trong lớp có thể khiến một lớp con bị phá vỡ. Điều đó có nghĩa là hầu như bất kỳ thay đổi nào cũng có thể khiến một lớp con bị phá vỡ.

Cuối cùng ngăn chặn bất cứ ai phân lớp của bạn. Theo cách đó, các phương thức bên trong lớp có thể được thay đổi mà không phải lo lắng rằng một nơi nào đó phụ thuộc vào chính xác các cuộc gọi phương thức nào được thực hiện.


1

Cuối cùng có nghĩa là lớp của bạn an toàn để thay đổi trong tương lai mà không ảnh hưởng đến bất kỳ lớp dựa trên kế thừa xuôi dòng nào (vì không có), hoặc bất kỳ vấn đề nào về an toàn luồng của lớp (Tôi nghĩ rằng có những trường hợp từ khóa cuối cùng trên một trường ngăn chặn một số chủ đề dựa trên jinx cao).

Cuối cùng có nghĩa là bạn có thể tự do thay đổi cách lớp của bạn hoạt động mà không có bất kỳ thay đổi ngoài ý muốn nào trong hành vi len lỏi vào mã của người khác dựa vào bạn làm cơ sở.

Lấy ví dụ, tôi viết một lớp có tên HobbitKiller, thật tuyệt vời, bởi vì tất cả các hobbit đều là thủ đoạn và có lẽ sẽ chết. Cào đó, tất cả họ chắc chắn cần phải chết.

Bạn sử dụng lớp này làm lớp cơ sở và thêm vào một phương thức mới tuyệt vời để sử dụng súng phun lửa, nhưng sử dụng lớp của tôi làm cơ sở vì tôi có một phương pháp tuyệt vời để nhắm mục tiêu các hobbit (ngoài việc là thủ thuật, chúng rất nhanh), mà bạn sử dụng để giúp nhắm súng phun lửa của bạn.

Ba tháng sau tôi thay đổi việc thực hiện phương pháp nhắm mục tiêu của mình. Bây giờ, tại một số điểm trong tương lai khi nâng cấp thư viện của bạn, không biết đến bạn, việc triển khai thời gian chạy thực tế của lớp bạn đã thay đổi về cơ bản do thay đổi phương thức siêu hạng mà bạn phụ thuộc (và thường không kiểm soát).

Vì vậy, để tôi trở thành một nhà phát triển có lương tâm và đảm bảo cái chết của hobbit suôn sẻ trong tương lai bằng cách sử dụng lớp của tôi, tôi phải rất, rất cẩn thận với bất kỳ thay đổi nào tôi thực hiện đối với bất kỳ lớp nào có thể được mở rộng.

Bằng cách loại bỏ khả năng mở rộng ngoại trừ trong trường hợp tôi đặc biệt có ý định mở rộng lớp học, tôi đã tự cứu mình (và hy vọng những người khác) rất đau đầu.


2
Nếu phương pháp nhắm mục tiêu của bạn sẽ thay đổi tại sao bạn từng công khai nó? Và nếu bạn thay đổi hành vi của một lớp một cách đáng kể, bạn cần một phiên bản khác thay vì bảo vệ quá mứcfinal
M4ks

Tôi không biết tại sao điều này sẽ thay đổi. Hobbit là thủ đoạn và sự thay đổi là bắt buộc. Vấn đề là nếu tôi xây dựng nó như là một bản cuối cùng, tôi sẽ ngăn chặn sự kế thừa để bảo vệ người khác khỏi những thay đổi của tôi lây nhiễm mã của họ.
Scott Taylor

0

Việc sử dụng finalkhông theo bất kỳ cách nào là vi phạm các nguyên tắc RẮN. Thật không may, rất phổ biến để giải thích Nguyên tắc Mở / Đóng ("các thực thể phần mềm nên mở để mở rộng nhưng đóng để sửa đổi") có nghĩa là "thay vì sửa đổi một lớp, phân lớp và thêm các tính năng mới". Đây không phải là ý nghĩa ban đầu của nó và thường không phải là cách tiếp cận tốt nhất để đạt được mục tiêu của nó.

Cách tốt nhất để tuân thủ OCP là thiết kế các điểm mở rộng thành một lớp, bằng cách cung cấp cụ thể các hành vi trừu tượng được tham số hóa bằng cách tiêm phụ thuộc vào đối tượng (ví dụ: sử dụng mẫu thiết kế Chiến lược). Những hành vi này nên được thiết kế để sử dụng một giao diện để việc triển khai mới không phụ thuộc vào sự kế thừa.

Một cách tiếp cận khác là triển khai lớp của bạn với API công khai dưới dạng một lớp trừu tượng (hoặc giao diện). Sau đó, bạn có thể tạo ra một triển khai hoàn toàn mới có thể cắm vào cùng các máy khách. Nếu giao diện mới của bạn yêu cầu hành vi tương tự như ban đầu, bạn có thể:

  • sử dụng mẫu thiết kế Decorator để sử dụng lại hành vi hiện có của bản gốc, hoặc
  • cấu trúc lại các phần của hành vi mà bạn muốn giữ trong một đối tượng trợ giúp và sử dụng cùng một trình trợ giúp trong triển khai mới của bạn (tái cấu trúc không phải là sửa đổi).

0

Đối với tôi đó là vấn đề thiết kế.

Giả sử tôi có một chương trình tính lương cho nhân viên. Nếu tôi có một lớp trả về số ngày làm việc giữa 2 ngày dựa trên quốc gia (một lớp cho mỗi quốc gia), tôi sẽ đặt lớp cuối cùng đó và cung cấp một phương thức cho mỗi doanh nghiệp chỉ cung cấp một ngày miễn phí cho lịch của họ.

Tại sao? Đơn giản. Giả sử một nhà phát triển muốn kế thừa lớp cơ sở WorkDaysUSA trong một lớp WorkDaysUSAmyCompany và sửa đổi nó để phản ánh rằng doanh nghiệp của anh ta sẽ bị đóng cửa để đình công / bảo trì / vì lý do thứ 2 của sao hỏa.

Các tính toán cho đơn đặt hàng và giao hàng của khách hàng sẽ phản ánh độ trễ và hoạt động tương ứng khi trong thời gian chạy họ gọi WorkDaysUSAmyCompany.getWorkingDays (), nhưng điều gì xảy ra khi tôi tính thời gian nghỉ? Tôi có nên thêm lần thứ 2 của sao hỏa như một kỳ nghỉ cho tất cả mọi người? Không. Nhưng vì lập trình viên đã sử dụng tính kế thừa và tôi không bảo vệ lớp nên điều này có thể dẫn đến một sự nhầm lẫn.

Hoặc giả sử họ kế thừa và sửa đổi lớp để phản ánh rằng công ty này không làm việc vào thứ bảy, nơi họ làm việc nửa buổi vào thứ bảy. Sau đó, một trận động đất, khủng hoảng điện hoặc một số trường hợp khiến tổng thống tuyên bố 3 ngày không làm việc như nó đã xảy ra gần đây trên Venezuela. Nếu phương thức của lớp kế thừa đã bị trừ vào mỗi thứ bảy, các sửa đổi của tôi trên lớp ban đầu có thể dẫn đến trừ đi cùng một ngày hai lần. Tôi sẽ phải đi đến từng lớp con trên mỗi khách hàng và xác minh tất cả các thay đổi là tương thích.

Giải pháp? Làm cho lớp cuối cùng và cung cấp một phương thức addFreeDay (companyID mycompany, Date freeDay). Bằng cách đó, bạn chắc chắn rằng khi bạn gọi một lớp WorkDaysCountry, đó là lớp chính của bạn chứ không phải là lớp con

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.