Không bao giờ làm cho các thành viên công cộng ảo / trừu tượng - thực sự?


20

Trở lại những năm 2000, một đồng nghiệp của tôi nói với tôi rằng đó là một mô hình chống làm cho các phương thức công khai trở nên ảo hoặc trừu tượng.

Ví dụ, ông coi một lớp học như thế này không được thiết kế tốt:

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

Anh ta tuyên bố

  • nhà phát triển của một lớp dẫn xuất thực hiện Method1và ghi đè Method2phải lặp lại xác thực đối số.
  • trong trường hợp nhà phát triển của lớp cơ sở quyết định thêm một cái gì đó xung quanh phần tùy chỉnh Method1hoặc Method2sau này, anh ta không thể làm điều đó.

Thay vào đó, đồng nghiệp của tôi đề xuất phương pháp này:

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

Anh ấy nói với tôi làm cho các phương thức công khai (hoặc thuộc tính) trở nên ảo hoặc trừu tượng cũng tệ như làm cho các trường công khai. Bằng cách gói các trường vào các thuộc tính, người ta có thể chặn mọi quyền truy cập vào các trường đó sau, nếu cần. Điều tương tự cũng áp dụng cho các thành viên ảo / trừu tượng công khai: gói chúng theo cách như trong ProtectedAbstractOrVirtuallớp cho phép nhà phát triển lớp cơ sở chặn bất kỳ cuộc gọi nào đi đến các phương thức ảo / trừu tượng.

Nhưng tôi không xem đây là một hướng dẫn thiết kế. Ngay cả Microsoft cũng không tuân theo nó: chỉ cần nhìn vào Streamlớp để xác minh điều này.

Bạn nghĩ gì về hướng dẫn đó? Nó có ý nghĩa gì không, hay bạn nghĩ nó quá phức tạp với API?


5
Phương thức làm virtual cho phép ghi đè tùy chọn. Phương pháp của bạn có lẽ nên được công khai, bởi vì nó có thể không bị ghi đè. Làm phương thức abstract buộc bạn phải ghi đè chúng; có lẽ chúng nên protected, bởi vì chúng không đặc biệt hữu ích trong publicngữ cảnh.
Robert Harvey

4
Trên thực tế, protectedhữu ích nhất khi bạn muốn đưa các thành viên riêng của lớp trừu tượng đến các lớp dẫn xuất. Trong mọi trường hợp, tôi không đặc biệt quan tâm đến ý kiến ​​của bạn bè bạn; chọn công cụ sửa đổi truy cập có ý nghĩa nhất cho tình huống cụ thể của bạn.
Robert Harvey

4
Đồng nghiệp của bạn đã ủng hộ Mẫu Phương thức Mẫu . Có thể có các trường hợp sử dụng cho cả hai cách, tùy thuộc vào mức độ phụ thuộc lẫn nhau của hai phương thức.
Greg Burghardt

8
@GregBurghardt: nghe có vẻ như đồng nghiệp của OP đề nghị luôn luôn sử dụng mẫu phương thức mẫu, bất kể có yêu cầu hay không. Đó là sự lạm dụng điển hình của các mẫu - nếu một người có búa, sớm hay muộn mọi vấn đề bắt đầu trông giống như một cái đinh ;-)
Doc Brown

3
@PeterPerot: Tôi chưa bao giờ gặp vấn đề khi bắt đầu với các DTO đơn giản chỉ với các trường công khai và khi một DTO như vậy xuất hiện để yêu cầu các thành viên có logic nghiệp vụ, hãy cấu trúc lại chúng thành các lớp có thuộc tính. Chắc chắn mọi thứ sẽ khác khi một người làm việc như một nhà cung cấp thư viện và phải quan tâm đến việc không thay đổi API công khai, sau đó thậm chí biến một lĩnh vực công cộng thành một tài sản công cộng cùng tên có thể gây ra vấn đề.
Doc Brown

Câu trả lời:


30

Nói

rằng nó là một mô hình chống biến các phương thức công khai thành ảo hoặc trừu tượng do nhà phát triển của một lớp dẫn xuất thực hiện Phương thức 1 và ghi đè Phương thức 2 phải lặp lại xác thực đối số

là trộn lẫn nhân quả. Nó đưa ra giả định rằng mọi phương thức có thể ghi đè yêu cầu xác thực đối số không thể tùy chỉnh. Nhưng nó hoàn toàn khác.

Nếu một người muốn thiết kế một phương thức theo cách nó cung cấp một số xác nhận đối số cố định trong tất cả các dẫn xuất của lớp (hoặc - nói chung hơn - một phần có thể tùy chỉnh và không thể tùy chỉnh), thì điều đó có nghĩa là làm cho điểm vào không ảo và thay vào đó cung cấp một phương thức ảo hoặc trừu tượng cho phần có thể tùy chỉnh được gọi là nội bộ.

Nhưng có rất nhiều ví dụ mà nó làm cho cảm giác hoàn hảo để có một phương pháp ảo nào, vì có không cố định không tùy chỉnh được phần: nhìn vào các phương pháp tiêu chuẩn như ToStringhay Equalshay GetHashCode- nó sẽ cải thiện thiết kế của objectlớp để có những không được công khai và ảo cùng một lúc? Tôi không nghĩ vậy.

Hoặc, về mặt mã của riêng bạn: khi mã trong lớp cơ sở cuối cùng và cố ý trông như thế này

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

có sự tách biệt giữa Method1Method1Corechỉ làm phức tạp mọi thứ mà không có lý do rõ ràng.


1
Trong trường hợp của ToString()phương thức, Microsoft tốt hơn là làm cho nó không ảo và giới thiệu một phương thức mẫu ảo ToStringCore(). Tại sao: vì điều này: ToString()- lưu ý cho người thừa kế . Họ tuyên bố rằng ToString()không nên trả về null. Họ có thể đã ép buộc nhu cầu này bằng cách thực hiện ToString() => ToStringCore() ?? string.Empty.
Peter Perot

9
@PeterPerot: hướng dẫn mà bạn liên kết đến cũng khuyên bạn không nên quay lại string.Empty, bạn có để ý không? Và nó khuyến nghị rất nhiều thứ khác không thể được thi hành bằng mã bằng cách giới thiệu một cái gì đó giống như một ToStringCorephương thức. Vì vậy, kỹ thuật này có lẽ không phải là công cụ phù hợp cho ToString.
Doc Brown

3
@Theraot: chắc chắn người ta có thể tìm thấy một số lý do hoặc lý do tại sao ToString và Equals hoặc GetHashcode có thể được thiết kế khác nhau, nhưng ngày nay chúng vẫn như vậy (ít nhất tôi nghĩ rằng thiết kế của chúng đủ tốt để làm ví dụ tốt).
Doc Brown

1
@PeterPerot "Tôi đã thấy rất nhiều mã như anyObjectIGot.ToString()thay vì anyObjectIGot?.ToString()" - điều này có liên quan như thế nào? ToStringCore()Cách tiếp cận của bạn sẽ ngăn không cho chuỗi null được trả về, nhưng nó vẫn sẽ ném NullReferenceExceptionnếu đối tượng là null.
IMil

1
@PeterPerot Tôi đã không cố gắng truyền đạt một cuộc tranh luận từ chính quyền. Nó không phải là quá nhiều mà Microsoft sử dụng public virtual, nhưng có những trường hợp trong đó public virtuallà ok. Chúng ta có thể lập luận cho một phần trống không thể tùy chỉnh là làm cho mã bằng chứng trong tương lai ... Nhưng điều đó không hiệu quả. Quay trở lại và thay đổi nó có thể phá vỡ các loại dẫn xuất. Như vậy, nó chẳng kiếm được gì.
Theraot

6

Làm theo cách mà đồng nghiệp của bạn gợi ý sẽ mang lại sự linh hoạt hơn cho người thực hiện lớp cơ sở. Nhưng với nó cũng phức tạp hơn mà thường không được chứng minh bằng các lợi ích giả định.

Lưu ý rằng tính linh hoạt tăng đối với người triển khai lớp cơ sở phải trả giá bằng sự kém linh hoạt cho bên bị ghi đè. Họ nhận được một số hành vi áp đặt mà họ có thể không đặc biệt quan tâm. Đối với họ mọi thứ đã trở nên cứng nhắc hơn. Điều này có thể hợp lý và hữu ích nhưng tất cả phụ thuộc vào kịch bản.

Quy ước đặt tên để thực hiện điều này (mà tôi biết) là để dành tên tốt cho giao diện công cộng và đặt tiền tố tên của phương thức nội bộ bằng "Do".

Một trường hợp hữu ích là khi hành động được thực hiện cần một số thiết lập và một số đóng cửa. Giống như mở một luồng và đóng nó sau khi ghi đè xong. Nói chung, cùng một loại khởi tạo và hoàn thiện. Đó là một mẫu hợp lệ để sử dụng nhưng sẽ là vô nghĩa khi bắt buộc sử dụng nó trong tất cả các kịch bản trừu tượng và ảo.


1
Các Đỗ Phương pháp tiền tố là một lựa chọn. Microsoft thường sử dụng hậu tố phương thức Core .
Peter Perot

@Peter Perot. Tôi chưa bao giờ thấy tiền tố Core trong bất kỳ tài liệu nào của Microsoft nhưng điều này có thể là do tôi đã không được chú ý gần đây. Tôi nghi ngờ họ bắt đầu làm điều này gần đây chỉ để quảng bá cho biệt danh Core, để đặt tên cho .NET Core.
Martin Maat

Không, đó là một chiếc mũ cũ : BindingList. Ngoài ra, tôi đã tìm thấy đề xuất ở đâu đó, có thể là Nguyên tắc thiết kế khung của họ hoặc một cái gì đó tương tự. Và: đó là một postfix . ;-)
Peter Perot

Ít linh hoạt hơn cho lớp dẫn xuất là điểm. Lớp cơ sở là một ranh giới trừu tượng. Lớp cơ sở cho người tiêu dùng biết API công khai của nó làm gì và nó xác định API được yêu cầu để thực hiện các mục tiêu đó. Nếu một lớp dẫn xuất có thể ghi đè một phương thức công khai trong lớp cơ sở, bạn sẽ tăng nguy cơ vi phạm Nguyên tắc thay thế Liskov.
Adrian McCarthy

2

Trong C ++, đây được gọi là mẫu giao diện không ảo (NVI). (Ngày xửa ngày xưa, nó được gọi là Phương thức mẫu. Điều đó thật khó hiểu, nhưng một số bài viết cũ hơn có thuật ngữ đó.) NVI được Herb Sutter quảng cáo, người đã viết về nó ít nhất một vài lần. Tôi nghĩ rằng một trong những sớm nhất là ở đây .

Nếu tôi nhớ lại một cách chính xác, tiền đề là một lớp dẫn xuất không nên thay đổi những gì lớp cơ sở làm mà là nó làm như thế nào .

Ví dụ: Hình dạng có thể có phương thức Di chuyển để định vị lại hình dạng. Việc triển khai cụ thể (ví dụ như Quảng trường và Vòng kết nối) không nên ghi đè trực tiếp Di chuyển, vì Hình dạng xác định ý nghĩa của Di chuyển (ở cấp độ khái niệm). Quảng trường có thể có các chi tiết triển khai khác biệt so với Vòng tròn về cách vị trí được thể hiện bên trong, vì vậy họ sẽ phải ghi đè một số phương thức để cung cấp chức năng Di chuyển.

Trong các ví dụ đơn giản, điều này thường làm sôi động Di chuyển công khai, chỉ ủy thác tất cả công việc cho một VirtualDoTheMove ảo riêng tư, do đó có vẻ như rất nhiều chi phí không có lợi.

Nhưng sự tương ứng một-một này không phải là một yêu cầu. Ví dụ: bạn có thể thêm một phương thức Animate vào API công khai của Shape và nó có thể thực hiện điều đó bằng cách gọi reallyDoTheMove trong một vòng lặp. Bạn kết thúc với hai API phương thức không ảo công khai mà cả hai đều dựa trên một phương thức trừu tượng riêng tư. Vòng kết nối và hình vuông của bạn không cần thực hiện thêm bất kỳ công việc nào, cũng không thể ghi đè Animate .

Lớp cơ sở định nghĩa giao diện chung được sử dụng bởi người tiêu dùng và nó định nghĩa một giao diện của các hoạt động nguyên thủy mà nó cần để thực hiện các phương thức công khai đó. Các loại dẫn xuất có trách nhiệm cung cấp việc thực hiện các hoạt động nguyên thủy đó.

Tôi không nhận thấy bất kỳ sự khác biệt nào giữa C # và C ++ sẽ thay đổi khía cạnh này của thiết kế lớp.


Tìm tốt Bây giờ tôi nhớ rằng tôi đã tìm thấy chính xác rằng bài đăng liên kết thứ 2 của bạn tới (hoặc một bản sao của nó), vào những năm 2000. Tôi nhớ rằng tôi đã tìm kiếm thêm bằng chứng về yêu cầu của đồng nghiệp của tôi và rằng tôi không tìm thấy bất cứ điều gì trong bối cảnh C #, nhưng C ++. Điều này. Là. Nó! :-) Nhưng, trở lại vùng đất C #, có vẻ như mẫu này không được sử dụng nhiều. Có thể mọi người nhận ra rằng việc thêm chức năng cơ bản sau này cũng có thể phá vỡ các lớp dẫn xuất và việc sử dụng nghiêm ngặt TMP hoặc NVIP thay vì các phương thức ảo công khai không phải lúc nào cũng có ý nghĩa.
Peter Perot

Lưu ý đến bản thân: thật tốt khi biết mẫu này có tên: NVIP.
Peter Perot
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.