Tại sao kế thừa một lớp và không thêm thuộc tính?


39

Tôi đã tìm thấy một cây thừa kế trong cơ sở mã (khá lớn) của chúng tôi có dạng như thế này:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

Từ những gì tôi có thể thu thập, điều này chủ yếu được sử dụng để liên kết các công cụ trên front-end.

Đối với tôi, điều này có ý nghĩa vì nó đặt một cái tên cụ thể cho lớp, thay vì dựa vào cái chung NamedEntity. Mặt khác, có một số lớp như vậy đơn giản là không có thuộc tính bổ sung.

Có bất kỳ nhược điểm của phương pháp này?


25
Mặt trái không được đề cập: Bạn có thể phân biệt các phương pháp chỉ liên quan đến OrderDateInfos với các phương pháp có liên quan đến các NamedEntitys khác
Caleth 12/12/18

8
Vì lý do tương tự mà nếu bạn tình cờ có, giả sử, một Identifierlớp không liên quan gì ngoài việc NamedEntityyêu cầu cả tài sản IdNametài sản, bạn sẽ không sử dụng NamedEntitythay thế. Bối cảnh và cách sử dụng đúng của một lớp không chỉ là các thuộc tính và phương thức mà nó giữ.
Neil

Câu trả lời:


15

Đối với tôi, điều này có ý nghĩa vì nó đặt một cái tên cụ thể cho lớp, thay vì dựa vào NamedEntity chung. Mặt khác, có một số lớp như vậy đơn giản là không có thuộc tính bổ sung.

Có bất kỳ nhược điểm của phương pháp này?

Cách tiếp cận không tệ, nhưng có những giải pháp tốt hơn. Nói tóm lại, một giao diện sẽ là một giải pháp tốt hơn cho việc này. Lý do chính tại sao giao diện và kế thừa khác nhau ở đây là vì bạn chỉ có thể kế thừa từ một lớp, nhưng bạn có thể thực hiện nhiều giao diện .

Ví dụ, hãy xem xét rằng bạn đã đặt tên cho các thực thể và các thực thể được kiểm toán. Bạn có một vài thực thể:

Onekhông phải là một thực thể được kiểm toán cũng không phải là một thực thể được đặt tên. Điều đó thật đơn giản:

public class One 
{ }

Twolà một thực thể được đặt tên nhưng không phải là một thực thể được kiểm toán. Đó chính là những gì bạn có bây giờ:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Threelà cả một mục được đặt tên và kiểm toán. Đây là nơi bạn gặp phải một vấn đề. Bạn có thể tạo một AuditedEntitylớp cơ sở, nhưng bạn không thể Threekế thừa cả hai AuditedEntity NamedEntity :

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

Tuy nhiên, bạn có thể nghĩ đến một cách giải quyết bằng cách AuditedEntitythừa kế từ NamedEntity. Đây là một cách hack thông minh để đảm bảo rằng mọi lớp chỉ cần kế thừa (trực tiếp) từ một lớp khác.

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

Điều này vẫn hoạt động. Nhưng những gì bạn đã làm ở đây được tuyên bố rằng mọi thực thể được kiểm toán vốn dĩ cũng là một thực thể được đặt tên . Điều này đưa tôi đến ví dụ cuối cùng của tôi. Fourlà một thực thể được kiểm toán nhưng không phải là một thực thể được đặt tên. Nhưng bạn không thể để Fourthừa kế từ đó AuditedEntitynhư bạn cũng sẽ biến nó thành NamedEntitydo sự kế thừa giữa AuditedEntity andNamedEntity`.

Sử dụng tính kế thừa, không có cách nào để tạo cả hai ThreeFourhoạt động trừ khi bạn bắt đầu sao chép các lớp (điều này mở ra một loạt các vấn đề mới).

Sử dụng giao diện, điều này có thể dễ dàng đạt được:

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

Hạn chế nhỏ duy nhất ở đây là bạn vẫn phải thực hiện giao diện. Nhưng bạn nhận được tất cả lợi ích từ việc có một loại có thể tái sử dụng chung, không có bất kỳ nhược điểm nào xuất hiện khi bạn cần các biến thể trên nhiều loại phổ biến cho một thực thể nhất định.

Nhưng tính đa hình của bạn vẫn còn nguyên:

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

Mặt khác, có một số lớp như vậy đơn giản là không có thuộc tính bổ sung.

Đây là một biến thể trên mẫu giao diện đánh dấu , trong đó bạn thực hiện một giao diện trống hoàn toàn để có thể sử dụng loại giao diện để kiểm tra xem một lớp nhất định có được "đánh dấu" với giao diện này không.
Bạn đang sử dụng các lớp kế thừa thay vì các giao diện được triển khai, nhưng mục tiêu là như nhau, vì vậy tôi sẽ gọi nó là "lớp được đánh dấu".

Theo mệnh giá, không có gì sai với các giao diện / lớp đánh dấu. Chúng có giá trị về mặt cú pháp và kỹ thuật, và không có nhược điểm cố hữu khi sử dụng chúng với điều kiện là điểm đánh dấu là hoàn toàn đúng (tại thời gian biên dịch) và không có điều kiện .

Đây chính xác là cách bạn nên phân biệt giữa các ngoại lệ khác nhau, ngay cả khi những ngoại lệ đó không thực sự có bất kỳ thuộc tính / phương thức bổ sung nào so với phương thức cơ sở.

Vì vậy, không có gì sai khi làm như vậy, nhưng tôi sẽ khuyên bạn nên thận trọng khi sử dụng, đảm bảo rằng bạn không chỉ đang cố che đậy một lỗi kiến ​​trúc hiện có với đa hình được thiết kế xấu.


4
"bạn chỉ có thể kế thừa từ một lớp, nhưng bạn có thể thực hiện nhiều giao diện." Tuy nhiên, điều đó không đúng với mọi ngôn ngữ. Một số ngôn ngữ cho phép kế thừa nhiều lớp.
Kenneth K.

như Kenneth đã nói, hai ngôn ngữ khá phổ biến có khả năng đa kế thừa là C ++ và Python. lớp threetrong ví dụ của bạn về những cạm bẫy của việc không sử dụng giao diện thực sự hợp lệ trong C ++, trên thực tế, điều này hoạt động đúng cho cả bốn ví dụ. Trong thực tế, tất cả điều này dường như là một hạn chế của C # như một ngôn ngữ chứ không phải là một câu chuyện cảnh báo về việc không sử dụng tính kế thừa khi bạn nên sử dụng giao diện.
whn

63

Đây là một cái gì đó mà tôi sử dụng để ngăn chặn đa hình được sử dụng.

Giả sử bạn có 15 lớp khác nhau có NamedEntitylớp cơ sở ở đâu đó trong chuỗi thừa kế của chúng và bạn đang viết một phương thức mới chỉ áp dụng cho OrderDateInfo.

Bạn "có thể" chỉ cần viết chữ ký là

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

Và hy vọng và cầu nguyện không ai lạm dụng hệ thống loại để đẩy một FooBazNamedEntitytrong đó.

Hoặc bạn "có thể" chỉ viết void MyMethod(OrderDateInfo foo). Bây giờ được thực thi bởi trình biên dịch. Đơn giản, thanh lịch và không dựa vào những người không phạm sai lầm.

Ngoài ra, như @candied_orange đã chỉ ra , các trường hợp ngoại lệ là một trường hợp tuyệt vời về điều này. Rất hiếm khi (và ý tôi là rất, rất, rất hiếm khi) bạn có bao giờ muốn nắm bắt mọi thứ với catch (Exception e). Nhiều khả năng bạn muốn bắt một SqlExceptionhoặc một FileNotFoundExceptionhoặc một ngoại lệ tùy chỉnh cho ứng dụng của bạn. Các lớp đó thường không cung cấp nhiều dữ liệu hoặc chức năng hơn Exceptionlớp cơ sở , nhưng chúng cho phép bạn phân biệt những gì chúng thể hiện mà không phải kiểm tra chúng và kiểm tra trường loại hoặc tìm kiếm văn bản cụ thể.

Nhìn chung, đó là một mẹo để có được hệ thống loại cho phép bạn sử dụng một bộ loại hẹp hơn so với bạn có thể nếu bạn sử dụng một lớp cơ sở. Ý tôi là, bạn có thể định nghĩa tất cả các biến và đối số của mình là có loại Object, nhưng điều đó sẽ khiến công việc của bạn khó khăn hơn, phải không?


4
Xin tha thứ cho sự mới mẻ của tôi nhưng tôi tò mò tại sao nó sẽ không phải là một cách tiếp cận tốt hơn để khiến OrderDateInfo CÓ một đối tượng NamedEntity làm tài sản?
Adam B

9
@AdamB Đó là một sự lựa chọn thiết kế hợp lệ. Cho dù nó tốt hơn hay không phụ thuộc vào những gì nó sẽ được sử dụng cho những thứ khác. Trong trường hợp ngoại lệ, tôi không thể tạo một lớp mới với thuộc tính ngoại lệ vì sau đó ngôn ngữ (C # cho tôi) sẽ không cho phép tôi ném nó. Có thể có những cân nhắc thiết kế khác sẽ ngăn cản việc sử dụng thành phần hơn là kế thừa. Nhiều lần thành phần là tốt hơn. Nó chỉ phụ thuộc vào hệ thống của bạn.
Becuzz

17
@AdamB Vì lý do tương tự rằng khi bạn thừa kế từ một lớp cơ sở và DO thêm các phương thức hoặc thuộc tính mà cơ sở thiếu, bạn không tạo một lớp mới và đặt lớp cơ sở làm tài sản. Có một sự khác biệt giữa một con người là một động vật có vú và có một động vật có vú.
Logarr

8
@Logarr đúng là có một sự khác biệt giữa tôi là một động vật có vú và có một động vật có vú. Nhưng nếu tôi trung thực với động vật có vú bên trong của tôi, bạn sẽ không bao giờ biết sự khác biệt. Bạn có thể tự hỏi tại sao tôi có thể cho rất nhiều loại sữa khác nhau trên một ý thích.
candied_orange

5
@AdamB Bạn có thể làm điều đó, và điều này được gọi là thừa kế thành phần. Nhưng bạn sẽ đổi tên NamedEntitycho điều này, ví dụ EntityName, để bố cục có ý nghĩa khi được mô tả.
Sebastian Redl

28

Đây là cách sử dụng kế thừa yêu thích của tôi. Tôi sử dụng nó chủ yếu cho các trường hợp ngoại lệ có thể sử dụng tên tốt hơn, cụ thể hơn

Vấn đề thông thường về thừa kế dẫn đến chuỗi dài và gây ra vấn đề yo-yo không áp dụng ở đây vì không có gì để thúc đẩy bạn xâu chuỗi.


1
Hmm, ngoại lệ điểm tốt. Tôi sẽ nói rằng đó là một điều khủng khiếp để làm. Nhưng bạn đã thuyết phục tôi bằng một trường hợp sử dụng tuyệt vời.
David Arno

Yo-Yo Ho và một chai rượu rum. Tis hiếm orlop là yar. Nó bị rò rỉ vì muốn gỗ sồi thích hợp tween các tấm ván. Đối với thợ khóa Davey Jones với những người làm đất đã xây dựng nó. DỊCH: Rất hiếm khi một chuỗi thừa kế tốt đến mức lớp cơ sở có thể bị bỏ qua một cách trừu tượng / hiệu quả.
radarbob

Tôi nghĩ rằng đáng để chỉ ra rằng một lớp mới kế thừa từ một lớp khác sẽ thêm một thuộc tính mới - tên của lớp mới. Đây chính xác là những gì bạn sử dụng khi tạo ngoại lệ với tên tốt hơn.
Pickup Logan

1

IMHO thiết kế lớp là sai. Nó nên

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoĐÃ A Namelà một mối quan hệ tự nhiên hơn và tạo ra hai lớp dễ hiểu mà sẽ không gây ra câu hỏi ban đầu.

Bất kỳ phương thức nào được chấp nhận NamedEntitynhư một tham số chỉ nên được quan tâm đến các thuộc tính IdNamecác thuộc tính, vì vậy bất kỳ phương thức nào như vậy nên được thay đổi để chấp nhận EntityNamethay thế.

Lý do kỹ thuật duy nhất tôi chấp nhận cho thiết kế ban đầu là vì ràng buộc tài sản, mà OP đã đề cập. Một khung crap sẽ không thể đối phó với tài sản thêm và liên kết với object.Name.Id. Nhưng nếu khung ràng buộc của bạn không thể đối phó với điều đó thì bạn có thêm một số nợ công nghệ để thêm vào danh sách.

Tôi sẽ đi cùng với câu trả lời của @ Flater, nhưng với rất nhiều giao diện chứa các thuộc tính, cuối cùng bạn sẽ viết rất nhiều mã soạn sẵn, ngay cả với các thuộc tính tự động đáng yêu của C #. Hãy tưởng tượng làm điều đó trong Java!


1

Các lớp phơi bày hành vi và Cấu trúc dữ liệu phơi bày dữ liệu.

Tôi thấy các từ khóa lớp, nhưng tôi không thấy bất kỳ hành vi nào. Điều này có nghĩa là tôi sẽ bắt đầu xem lớp này dưới dạng cấu trúc dữ liệu. Theo hướng này, tôi sẽ viết lại câu hỏi của bạn là

Tại sao có cấu trúc dữ liệu cấp cao nhất?

Vì vậy, bạn có thể sử dụng loại dữ liệu cấp cao nhất. Điều này cho phép tận dụng hệ thống loại để đảm bảo chính sách trên các tập hợp lớn các cấu trúc dữ liệu khác nhau bằng cách đảm bảo các thuộc tính đều ở đó.

Tại sao có cấu trúc dữ liệu bao gồm cấp cao nhất, nhưng không thêm gì?

Vì vậy, bạn có thể sử dụng loại dữ liệu cấp thấp hơn. Điều này cho phép đưa các gợi ý vào hệ thống gõ để thể hiện mục đích của biến

Top level data structure - Named
   property: name;

Bottom level data structure - Person

Trong hệ thống phân cấp ở trên, chúng tôi thấy thuận tiện khi chỉ định rằng một Người được đặt tên, vì vậy mọi người có thể lấy và thay đổi tên của họ. Mặc dù có thể hợp lý khi thêm các thuộc tính bổ sung vào Personcấu trúc dữ liệu, vấn đề mà chúng tôi đang giải quyết không yêu cầu một mô hình hoàn hảo về Người, và vì vậy chúng tôi đã bỏ qua việc thêm các thuộc tính chung như age, v.v.

Vì vậy, đây là một đòn bẩy của hệ thống gõ để thể hiện ý định của mục được đặt tên này theo cách không phá vỡ các bản cập nhật (như tài liệu) và có thể được gia hạn vào một ngày sau một cách dễ dàng (nếu bạn thấy bạn thực sự cần agetrường sau ).

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.