Tại sao không kế thừa từ Danh sách <T>?


1400

Khi lên kế hoạch cho các chương trình của mình, tôi thường bắt đầu với một chuỗi những suy nghĩ như vậy:

Một đội bóng chỉ là một danh sách các cầu thủ bóng đá. Vì vậy, tôi nên trình bày nó với:

var football_team = new List<FootballPlayer>();

Thứ tự của danh sách này thể hiện thứ tự mà người chơi được liệt kê trong danh sách.

Nhưng tôi nhận ra rằng các đội cũng có các thuộc tính khác, ngoài danh sách người chơi đơn thuần, phải được ghi lại. Ví dụ: tổng số điểm đang chạy trong mùa này, ngân sách hiện tại, màu đồng nhất, stringđại diện cho tên của đội, v.v.

Vì vậy, sau đó tôi nghĩ:

Được rồi, một đội bóng đá giống như một danh sách các cầu thủ, nhưng ngoài ra, nó có một tên (a string) và tổng số điểm (an int) đang chạy . .NET không cung cấp một lớp để lưu trữ các đội bóng đá, vì vậy tôi sẽ tạo một lớp của riêng mình. Cấu trúc hiện có tương tự và phù hợp nhất là List<FootballPlayer>, vì vậy tôi sẽ kế thừa từ nó:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

Nhưng nó chỉ ra rằng một hướng dẫn nói rằng bạn không nên thừa kế từList<T> . Tôi hoàn toàn bối rối bởi hướng dẫn này trong hai khía cạnh.

Tại sao không?

Rõ ràng Listlà bằng cách nào đó được tối ưu hóa cho hiệu suất . Làm sao vậy Những vấn đề hiệu suất tôi sẽ gây ra nếu tôi mở rộng List? Chính xác thì cái gì sẽ phá vỡ?

Một lý do khác mà tôi thấy là Listdo Microsoft cung cấp và tôi không có quyền kiểm soát đối với nó, vì vậy tôi không thể thay đổi nó sau này, sau khi tiết lộ "API công khai" . Nhưng tôi đấu tranh để hiểu điều này. API công khai là gì và tại sao tôi nên quan tâm? Nếu dự án hiện tại của tôi không và không có khả năng có API công khai này, tôi có thể bỏ qua hướng dẫn này một cách an toàn không? Nếu tôi thừa kế từ List hóa ra tôi cần API công khai, tôi sẽ gặp khó khăn gì?

Tại sao nó thậm chí còn quan trọng? Một danh sách là một danh sách. Điều gì có thể thay đổi? Tôi có thể muốn thay đổi điều gì?

Và cuối cùng, nếu Microsoft không muốn tôi thừa kế List, tại sao họ không tạo ra lớp học sealed?

Tôi còn phải sử dụng cái gì nữa?

Rõ ràng, đối với các bộ sưu tập tùy chỉnh, Microsoft đã cung cấp một Collectionlớp nên được mở rộng thay vì List. Nhưng lớp này rất trống trải và không có nhiều thứ hữu ích, chẳng hạn nhưAddRange . Câu trả lời của jvitor83 cung cấp một lý do hiệu suất cho phương pháp cụ thể đó, nhưng làm thế nào chậm AddRangekhông tốt hơn không AddRange?

Kế thừa từ Collectioncách làm việc nhiều hơn là kế thừa từ List, và tôi thấy không có lợi ích gì. Chắc chắn Microsoft sẽ không bảo tôi làm thêm mà không có lý do, vì vậy tôi không thể cảm thấy như mình đang hiểu nhầm điều gì đó và kế thừa Collectionthực sự không phải là giải pháp phù hợp cho vấn đề của tôi.

Tôi đã thấy các đề xuất như thực hiện IList. Không. Đây là hàng tá dòng mã soạn sẵn mà tôi chẳng thu được gì.

Cuối cùng, một số gợi ý gói Listtrong một cái gì đó:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Có hai vấn đề này:

  1. Nó làm cho mã của tôi dài dòng không cần thiết. Bây giờ tôi phải gọi my_team.Players.Countthay vì chỉ my_team.Count. Rất may, với C # tôi có thể định nghĩa các bộ chỉ mục để làm cho việc lập chỉ mục trở nên minh bạch và chuyển tiếp tất cả các phương thức của nội bộ List... Nhưng đó là rất nhiều mã! Tôi nhận được gì cho tất cả công việc đó?

  2. Nó chỉ đơn giản là không có ý nghĩa gì. Một đội bóng đá không "có" một danh sách các cầu thủ. Đây danh sách các cầu thủ. Bạn không nói "John McFootballer đã tham gia với người chơi của someTeam". Bạn nói "John đã tham gia vào một sốTeam". Bạn không thêm chữ cái vào "ký tự của chuỗi", bạn thêm chữ cái vào chuỗi. Bạn không thêm sách vào sách của thư viện, bạn thêm sách vào thư viện.

Tôi nhận ra rằng những gì xảy ra "dưới mui xe" có thể nói là "thêm X vào danh sách nội bộ của Y", nhưng đây có vẻ như là một cách suy nghĩ rất trực quan về thế giới.

Câu hỏi của tôi (tóm tắt)

Cách C # chính xác để biểu diễn một cấu trúc dữ liệu, trong đó, "về mặt logic" (nghĩa là "với tâm trí con người") chỉ là một listtrong thingsnhững tiếng chuông và tiếng huýt sáo?

Là thừa kế từ List<T>luôn luôn không thể chấp nhận? Khi nào nó được chấp nhận? Tại sao tại sao không? Một lập trình viên phải xem xét điều gì, khi quyết định có nên thừa kế từ List<T>hay không?


90
Vì vậy, câu hỏi của bạn thực sự là khi nào nên sử dụng tính kế thừa (Hình vuông là hình chữ nhật vì luôn cung cấp Hình vuông khi hình chữ nhật chung được yêu cầu) và khi nào nên sử dụng bố cục (Ngoài ra, còn có Danh sách FootballTeam đối với các thuộc tính khác chỉ là "cơ bản" đối với nó, như tên)? Các lập trình viên khác có bị nhầm lẫn nếu ở nơi khác trong mã bạn đã chuyển cho họ một FootballTeam khi họ đang mong đợi một Danh sách đơn giản không?
Chuyến bay Odyssey

77
Điều gì sẽ xảy ra nếu tôi nói với bạn rằng một đội bóng đá là một công ty kinh doanh sở hữu một danh sách các hợp đồng cầu thủ thay vì một danh sách các cầu thủ với một số tài sản bổ sung?
STT LCU

75
Nó thực sự khá đơn giản. IS một đội bóng đá một danh sách các cầu thủ? Rõ ràng là không, vì như bạn nói, có những thuộc tính khác có liên quan. Có một đội bóng đá CÓ một danh sách các cầu thủ? Vâng, vì vậy nó nên chứa một danh sách. Ngoài ra, thành phần nói chung chỉ thích kế thừa hơn, bởi vì nó dễ sửa đổi sau này. Nếu bạn thấy sự kế thừa và thành phần hợp lý cùng một lúc, hãy đi với thành phần.
Tobberoth

45
@FlightOdyssey: Giả sử bạn có một phương thức SquashR Hình chữ nhật lấy một hình chữ nhật, giảm một nửa chiều cao và tăng gấp đôi chiều rộng của nó. Bây giờ vượt qua trong một hình chữ nhật xảy ra là 4 x 4 nhưng là loại hình chữ nhật, và một hình vuông là 4 x 4. Các kích thước của đối tượng sau khi SquashRonymous được gọi là gì? Đối với hình chữ nhật rõ ràng là 2x8. Nếu hình vuông là 2x8 thì nó không còn là hình vuông nữa; nếu nó không phải là 2x8 thì thao tác tương tự trên cùng một hình sẽ tạo ra kết quả khác. Kết luận là một hình vuông có thể thay đổi không phải là một loại hình chữ nhật.
Eric Lippert

29
@Gangnus: Tuyên bố của bạn đơn giản là sai; một lớp con thực sự được yêu cầu phải có chức năng là siêu lớp của siêu lớp. A stringlà cần thiết để làm mọi thứ objectcó thể làm và nhiều hơn nữa .
Eric Lippert

Câu trả lời:


1566

Có một số câu trả lời tốt ở đây. Tôi sẽ thêm cho họ những điểm sau.

Cách C # chính xác để biểu diễn một cấu trúc dữ liệu, trong đó, "về mặt logic" (nghĩa là "với tâm trí con người") chỉ là một danh sách các thứ có một vài tiếng chuông và còi?

Yêu cầu bất kỳ mười người không lập trình viên máy tính nào quen thuộc với sự tồn tại của bóng đá để điền vào chỗ trống:

Một đội bóng đá là một loại đặc biệt của _____

ai nói "danh sách các cầu thủ bóng đá có một vài tiếng chuông và còi", hoặc tất cả họ đã nói "đội thể thao" hay "câu lạc bộ" hoặc "tổ chức"? Quan niệm của bạn rằng một đội bóng đá là một loại danh sách cầu thủ đặc biệt nằm trong tâm trí con người và tâm trí con người của bạn.

List<T>là một cơ chế . Đội bóng đá là một đối tượng kinh doanh - nghĩa là một đối tượng đại diện cho một số khái niệm nằm trong lĩnh vực kinh doanh của chương trình. Đừng trộn lẫn chúng! Một đội bóng đá là một loại đội; nó có một danh sách, một danh sách là một danh sách các cầu thủ . Danh sách không phải là một loại danh sách người chơi cụ thể . Một danh sách một danh sách các cầu thủ. Vì vậy, làm cho một tài sản được gọi Rosterlà a List<Player>. Và làm điều đó ReadOnlyList<Player>trong khi bạn đang ở đó, trừ khi bạn tin rằng mọi người biết về một đội bóng sẽ xóa các cầu thủ khỏi danh sách.

Là thừa kế từ List<T>luôn luôn không thể chấp nhận?

Không thể chấp nhận với ai? Tôi? Không.

Khi nào nó được chấp nhận?

Khi bạn đang xây dựng một cơ chế mở rộng List<T>cơ chế .

Một lập trình viên phải xem xét điều gì, khi quyết định có nên thừa kế từ List<T>hay không?

Tôi đang xây dựng một cơ chế hay một đối tượng kinh doanh ?

Nhưng đó là rất nhiều mã! Tôi nhận được gì cho tất cả công việc đó?

Bạn đã dành nhiều thời gian hơn để gõ lên câu hỏi của bạn rằng nó sẽ khiến bạn phải viết phương thức chuyển tiếp cho các thành viên có liên quan trong List<T>năm mươi lần. Bạn rõ ràng không sợ tính dài dòng và chúng tôi đang nói về một lượng mã rất nhỏ ở đây; đây là một vài phút làm việc

CẬP NHẬT

Tôi đã suy nghĩ thêm và có một lý do khác để không mô hình một đội bóng đá như một danh sách các cầu thủ. Trong thực tế, nó có thể là một ý tưởng tồi để mô hình một đội bóng đá vì một danh sách các cầu thủ quá. Vấn đề với một đội khi có một danh sách các cầu thủ là những gì bạn có là một ảnh chụp nhanh của đội tại một thời điểm . Tôi không biết trường hợp kinh doanh của bạn là gì đối với lớp này, nhưng nếu tôi có một lớp đại diện cho một đội bóng đá, tôi sẽ muốn hỏi nó những câu hỏi như "có bao nhiêu cầu thủ Seahawks đã bỏ lỡ các trận đấu do chấn thương từ năm 2003 đến 2013?" hoặc "Người chơi Denver nào trước đây từng chơi cho một đội khác có số lần tăng sân nhiều nhất trong năm đã chạy?" hoặc " Piggers có đi hết con đường trong năm nay không? "

Đó là, một đội bóng đá đối với tôi dường như được mô phỏng tốt như một tập hợp các sự kiện lịch sử như khi một cầu thủ được tuyển dụng, bị thương, nghỉ hưu, v.v ... Rõ ràng đội hình cầu thủ hiện tại là một sự thật quan trọng có lẽ nên được đưa ra trước và trung tâm, nhưng có thể có những điều thú vị khác mà bạn muốn làm với đối tượng này đòi hỏi một viễn cảnh lịch sử hơn.


84
Mặc dù các câu trả lời khác rất hữu ích, tôi nghĩ rằng câu trả lời này giải quyết mối quan tâm của tôi trực tiếp nhất. Đối với việc phóng đại số lượng mã, bạn đúng là cuối cùng nó không hoạt động nhiều, nhưng tôi rất dễ nhầm lẫn khi tôi đưa một số mã vào chương trình của mình mà không hiểu tại sao tôi lại đưa nó vào.
Tuyệt vời nhất

128
@Superbest: Vui mừng tôi có thể giúp! Bạn có quyền lắng nghe những nghi ngờ đó; nếu bạn không hiểu tại sao bạn viết một số mã, hãy tìm ra nó hoặc viết mã khác mà bạn hiểu.
Eric Lippert

48
@Mehrdad Nhưng bạn dường như quên mất các quy tắc tối ưu hóa vàng: 1) không làm điều đó, 2) chưa làm và 3) không làm điều đó mà không thực hiện hồ sơ hiệu suất để hiển thị những gì cần tối ưu hóa.
AJMansfield

107
@Mehrdad: Thành thật mà nói, nếu ứng dụng của bạn yêu cầu bạn quan tâm đến gánh nặng hiệu năng của các phương thức ảo, thì bất kỳ ngôn ngữ hiện đại nào (C #, Java, v.v.) không phải là ngôn ngữ dành cho bạn. Tôi có một máy tính xách tay ngay tại đây mà có thể chạy một mô phỏng của mỗi trò chơi arcade tôi đã chơi như một đứa trẻ cùng một lúc . Điều này cho phép tôi không lo lắng về một mức độ gián tiếp ở đây và đó.
Eric Lippert

44
@Superbest: Bây giờ chúng tôi chắc chắn ở cuối "thành phần không kế thừa" của phổ. Một tên lửa bao gồm các bộ phận tên lửa . Một tên lửa không phải là "một loại danh sách các bộ phận đặc biệt"! Tôi sẽ đề nghị với bạn rằng bạn cắt nó xuống hơn nữa; tại sao một danh sách, trái ngược với một IEnumerable<T>- một chuỗi ?
Eric Lippert

161

Wow, bài viết của bạn có một loạt các câu hỏi và điểm. Hầu hết các lý do bạn nhận được từ Microsoft là chính xác. Hãy bắt đầu với mọi thứ vềList<T>

  • List<T> được tối ưu hóa cao. Cách sử dụng chính của nó là được sử dụng như một thành viên riêng của một đối tượng.
  • Microsoft đã không niêm phong nó bởi vì đôi khi bạn có thể muốn tạo một lớp có tên thân thiện hơn : class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Bây giờ thì dễ như làm var list = new MyList<int, string>();.
  • CA1002: Không để lộ danh sách chung : Về cơ bản, ngay cả khi bạn dự định sử dụng ứng dụng này với tư cách là nhà phát triển duy nhất, thì cũng đáng để phát triển với các thực tiễn mã hóa tốt, vì vậy chúng trở nên thấm nhuần vào bạn và bản chất thứ hai. Bạn vẫn được phép hiển thị danh sách IList<T>nếu bạn cần bất kỳ người tiêu dùng nào có danh sách được lập chỉ mục. Điều này cho phép bạn thay đổi việc thực hiện trong một lớp học sau này.
  • Microsoft đã tạo ra Collection<T>rất chung chung vì đó là một khái niệm chung chung ... cái tên nói lên tất cả; nó chỉ là một bộ sưu tập Có phiên bản chính xác hơn ví dụ như SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>vv mỗi trong số đó thực hiện IList<T>nhưng không List<T> .
  • Collection<T>cho phép các thành viên (ví dụ Thêm, Xóa, v.v.) bị ghi đè vì chúng là ảo. List<T>không làm.
  • Phần cuối cùng của câu hỏi của bạn là tại chỗ. Một đội bóng đá không chỉ là một danh sách các cầu thủ, vì vậy nó phải là một lớp chứa danh sách các cầu thủ đó. Hãy suy nghĩ Thành phần vs Kế thừa . Một đội bóng đá một danh sách các cầu thủ (một danh sách), đó không phải là một danh sách các cầu thủ.

Nếu tôi viết mã này, lớp có thể sẽ trông giống như vậy:

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

6
" Bài đăng của bạn có toàn bộ câu hỏi " - Chà, tôi đã nói rằng tôi hoàn toàn bối rối. Cảm ơn bạn đã liên kết với câu hỏi của @ Readonly, bây giờ tôi thấy rằng một phần lớn câu hỏi của tôi là một trường hợp đặc biệt của vấn đề Thành phần so với Kế thừa.
Tuyệt vời nhất

3
@Brian: Ngoại trừ việc bạn không thể tạo chung bằng cách sử dụng bí danh, tất cả các loại phải cụ thể. Trong ví dụ của tôi, List<T>được mở rộng như một lớp bên trong riêng sử dụng các tổng quát để đơn giản hóa việc sử dụng và khai báo List<T>. Nếu phần mở rộng chỉ đơn thuần class FootballRoster : List<FootballPlayer> { }, thì có, tôi có thể đã sử dụng một bí danh thay thế. Tôi đoán rằng đáng lưu ý rằng bạn có thể thêm các thành viên / chức năng bổ sung, nhưng nó bị hạn chế vì bạn không thể ghi đè các thành viên quan trọng của List<T>.
tôi

6
Nếu đây là Lập trình viên. Tôi, tôi đồng ý với phạm vi phiếu trả lời hiện tại, nhưng, vì đó là một bài viết SO, câu trả lời này ít nhất là cố gắng trả lời các vấn đề cụ thể của C # (.NET), mặc dù có chung chung vấn đề thiết kế.
Đánh dấu Hurd

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenBây giờ đó là một lý do tốt để không kế thừa từ Danh sách. Những câu trả lời khác có quá nhiều triết lý.
Navin

10
@my: Tôi nghi ngờ bạn có nghĩa là "hàng loạt" câu hỏi, vì một thám tử là một thám tử.
batty

152
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Mã trước có nghĩa là: một nhóm những người từ đường phố chơi bóng đá, và họ tình cờ có một cái tên. Cái gì đó như:

Người chơi bóng đá

Dù sao, mã này (từ câu trả lời của tôi)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

Phương tiện: đây là một đội bóng đá có quản lý, cầu thủ, quản trị viên, v.v ... Đại loại như:

Hình ảnh hiển thị các thành viên (và các thuộc tính khác) của đội Manchester United

Đây là cách logic của bạn được thể hiện trong hình ảnh


9
Tôi hy vọng rằng ví dụ thứ hai của bạn là Câu lạc bộ bóng đá, không phải đội bóng đá. Một câu lạc bộ bóng đá có người quản lý và quản trị viên, v.v ... Từ nguồn này : "Một đội bóng đá là tên tập thể được đặt cho một nhóm cầu thủ ... Những đội như vậy có thể được chọn để chơi trong trận đấu với đội đối thủ, để đại diện cho một câu lạc bộ bóng đá ... ". Vì vậy, theo ý kiến ​​của tôi, một đội một danh sách các cầu thủ (hoặc có lẽ chính xác hơn là một bộ sưu tập các cầu thủ).
Ben

3
private readonly List<T> _roster = new List<T>(44);
Tối

2
Cần nhập System.Linqkhông gian tên.
Davide Cannizzo

1
Đối với hình ảnh: +1
V.7

115

Đây là một ví dụ cổ điển về thành phần so với thừa kế .

Trong trường hợp cụ thể này:

Đội có một danh sách các cầu thủ có hành vi được thêm vào không

hoặc là

Có phải đội là một đối tượng của riêng mình mà có chứa một danh sách các cầu thủ.

Bằng cách mở rộng Danh sách, bạn đang giới hạn bản thân theo một số cách:

  1. Bạn không thể hạn chế quyền truy cập (ví dụ: dừng mọi người thay đổi danh sách). Bạn nhận được tất cả các phương thức Danh sách cho dù bạn cần / muốn tất cả chúng hay không.

  2. Điều gì xảy ra nếu bạn muốn có danh sách những thứ khác là tốt. Ví dụ, các đội có huấn luyện viên, người quản lý, người hâm mộ, thiết bị, v.v ... Một số trong số đó có thể được liệt kê theo quyền riêng của họ.

  3. Bạn giới hạn các lựa chọn của bạn cho thừa kế. Ví dụ: bạn có thể muốn tạo một đối tượng Đội chung, và sau đó có SoccerTeam, FootballTeam, v.v. kế thừa từ đó. Để kế thừa từ Danh sách, bạn cần thực hiện kế thừa từ Nhóm, nhưng điều đó có nghĩa là tất cả các loại nhóm khác nhau buộc phải có cùng triển khai danh sách đó.

Thành phần - bao gồm một đối tượng đưa ra hành vi bạn muốn bên trong đối tượng của bạn.

Kế thừa - đối tượng của bạn trở thành một thể hiện của đối tượng có hành vi bạn muốn.

Cả hai đều có công dụng của chúng, nhưng đây là một trường hợp rõ ràng trong đó thành phần là thích hợp hơn.


1
Mở rộng trên # 2, không có nghĩa là Danh sách phải có - Danh sách.
Cruncher

Đầu tiên, câu hỏi không liên quan gì đến thành phần so với kế thừa. Câu trả lời là OP không muốn triển khai một loại danh sách cụ thể hơn, vì vậy anh không nên mở rộng Danh sách <>. Tôi vặn vẹo vì bạn có điểm số rất cao trên stackoverflow và nên biết rõ điều này và mọi người tin tưởng vào những gì bạn nói, vì vậy bây giờ chỉ có 55 người ủng hộ và ý tưởng của họ bị nhầm lẫn tin rằng cách này hay cách khác đều ổn để xây dựng một hệ thống nhưng rõ ràng là không! # không giận dữ
Mauro Sampietro

6
@sam Anh muốn hành vi của danh sách. Anh ta có hai lựa chọn, anh ta có thể mở rộng danh sách (thừa kế) hoặc anh ta có thể bao gồm một danh sách bên trong đối tượng của mình (thành phần). Có lẽ bạn đã hiểu nhầm một phần câu hỏi hoặc câu trả lời thay vì 55 người sai và bạn đúng? :)
Tim B

4
Đúng. Đó là một trường hợp thoái hóa chỉ có một siêu và phụ nhưng tôi đang đưa ra lời giải thích tổng quát hơn cho câu hỏi cụ thể của mình. Anh ta có thể chỉ có một nhóm và có thể luôn có một danh sách nhưng lựa chọn vẫn là kế thừa danh sách hoặc đưa nó vào danh sách đối tượng. Khi bạn bắt đầu bao gồm nhiều loại đội (bóng đá. Cricket. Vv) và họ bắt đầu không chỉ là một danh sách các cầu thủ mà bạn thấy bạn có toàn bộ thành phần so với câu hỏi kế thừa. Nhìn vào bức tranh lớn là rất quan trọng trong các trường hợp như thế này để tránh những lựa chọn sai lầm sớm có nghĩa là rất nhiều tái cấu trúc sau này.
Tim B

3
OP không yêu cầu Thành phần, nhưng trường hợp sử dụng là một ví dụ rõ ràng về vấn đề X: Y trong đó một trong 4 nguyên tắc lập trình hướng đối tượng bị phá vỡ, sử dụng sai hoặc không được hiểu đầy đủ. Câu trả lời rõ ràng hơn là viết một bộ sưu tập chuyên biệt như Stack hoặc Queue (không xuất hiện để phù hợp với trường hợp sử dụng) hoặc để hiểu thành phần. Một đội bóng đá KHÔNG phải là một danh sách các cầu thủ bóng đá. Cho dù OP yêu cầu rõ ràng hay không là không liên quan, nếu không hiểu về Trừu tượng, Đóng gói, Kế thừa và Đa hình, họ sẽ không hiểu câu trả lời, ergo, X: Y amok
Julia McGuigan

67

Như mọi người đã chỉ ra, một nhóm các cầu thủ không phải là một danh sách các cầu thủ. Sai lầm này được thực hiện bởi nhiều người ở khắp mọi nơi, có lẽ ở nhiều cấp độ chuyên môn. Thường thì vấn đề là tinh tế và đôi khi rất thô thiển, như trong trường hợp này. Thiết kế như vậy là xấu vì những điều này vi phạm Nguyên tắc thay thế Liskov . Internet có nhiều bài viết hay giải thích khái niệm này, ví dụ: http://en.wikipedia.org/wiki/Liskov_substlation_principl

Tóm lại, có hai quy tắc được bảo tồn trong mối quan hệ Cha mẹ / Con cái giữa các lớp:

  • Một đứa trẻ không đòi hỏi phải có đặc điểm ít hơn những gì hoàn toàn xác định Cha mẹ.
  • Phụ huynh không yêu cầu đặc điểm nào ngoài những gì định nghĩa hoàn toàn về Trẻ.

Nói cách khác, Cha mẹ là một định nghĩa cần thiết về một đứa trẻ, và một đứa trẻ là một định nghĩa đầy đủ về Cha mẹ.

Dưới đây là một cách để suy nghĩ thông qua các giải pháp và áp dụng nguyên tắc trên sẽ giúp người ta tránh được một sai lầm như vậy. Người ta nên kiểm tra giả thuyết bằng cách xác minh xem tất cả các hoạt động của lớp cha có hợp lệ đối với lớp dẫn xuất cả về cấu trúc và ngữ nghĩa hay không.

  • Là một đội bóng đá một danh sách các cầu thủ bóng đá? (Làm tất cả các thuộc tính của danh sách áp dụng cho một nhóm có cùng ý nghĩa)
    • Là một nhóm một tập hợp các thực thể đồng nhất? Vâng, đội là một tập hợp các cầu thủ
    • Là thứ tự bao gồm các cầu thủ mô tả trạng thái của đội và đội có đảm bảo rằng chuỗi được bảo tồn trừ khi thay đổi rõ ràng không? Không và không
    • Những người chơi dự kiến ​​sẽ được bao gồm / loại bỏ dựa trên vị trí tiếp theo của họ trong đội? Không

Như bạn thấy, chỉ có đặc điểm đầu tiên của danh sách được áp dụng cho một nhóm. Do đó một nhóm không phải là một danh sách. Một danh sách sẽ là một chi tiết triển khai về cách bạn quản lý nhóm của mình, vì vậy nó chỉ nên được sử dụng để lưu trữ các đối tượng người chơi và được thao tác với các phương thức của lớp Team.

Tại thời điểm này, tôi muốn nhận xét rằng một lớp Nhóm nên, theo tôi, thậm chí không được triển khai bằng Danh sách; nó nên được thực hiện bằng cách sử dụng cấu trúc dữ liệu Set (ví dụ Hashset) trong hầu hết các trường hợp.


8
Bắt tốt trong Danh sách so với Bộ. Đó dường như là một lỗi chỉ quá phổ biến. Khi chỉ có thể có một phiên bản của một thành phần trong bộ sưu tập, một số loại tập hợp hoặc từ điển sẽ là một ứng cử viên ưa thích để thực hiện. Có thể có một đội có hai người chơi cùng tên. Sẽ không ổn nếu có một người chơi bao gồm hai lần cùng một lúc.
Fred Mitchell

1
+1 cho một số điểm tốt, mặc dù điều quan trọng hơn là hỏi "cấu trúc dữ liệu có thể hỗ trợ hợp lý mọi thứ hữu ích (lý tưởng là ngắn hạn và dài hạn)", thay vì "cấu trúc dữ liệu có làm được nhiều hơn là hữu ích không".
Tony Delroy

1
@TonyD Chà, những điểm tôi đang nêu ra không phải là người ta nên kiểm tra "nếu cấu trúc dữ liệu làm nhiều hơn những gì hữu ích". Đó là kiểm tra "Nếu cấu trúc dữ liệu của Cha làm một việc gì đó không liên quan, vô nghĩa hoặc phản trực giác đối với hành vi mà lớp Con có thể ám chỉ".
Satyan Raina

1
@TonyD Thực sự có một vấn đề với việc có các đặc điểm không liên quan xuất phát từ Cha mẹ vì nó sẽ thất bại trong các thử nghiệm âm tính trong nhiều trường hợp. Một lập trình viên có thể mở rộng Human {eat (); chạy(); write ();} từ Gorilla {eat (); chạy(); swing ();} nghĩ rằng không có gì sai với con người với một tính năng bổ sung là có thể xoay. Và sau đó trong một thế giới trò chơi, con người của bạn đột nhiên bắt đầu vượt qua mọi rào cản được cho là bằng cách chỉ đu trên cây. Trừ khi được chỉ định rõ ràng, một con người thực tế sẽ không thể xoay. Thiết kế như vậy khiến api rất cởi mở để lạm dụng và khó hiểu
Satyan Raina

1
@TonyD Tôi không gợi ý rằng lớp Người chơi nên được bắt nguồn từ Hashset. Tôi đề nghị lớp Người chơi nên 'trong hầu hết các trường hợp' nên được triển khai bằng Hashset thông qua Thành phần và đó hoàn toàn là một chi tiết cấp độ triển khai, không phải là cấp độ thiết kế (đó là lý do tại sao tôi đề cập đến nó như là một phụ đề cho câu trả lời của tôi). Nó rất có thể được thực hiện bằng cách sử dụng một danh sách nếu có bằng chứng hợp lệ cho việc thực hiện đó. Vì vậy, để trả lời câu hỏi của bạn, Có cần phải tra cứu O (1) bằng phím không? Không. Do đó, người ta KHÔNG nên mở rộng Trình phát từ Hashset.
Satyan Raina

50

Điều gì nếu FootballTeamcó một đội dự bị cùng với đội chính?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

Làm thế nào bạn sẽ mô hình hóa với?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

Mối quan hệ rõ ràng và không phải là một .

hay RetiredPlayers?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

Theo nguyên tắc thông thường, nếu bạn muốn kế thừa từ bộ sưu tập, hãy đặt tên cho lớp SomethingCollection .

Liệu SomethingCollectionngữ nghĩa của bạn có ý nghĩa? Chỉ làm điều này nếu loại của bạn là một bộ sưu tậpSomething .

Trong trường hợp FootballTeamnó không đúng. A Teamlà nhiều hơn a Collection. MộtTeam có thể có huấn luyện viên, huấn luyện viên, vv như các câu trả lời khác đã chỉ ra.

FootballCollection Âm thanh như một bộ sưu tập bóng đá hoặc có thể là một bộ sưu tập đồ dùng bóng đá. TeamCollection, một bộ sưu tập của các đội.

FootballPlayerCollection Nghe có vẻ như một tập hợp những người chơi sẽ là một tên hợp lệ cho một lớp kế thừa từ List<FootballPlayer> nếu bạn thực sự muốn làm điều đó.

Thực sự List<FootballPlayer>là một loại hoàn toàn tốt để đối phó. Có lẽIList<FootballPlayer> nếu bạn đang trả lại nó từ một phương thức.

Tóm tắt

Tự hỏi mình đi

  1. X một Y? hoặc X một Y?

  2. Tên lớp của tôi có nghĩa là những gì họ đang có?


Nếu loại của mỗi cầu thủ sẽ phân loại nó là thuộc về một trong hai DefensivePlayers, OffensivePlayershoặc OtherPlayers, nó có thể là hợp pháp hữu ích để có một loại mà có thể được sử dụng bởi mã mà hy vọng một List<Player>nhưng các thành viên cũng bao gồm DefensivePlayers, OffsensivePlayershoặc SpecialPlayerscác loại IList<DefensivePlayer>, IList<OffensivePlayer>IList<Player>. Người ta có thể sử dụng một đối tượng riêng biệt để lưu trữ các danh sách riêng biệt, nhưng đóng gói chúng trong cùng một đối tượng vì danh sách chính sẽ có vẻ sạch hơn [sử dụng tính hợp lệ của một điều tra viên danh sách ...
supercat

... Như một gợi ý cho thực tế là danh sách chính đã thay đổi và danh sách phụ sẽ cần được tạo lại khi chúng được truy cập tiếp theo].
supercat

2
Mặc dù tôi đồng ý với quan điểm đã đưa ra, nhưng tôi thực sự cảm thấy xót xa khi thấy ai đó đưa ra lời khuyên thiết kế và đề nghị đưa ra một Danh sách cụ thể <T> với một getter công khai và setter trong một đối tượng kinh doanh :(
sara

38

Thiết kế> Thực hiện

Những phương pháp và tính chất bạn đưa ra là một quyết định thiết kế. Lớp cơ sở nào bạn kế thừa từ một chi tiết triển khai. Tôi cảm thấy đáng để lùi một bước về trước.

Một đối tượng là một tập hợp dữ liệu và hành vi.

Vì vậy, câu hỏi đầu tiên của bạn nên là:

  • Dữ liệu nào mà đối tượng này bao gồm trong mô hình tôi đang tạo?
  • Hành vi nào mà đối tượng này thể hiện trong mô hình đó?
  • Làm thế nào điều này có thể thay đổi trong tương lai?

Hãy nhớ rằng sự kế thừa ngụ ý một mối quan hệ "isa" (là một), trong khi thành phần ngụ ý một mối quan hệ "có một" (hasa). Chọn một tình huống phù hợp với tình huống của bạn theo quan điểm của bạn, ghi nhớ nơi mọi thứ có thể diễn ra khi ứng dụng của bạn phát triển.

Cân nhắc suy nghĩ trong các giao diện trước khi bạn nghĩ về các loại cụ thể, vì một số người thấy việc đưa bộ não của họ vào "chế độ thiết kế" theo cách đó dễ dàng hơn.

Đây không phải là điều mà tất cả mọi người có ý thức ở cấp độ này trong mã hóa hàng ngày. Nhưng nếu bạn nghiền ngẫm loại chủ đề này, bạn sẽ bước vào vùng nước thiết kế. Nhận thức được nó có thể được giải phóng.

Xem xét thiết kế cụ thể

Hãy xem Danh sách <T> và IList <T> trên MSDN hoặc Visual Studio. Xem những phương pháp và tính chất họ tiếp xúc. Tất cả các phương thức này có giống như một cái gì đó mà ai đó muốn làm với FootballTeam theo quan điểm của bạn không?

FootballTeam.Reverse () có ý nghĩa với bạn không? Bóng đáTeam.Convert ALL <TOutput> () có giống như thứ bạn muốn không?

Đây không phải là một câu hỏi mẹo; câu trả lời thực sự có thể là "có". Nếu bạn triển khai / kế thừa Danh sách <Người chơi> hoặc IList <Người chơi>, bạn sẽ bị mắc kẹt với chúng; Nếu đó là lý tưởng cho mô hình của bạn, hãy làm điều đó.

Nếu bạn quyết định có, điều đó hợp lý và bạn muốn đối tượng của mình có thể được coi là một bộ sưu tập / danh sách người chơi (hành vi) và do đó bạn muốn thực hiện ICollection hoặc IList, bằng mọi cách, hãy làm như vậy. Về mặt tình cảm:

class FootballTeam : ... ICollection<Player>
{
    ...
}

Nếu bạn muốn đối tượng của mình chứa bộ sưu tập / danh sách người chơi (dữ liệu) và do đó bạn muốn bộ sưu tập hoặc danh sách là tài sản hoặc thành viên, bằng mọi cách, hãy làm như vậy. Về mặt tình cảm:

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

Bạn có thể cảm thấy rằng bạn muốn mọi người chỉ có thể liệt kê nhóm người chơi, thay vì đếm họ, thêm vào họ hoặc xóa họ. IEnumerable <Player> là một tùy chọn hoàn toàn hợp lệ để xem xét.

Bạn có thể cảm thấy rằng không có giao diện nào trong số này có ích trong mô hình của bạn cả. Điều này ít có khả năng (IEnumerable <T> hữu ích trong nhiều tình huống) nhưng vẫn có thể.

Bất cứ ai cố gắng nói với bạn rằng một trong những điều này là sai về mặt phân loại và dứt khoát trong mọi trường hợp là sai lầm. Bất cứ ai cố gắng nói với bạn điều đó là đúng đắn và dứt khoát trong mọi trường hợp đều sai lầm.

Chuyển sang thực hiện

Khi bạn đã quyết định về dữ liệu và hành vi, bạn có thể đưa ra quyết định về việc triển khai. Điều này bao gồm các lớp cụ thể mà bạn phụ thuộc vào thông qua kế thừa hoặc thành phần.

Đây có thể không phải là một bước tiến lớn và mọi người thường kết hợp thiết kế và triển khai vì hoàn toàn có thể chạy qua tất cả trong đầu bạn trong một hoặc hai giây và bắt đầu gõ đi.

Một thí nghiệm suy nghĩ

Một ví dụ giả tạo: như những người khác đã đề cập, một đội không phải lúc nào cũng "chỉ" một bộ sưu tập các cầu thủ. Bạn có duy trì một bộ sưu tập điểm số trận đấu cho đội? Là đội có thể hoán đổi với câu lạc bộ, trong mô hình của bạn? Nếu vậy, và nếu đội của bạn là một bộ sưu tập các cầu thủ, có lẽ đó cũng là một bộ sưu tập nhân viên và / hoặc một bộ sưu tập điểm số. Sau đó, bạn kết thúc với:

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

Mặc dù thiết kế, tại thời điểm này trong C #, bạn sẽ không thể thực hiện tất cả những điều này bằng cách kế thừa từ Danh sách <T>, vì C # "chỉ" hỗ trợ kế thừa duy nhất. (Nếu bạn đã thử tính xấu này trong C ++, bạn có thể coi đây là một điều tốt.) Thực hiện một bộ sưu tập thông qua thừa kế và một bộ sưu tập thông qua thành phần có thể cảm thấy bẩn. Và các thuộc tính như Count trở nên khó hiểu với người dùng trừ khi bạn triển khai ILIst <Player> .Count và IList <StaffMember> .Count, v.v., và sau đó họ chỉ đau đớn thay vì khó hiểu. Bạn có thể thấy nơi này sẽ đi; ruột cảm thấy trong khi suy nghĩ về con đường này cũng có thể cho bạn biết cảm giác sai lầm khi đi theo hướng này (và đúng hay sai, đồng nghiệp của bạn cũng có thể nếu bạn thực hiện theo cách này!)

Câu trả lời ngắn (quá muộn)

Hướng dẫn về việc không kế thừa từ các lớp bộ sưu tập không phải là C # cụ thể, bạn sẽ tìm thấy nó trong nhiều ngôn ngữ lập trình. Đó là nhận được sự khôn ngoan không phải là một luật. Một lý do là trong thực tế thành phần được coi là thường giành chiến thắng về sự kế thừa về tính dễ hiểu, khả năng thực hiện và khả năng duy trì. Nó phổ biến hơn với các đối tượng trong thế giới / miền thực để tìm các mối quan hệ "hasa" hữu ích và nhất quán hơn các mối quan hệ "isa" hữu ích và nhất quán trừ khi bạn đi sâu vào trừu tượng, đặc biệt là khi thời gian trôi qua và dữ liệu và hành vi chính xác của các đối tượng trong mã thay đổi. Điều này không nên khiến bạn luôn loại trừ việc kế thừa từ các lớp sưu tập; nhưng nó có thể gợi ý


34

Trước hết, nó phải làm với khả năng sử dụng. Nếu bạn sử dụng tính kế thừa, Teamlớp sẽ hiển thị hành vi (phương thức) được thiết kế hoàn toàn cho thao tác đối tượng. Ví dụ, AsReadOnly()hoặc CopyTo(obj)các phương thức không có ý nghĩa đối với nhóm đối tượng. Thay vì AddRange(items)phương pháp có lẽ bạn sẽ muốn một AddPlayers(players)phương pháp mô tả nhiều hơn .

Nếu bạn muốn sử dụng LINQ, thực hiện một giao diện chung như ICollection<T>hoặc IEnumerable<T>sẽ có ý nghĩa hơn.

Như đã đề cập, thành phần là cách đúng đắn để đi về nó. Chỉ cần thực hiện một danh sách các cầu thủ như một biến riêng tư.


30

Một đội bóng không phải là một danh sách các cầu thủ bóng đá. Một đội bóng bao gồm một danh sách các cầu thủ bóng đá!

Điều này là sai về mặt logic:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

và điều này là chính xác:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}

19
Điều này không giải thích tại sao . OP biết rằng hầu hết các lập trình viên khác đều cảm thấy như vậy, anh ta không hiểu tại sao nó quan trọng. Điều này chỉ thực sự cung cấp thông tin đã có trong câu hỏi.
Phục vụ

2
Đây là điều đầu tiên tôi nghĩ đến, làm thế nào dữ liệu của anh ta được mô hình hóa không chính xác. Vì vậy, lý do tại sao, mô hình dữ liệu của bạn là không chính xác.
rball

3
Tại sao không kế thừa từ danh sách? Bởi vì anh ta không muốn một loại danh sách cụ thể hơn.
Mauro Sampietro

2
Tôi không nghĩ ví dụ thứ hai là đúng. Bạn đang phơi bày trạng thái và do đó phá vỡ đóng gói. Trạng thái nên được ẩn / nội bộ đối tượng và cách để biến đổi trạng thái này phải thông qua các phương thức công khai. Chính xác phương pháp nào là một cái gì đó được xác định từ các yêu cầu kinh doanh, nhưng nó nên ở trạng thái "cần điều này ngay bây giờ", chứ không phải "có thể hữu ích vào lúc nào đó tôi không biết". Giữ chặt api của bạn và bạn sẽ tiết kiệm thời gian và công sức cho mình.
sara

1
Đóng gói không phải là điểm của câu hỏi. Tôi tập trung vào việc không mở rộng một loại nếu bạn không muốn loại đó hành xử theo cách cụ thể hơn. Các trường đó phải là tự động hóa trong các phương thức c # và get / set trong các ngôn ngữ khác nhưng ở đây trong bối cảnh này là hoàn toàn thừa
Mauro Sampietro

28

Nó phụ thuộc vào ngữ cảnh

Khi bạn coi đội của mình là một danh sách các cầu thủ, bạn đang chiếu "ý tưởng" của một đội bóng chân xuống một khía cạnh: Bạn giảm "đội" cho những người bạn nhìn thấy trên sân. Dự đoán này chỉ đúng trong một bối cảnh nhất định. Trong một bối cảnh khác, điều này có thể hoàn toàn sai. Hãy tưởng tượng bạn muốn trở thành một nhà tài trợ của đội. Vì vậy, bạn phải nói chuyện với các nhà quản lý của đội. Trong bối cảnh này, nhóm được dự kiến ​​vào danh sách các nhà quản lý của nó. Và hai danh sách này thường không trùng lặp rất nhiều. Các bối cảnh khác là hiện tại so với các cầu thủ cũ, vv

Ngữ nghĩa không rõ ràng

Vì vậy, vấn đề với việc coi một đội là một danh sách các cầu thủ của mình là ngữ nghĩa của nó phụ thuộc vào bối cảnh và nó không thể được mở rộng khi bối cảnh thay đổi. Ngoài ra, thật khó để diễn đạt, bối cảnh bạn đang sử dụng.

Các lớp học có thể mở rộng

Khi bạn sử dụng một lớp chỉ có một thành viên (ví dụ: IList activePlayers ), bạn có thể sử dụng tên của thành viên (và thêm vào đó là nhận xét của nó) để làm cho bối cảnh rõ ràng. Khi có bối cảnh bổ sung, bạn chỉ cần thêm một thành viên.

Các lớp học phức tạp hơn

Trong một số trường hợp, nó có thể là quá mức cần thiết để tạo thêm một lớp. Mỗi định nghĩa lớp phải được tải thông qua trình nạp lớp và sẽ được lưu trữ bởi máy ảo. Điều này chi phí bạn hiệu năng thời gian chạy và bộ nhớ. Khi bạn có một bối cảnh rất cụ thể, có thể coi một đội bóng là một danh sách các cầu thủ. Nhưng trong trường hợp này, bạn thực sự chỉ nên sử dụng một IList chứ không phải một lớp xuất phát từ nó.

Kết luận / cân nhắc

Khi bạn có một bối cảnh rất cụ thể, bạn có thể coi một đội là một danh sách các cầu thủ. Ví dụ bên trong một phương thức, nó hoàn toàn OK để viết:

IList<Player> footballTeam = ...

Khi sử dụng F #, thậm chí có thể ổn khi tạo một từ viết tắt:

type FootballTeam = IList<Player>

Nhưng khi bối cảnh rộng hơn hoặc thậm chí không rõ ràng, bạn không nên làm điều này. Điều này đặc biệt là khi bạn tạo một lớp mới mà bối cảnh của nó có thể được sử dụng trong tương lai không rõ ràng. Dấu hiệu cảnh báo là khi bạn bắt đầu thêm các thuộc tính bổ sung vào lớp của mình (tên của đội, huấn luyện viên, v.v.). Đây là một dấu hiệu rõ ràng rằng bối cảnh mà lớp sẽ được sử dụng không cố định và sẽ thay đổi trong tương lai. Trong trường hợp này, bạn không thể coi đội là một danh sách các cầu thủ, nhưng bạn nên lập mô hình danh sách các cầu thủ (hiện đang hoạt động, không bị thương, v.v.) như một thuộc tính của đội.


27

Hãy để tôi viết lại câu hỏi của bạn. vì vậy bạn có thể nhìn chủ đề từ một góc nhìn khác.

Khi tôi cần đại diện cho một đội bóng đá, tôi hiểu rằng về cơ bản đó là một cái tên. Giống như: "Đại bàng"

string team = new string();

Sau đó tôi nhận ra các đội cũng có người chơi.

Tại sao tôi không thể mở rộng loại chuỗi để nó cũng chứa danh sách người chơi?

Điểm của bạn vào vấn đề là tùy ý. Cố gắng nghĩ những gì một nhóm (thuộc tính), không phải những gì nó .

Sau khi bạn làm điều đó, bạn có thể xem nếu nó chia sẻ các thuộc tính với các lớp khác. Và nghĩ về thừa kế.


1
Đó là một điểm tốt - người ta có thể nghĩ về đội chỉ là một cái tên. Tuy nhiên, nếu ứng dụng của tôi nhằm mục đích làm việc với hành động của người chơi, thì suy nghĩ đó là một chút ít rõ ràng. Dù sao, có vẻ như vấn đề đi xuống thành phần so với thừa kế cuối cùng.
Tuyệt vời nhất

6
Đó là một cách đáng khen để xem xét nó. Như một lưu ý phụ, hãy xem xét điều này: Một người đàn ông giàu có sở hữu một số đội bóng đá, anh ta tặng một người bạn như một món quà, người bạn thay đổi tên của đội, sa thải huấn luyện viên, và thay thế tất cả các cầu thủ. Đội bạn bè gặp đội nam trên green và khi đội nam thua, anh ta nói "Tôi không thể tin rằng bạn đang đánh bại tôi với đội tôi đã cho bạn!" Người đàn ông có đúng không? Làm thế nào bạn sẽ kiểm tra điều này?
dùng1852503

20

Chỉ vì tôi nghĩ rằng các câu trả lời khác đi khá nhiều về việc một đội bóng đá "là" List<FootballPlayer>hay "có" List<FootballPlayer>, mà thực sự không trả lời câu hỏi này như đã viết.

OP chủ yếu yêu cầu làm rõ về các hướng dẫn để kế thừa từ List<T>:

Một hướng dẫn nói rằng bạn không nên thừa kế từ List<T>. Tại sao không?

List<T>không có phương thức ảo. Đây không phải là một vấn đề trong mã của riêng bạn, vì bạn thường có thể chuyển đổi việc thực hiện với nỗi đau tương đối nhỏ - nhưng có thể là một vấn đề lớn hơn nhiều trong một API công khai.

API công khai là gì và tại sao tôi nên quan tâm?

API công khai là giao diện bạn hiển thị cho các lập trình viên bên thứ 3. Hãy suy nghĩ mã khung. Và nhớ lại rằng các hướng dẫn đang được tham chiếu là " Nguyên tắc thiết kế .NET Framework " chứ không phải " Nguyên tắc thiết kế ứng dụng .NET ". Có một sự khác biệt và - nói chung - thiết kế API công khai nghiêm ngặt hơn rất nhiều.

Nếu dự án hiện tại của tôi không và không có khả năng có API công khai này, tôi có thể bỏ qua hướng dẫn này một cách an toàn không? Nếu tôi thừa kế từ Danh sách và hóa ra tôi cần API công khai, tôi sẽ gặp khó khăn gì?

Khá nhiều, vâng. Bạn có thể muốn xem xét lý do đằng sau nó để xem liệu nó có áp dụng cho tình huống của bạn không, nhưng nếu bạn không xây dựng API công khai thì bạn không cần phải lo lắng về các mối lo ngại về API như phiên bản (trong đó, đây là một tập hợp con).

Nếu bạn thêm API công khai trong tương lai, bạn sẽ cần phải trừu tượng hóa API của mình khỏi việc triển khai (bằng cách không List<T>trực tiếp tiết lộ ) hoặc vi phạm các nguyên tắc với nỗi đau có thể xảy ra trong tương lai.

Tại sao nó thậm chí còn quan trọng? Một danh sách là một danh sách. Điều gì có thể thay đổi? Tôi có thể muốn thay đổi điều gì?

Phụ thuộc vào bối cảnh, nhưng vì chúng tôi đang sử dụng FootballTeamlàm ví dụ - hãy tưởng tượng rằng bạn không thể thêm vào FootballPlayernếu điều đó sẽ khiến nhóm vượt qua giới hạn lương. Một cách có thể để thêm đó sẽ là một cái gì đó như:

 class FootballTeam : List<FootballPlayer> {
     override void Add(FootballPlayer player) {
        if (this.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
 }

À ... nhưng bạn không thể override Addvì nó không virtual(vì lý do hiệu suất).

Nếu bạn đang ở trong một ứng dụng (về cơ bản, điều đó có nghĩa là bạn và tất cả những người gọi của bạn được biên dịch cùng nhau) thì bây giờ bạn có thể thay đổi thành sử dụng IList<T>và sửa chữa mọi lỗi biên dịch:

 class FootballTeam : IList<FootballPlayer> {
     private List<FootballPlayer> Players { get; set; }

     override void Add(FootballPlayer player) {
        if (this.Players.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
     /* boiler plate for rest of IList */
 }

nhưng, nếu bạn đã tiếp xúc công khai với bên thứ 3, bạn vừa thực hiện một thay đổi vi phạm sẽ gây ra lỗi biên dịch và / hoặc thời gian chạy.

TL; DR - hướng dẫn dành cho API công khai. Đối với API riêng, hãy làm những gì bạn muốn.


18

Có cho phép mọi người nói

myTeam.subList(3, 5);

có ý nghĩa gì không? Nếu không thì nó không nên là một Danh sách.


3
Nó có thể nếu bạn gọi nómyTeam.subTeam(3, 5);
Sam Leach

3
@SamLeach Giả sử điều đó là đúng, thì bạn vẫn sẽ cần thành phần chứ không phải kế thừa. Như subListsẽ không trở lại Teamnữa.
Cruncher

1
Điều này đã thuyết phục tôi hơn bất kỳ câu trả lời nào khác. +1
FireCubez

16

Nó phụ thuộc vào hành vi của đối tượng "nhóm" của bạn. Nếu nó hoạt động giống như một bộ sưu tập, có thể đồng ý đại diện cho nó trước tiên với Danh sách đơn giản. Sau đó, bạn có thể bắt đầu nhận thấy rằng bạn tiếp tục sao chép mã lặp lại trong danh sách; tại thời điểm này, bạn có tùy chọn tạo đối tượng FootballTeam bao bọc danh sách người chơi. Lớp FootballTeam trở thành ngôi nhà cho tất cả các mã lặp trong danh sách người chơi.

Nó làm cho mã của tôi dài dòng không cần thiết. Bây giờ tôi phải gọi my_team.Players.Count thay vì chỉ my_team.Count. Rất may, với C # tôi có thể định nghĩa các bộ chỉ mục để làm cho việc lập chỉ mục trở nên minh bạch và chuyển tiếp tất cả các phương thức của Danh sách nội bộ ... Nhưng đó là rất nhiều mã! Tôi nhận được gì cho tất cả công việc đó?

Đóng gói. Khách hàng của bạn không cần biết những gì diễn ra bên trong FootballTeam. Đối với tất cả khách hàng của bạn biết, nó có thể được thực hiện bằng cách tìm danh sách người chơi trong cơ sở dữ liệu. Họ không cần biết, và điều này cải thiện thiết kế của bạn.

Nó chỉ đơn giản là không có ý nghĩa gì. Một đội bóng đá không "có" một danh sách các cầu thủ. Đây là danh sách các cầu thủ. Bạn không nói "John McFootballer đã tham gia với người chơi của someTeam". Bạn nói "John đã tham gia vào một sốTeam". Bạn không thêm chữ cái vào "ký tự của chuỗi", bạn thêm chữ cái vào chuỗi. Bạn không thêm sách vào sách của thư viện, bạn thêm sách vào thư viện.

Chính xác :) bạn sẽ nói FootballTeam.Add (john), không phải FootballTeam.List.Add (john). Danh sách nội bộ sẽ không được nhìn thấy.


1
OP muốn hiểu làm thế nào để MÔ HÌNH THỰC SỰ nhưng sau đó định nghĩa một đội bóng đá là một danh sách các cầu thủ bóng đá (đó là sai về mặt khái niệm). Đây là vấn đề. Theo tôi, bất kỳ lý lẽ nào khác là đánh lừa anh ấy.
Mauro Sampietro

3
Tôi không đồng ý rằng danh sách các cầu thủ là sai về mặt khái niệm. Không cần thiết. Như một người khác đã viết trong trang này, "Tất cả các mô hình đều sai, nhưng một số mô hình là hữu ích"
xpmatteo 23/12/14

Đúng mô hình là khó để có được, một khi thực hiện chúng là những người hữu ích. Nếu một mô hình có thể bị lạm dụng nó là sai về mặt khái niệm. stackoverflow.com/a/21706411/711061
Mauro Sampietro 23/12/14

15

Có rất nhiều câu trả lời xuất sắc ở đây, nhưng tôi muốn chạm vào một điều mà tôi không thấy được đề cập: Thiết kế hướng đối tượng là trao quyền cho các đối tượng .

Bạn muốn gói gọn tất cả các quy tắc của mình, công việc bổ sung và các chi tiết nội bộ bên trong một đối tượng thích hợp. Theo cách này, các đối tượng khác tương tác với đối tượng này không phải lo lắng về tất cả. Trên thực tế, bạn muốn tiến thêm một bước và chủ động ngăn chặn các đối tượng khác bỏ qua các phần bên trong này.

Khi bạn thừa kế từ List, tất cả các đối tượng khác có thể xem bạn là Danh sách. Họ có quyền truy cập trực tiếp vào các phương thức để thêm và xóa người chơi. Và bạn sẽ mất kiểm soát; ví dụ:

Giả sử bạn muốn phân biệt khi người chơi rời đi bằng cách biết họ đã nghỉ hưu, từ chức hoặc bị sa thải. Bạn có thể thực hiện một RemovePlayerphương thức có một enum đầu vào thích hợp. Tuy nhiên, bằng cách kế thừa từ List, bạn sẽ không thể ngăn chặn truy cập trực tiếp đến Remove, RemoveAllvà thậm chí cả Clear. Kết quả là, bạn thực sự đã từ chốiFootballTeam lớp học của bạn .


Những suy nghĩ bổ sung về đóng gói ... Bạn nêu lên mối quan tâm sau:

Nó làm cho mã của tôi dài dòng không cần thiết. Bây giờ tôi phải gọi my_team.Players.Count thay vì chỉ my_team.Count.

Bạn đã đúng, điều đó sẽ không cần thiết cho tất cả các khách hàng sử dụng nhóm của bạn. Tuy nhiên, vấn đề đó rất nhỏ so với thực tế là bạn đã tiếp xúc List Playersvới tất cả và lặt vặt để họ có thể đùa giỡn với đội của bạn mà không cần sự đồng ý của bạn.

Bạn tiếp tục nói:

Nó chỉ đơn giản là không có ý nghĩa gì. Một đội bóng đá không "có" một danh sách các cầu thủ. Đây là danh sách các cầu thủ. Bạn không nói "John McFootballer đã tham gia với người chơi của someTeam". Bạn nói "John đã tham gia vào một sốTeam".

Bạn đã sai về bit đầu tiên: Bỏ từ 'danh sách', và thực sự rõ ràng là một đội có người chơi.
Tuy nhiên, bạn đánh cái đinh vào đầu bằng cái thứ hai. Bạn không muốn khách hàng gọi ateam.Players.Add(...). Bạn muốn họ gọi ateam.AddPlayer(...). Và việc triển khai của bạn sẽ (có thể trong số những thứ khác) gọi Players.Add(...)nội bộ.


Hy vọng bạn có thể thấy đóng gói quan trọng như thế nào đối với mục tiêu trao quyền cho các đối tượng của bạn. Bạn muốn cho phép mỗi lớp thực hiện tốt công việc của mình mà không sợ sự can thiệp từ các đối tượng khác.


12

Cách C # chính xác để biểu diễn cấu trúc dữ liệu là gì ...

Remeber, "Tất cả các mô hình đều sai, nhưng một số là hữu ích." - Hộp George EP

Không có "cách chính xác", chỉ có một cách hữu ích.

Chọn một cái hữu ích cho bạn và / người dùng của bạn. Đó là nó. Phát triển kinh tế, đừng quá kỹ sư. Bạn viết càng ít mã, bạn càng cần ít mã để gỡ lỗi. (đọc các phiên bản sau).

- Đã chỉnh sửa

Câu trả lời tốt nhất của tôi sẽ là ... nó phụ thuộc. Kế thừa từ Danh sách sẽ đưa các khách hàng của lớp này đến các phương thức có thể không được tiết lộ, chủ yếu vì FootballTeam trông giống như một thực thể kinh doanh.

- Phiên bản 2

Tôi thực sự không nhớ những gì tôi đã đề cập đến trên bình luận của kỹ sư không quá kỹ sư. Mặc dù tôi tin rằng tư duy KISS là một hướng dẫn tốt, tôi muốn nhấn mạnh rằng việc kế thừa một lớp doanh nghiệp từ Danh sách sẽ tạo ra nhiều vấn đề hơn nó giải quyết, do rò rỉ trừu tượng .

Mặt khác, tôi tin rằng có một số trường hợp hạn chế trong đó đơn giản là kế thừa từ Danh sách là hữu ích. Như tôi đã viết trong phiên bản trước, nó phụ thuộc. Câu trả lời cho từng trường hợp bị ảnh hưởng nặng nề bởi cả kiến ​​thức, kinh nghiệm và sở thích cá nhân.

Cảm ơn @kai đã giúp tôi suy nghĩ chính xác hơn về câu trả lời.


Câu trả lời tốt nhất của tôi sẽ là ... nó phụ thuộc. Kế thừa từ Danh sách sẽ đưa các khách hàng của lớp này đến các phương thức có thể không được tiết lộ, chủ yếu vì FootballTeam trông giống như một thực thể kinh doanh. Tôi không biết mình nên chỉnh sửa câu trả lời này hay đăng bài khác, có ý kiến ​​gì không?
ArturoTena

2
Một chút về " kế thừa từ một Listsẽ phơi bày các máy khách của lớp này thành các phương thức có thể không bị lộ " chính là điều làm cho việc kế thừa từ Listmột thứ không tuyệt đối. Sau khi tiếp xúc, họ dần bị lạm dụng và sử dụng sai theo thời gian. Tiết kiệm thời gian bây giờ bằng cách "phát triển kinh tế" có thể dễ dàng dẫn đến gấp mười lần số tiền tiết kiệm bị mất trong tương lai: gỡ lỗi các hành vi lạm dụng và cuối cùng là tái cấu trúc để sửa chữa di sản. Nguyên tắc YAGNI cũng có thể được coi là ý nghĩa: Bạn không cần tất cả các phương pháp Listđó, vì vậy đừng phơi bày chúng.
vỡ mộng

3
"đừng quá kỹ sư. Bạn viết càng ít mã, bạn càng cần ít mã để gỡ lỗi." <- Tôi nghĩ rằng điều này là sai lệch. Đóng gói và thành phần trên thừa kế KHÔNG phải là kỹ thuật quá mức và nó không gây ra nhiều nhu cầu gỡ lỗi. Bằng cách gói gọn, bạn đang GIỚI HẠN số cách khách hàng có thể sử dụng (và lạm dụng) lớp của mình và do đó bạn có ít điểm vào cần kiểm tra và xác thực đầu vào. Kế thừa từ Listvì nó nhanh chóng và dễ dàng và do đó sẽ dẫn đến ít lỗi hơn là sai, đó chỉ là thiết kế tồi và thiết kế xấu dẫn đến nhiều lỗi hơn so với "kỹ thuật".
sara

@kai Tôi đồng ý với bạn ở mọi điểm. Tôi thực sự không nhớ những gì tôi đã đề cập đến trên bình luận của kỹ sư không quá kỹ sư. OTOH, tôi tin rằng có một số trường hợp hạn chế trong đó đơn giản là kế thừa từ Danh sách là hữu ích. Như tôi đã viết trong phiên bản sau, nó phụ thuộc. Câu trả lời cho từng trường hợp bị ảnh hưởng nặng nề bởi cả kiến ​​thức, kinh nghiệm và sở thích cá nhân. Giống như mọi thứ trong cuộc sống. ;-)
ArturoTena

11

Điều này nhắc nhở tôi về "Là một" so với "có" sự đánh đổi. Đôi khi nó dễ dàng hơn và có ý nghĩa hơn để kế thừa trực tiếp từ một siêu hạng. Mặt khác, sẽ hợp lý hơn khi tạo một lớp độc lập và bao gồm lớp mà bạn sẽ được thừa hưởng từ một biến thành viên. Bạn vẫn có thể truy cập chức năng của lớp nhưng không bị ràng buộc với giao diện hoặc bất kỳ ràng buộc nào khác có thể đến từ việc kế thừa từ lớp.

Bạn làm nghề gì Như với rất nhiều thứ ... nó phụ thuộc vào bối cảnh. Hướng dẫn tôi sẽ sử dụng là để kế thừa từ một lớp khác thực sự cần có một mối quan hệ "là". Vì vậy, nếu bạn viết một lớp học gọi là BMW, nó có thể thừa hưởng từ Xe hơi vì BMW thực sự là một chiếc xe hơi. Một lớp Ngựa có thể thừa hưởng từ lớp Động vật có vú vì một con ngựa thực sự là động vật có vú trong đời thực và bất kỳ chức năng nào của Động vật có vú đều có liên quan đến Ngựa. Nhưng bạn có thể nói rằng một đội là một danh sách? Từ những gì tôi có thể nói, có vẻ như một Đội thực sự "là một" Danh sách. Vì vậy, trong trường hợp này, tôi sẽ có một Danh sách là một biến thành viên.


9

Bí mật bẩn thỉu của tôi: Tôi không quan tâm mọi người nói gì, và tôi làm điều đó. .NET Framework được lan truyền với "XxxxCollection" (UIEuityCollection cho ví dụ đầu của tôi).

Vì vậy, những gì ngăn tôi nói:

team.Players.ByName("Nicolas")

Khi tôi thấy nó tốt hơn

team.ByName("Nicolas")

Hơn nữa, PlayerCollection của tôi có thể được sử dụng bởi lớp khác, như "Câu lạc bộ" mà không có bất kỳ sự sao chép mã nào.

club.Players.ByName("Nicolas")

Thực hành tốt nhất của ngày hôm qua, có thể không phải là một trong ngày mai. Không có lý do đằng sau hầu hết các thực tiễn tốt nhất, hầu hết chỉ là thỏa thuận rộng rãi trong cộng đồng. Thay vì hỏi cộng đồng nếu nó sẽ đổ lỗi cho bạn khi bạn làm điều đó hãy tự hỏi mình, cái gì dễ đọc và dễ bảo trì hơn?

team.Players.ByName("Nicolas") 

hoặc là

team.ByName("Nicolas")

Có thật không. Bạn có nghi ngờ gì không? Bây giờ có lẽ bạn cần chơi với các ràng buộc kỹ thuật khác ngăn bạn sử dụng Danh sách <T> trong trường hợp sử dụng thực tế của bạn. Nhưng đừng thêm một ràng buộc không nên tồn tại. Nếu Microsoft không ghi lại lý do tại sao, thì đó chắc chắn là một "thực tiễn tốt nhất" đến từ hư không.


9
Mặc dù điều quan trọng là phải có can đảm và chủ động để thách thức sự khôn ngoan được chấp nhận khi thích hợp, tôi nghĩ rằng điều khôn ngoan trước tiên là phải hiểu tại sao sự khôn ngoan được chấp nhận lại được chấp nhận ngay từ đầu, trước khi bắt tay vào thử thách nó. -1 vì "Bỏ qua hướng dẫn đó!" không phải là một câu trả lời tốt cho "Tại sao hướng dẫn này tồn tại?" Ngẫu nhiên, cộng đồng không chỉ "đổ lỗi cho tôi" trong trường hợp này, mà còn đưa ra một lời giải thích thỏa đáng đã thành công trong việc thuyết phục tôi.
Tuyệt vời nhất

3
Thực tế, khi không có lời giải thích nào được đưa ra, cách duy nhất của bạn là vượt qua ranh giới và thử nghiệm không sợ vi phạm. Hiện nay. Hãy tự hỏi mình, ngay cả khi Danh sách <T> không phải là một ý tưởng tốt. Mất bao nhiêu đau đớn để thay đổi quyền thừa kế từ Danh sách <T> sang Bộ sưu tập <T>? Một dự đoán: Ít thời gian hơn so với việc đăng bài trên diễn đàn, bất kể độ dài mã của bạn với các công cụ tái cấu trúc. Bây giờ nó là một câu hỏi hay, nhưng không phải là một câu hỏi thực tế.
Nicolas Dorier

Để làm rõ: Tôi chắc chắn không chờ đợi phước lành của SO được thừa hưởng từ bất cứ điều gì tôi muốn tuy nhiên tôi muốn, nhưng quan điểm của câu hỏi của tôi là hiểu những cân nhắc nên đi vào quyết định này, và tại sao rất nhiều lập trình viên có kinh nghiệm dường như có quyết định ngược lại với những gì tôi đã làm Collection<T>không giống như List<T>vậy nên có thể cần khá nhiều công việc để xác minh trong một dự án lớn.
Tuyệt vời nhất

2
team.ByName("Nicolas")có nghĩa "Nicolas"là tên của đội.
Sam Leach

1
"không thêm một ràng buộc không nên tồn tại" Au contraire, đừng phơi bày bất cứ điều gì bạn không có lý do chính đáng để phơi bày. Đây không phải là chủ nghĩa thuần túy hay nhiệt tâm mù quáng, cũng không phải là xu hướng thiết kế thời trang mới nhất. Đó là thiết kế hướng đối tượng cơ bản 101, cấp độ mới bắt đầu. Đó không phải là một bí mật tại sao "quy tắc" này tồn tại, nó là kết quả của nhiều thập kỷ kinh nghiệm.
sara

8

Những gì hướng dẫn nói là API công khai không được tiết lộ quyết định thiết kế nội bộ cho dù bạn đang sử dụng danh sách, bộ, từ điển, cây hay bất cứ thứ gì. Một "đội" không nhất thiết là một danh sách. Bạn có thể triển khai nó dưới dạng danh sách nhưng người dùng API công khai của bạn sẽ sử dụng lớp bạn trên cơ sở cần biết. Điều này cho phép bạn thay đổi quyết định của mình và sử dụng cấu trúc dữ liệu khác mà không ảnh hưởng đến giao diện chung.


Nhìn lại, sau lời giải thích của @EricLippert và những người khác, bạn thực sự đã đưa ra một câu trả lời tuyệt vời cho phần API của câu hỏi của tôi - ngoài những gì bạn nói, nếu tôi làm class FootballTeam : List<FootballPlayers>, người dùng của lớp tôi sẽ có thể nói với tôi ' đã thừa hưởng từ List<T>bằng cách sử dụng phản chiếu, nhìn thấy những List<T>phương pháp mà không có ý nghĩa cho một đội bóng, và có thể sử dụng FootballTeamvào List<T>, vì vậy tôi sẽ được chi tiết thực hiện tiết lộ cho khách hàng (không cần thiết).
Tuyệt vời nhất vào

7

Khi họ nói List<T>là "tối ưu hóa", tôi nghĩ rằng họ muốn có nghĩa là nó không có các tính năng như các phương thức ảo đắt hơn một chút. Vì vậy, vấn đề là một khi bạn phơi bày List<T>trong API công khai của mình , bạn sẽ mất khả năng thực thi các quy tắc kinh doanh hoặc tùy chỉnh chức năng của nó sau này. Nhưng nếu bạn đang sử dụng lớp kế thừa này làm nội bộ trong dự án của mình (trái ngược với khả năng tiếp xúc với hàng ngàn khách hàng / đối tác / nhóm khác của bạn dưới dạng API) thì có thể ổn nếu tiết kiệm thời gian của bạn và đó là chức năng bạn muốn bản sao. Lợi thế của việc kế thừa từList<T> đó là bạn loại bỏ rất nhiều mã trình bao bọc ngu ngốc mà sẽ không bao giờ được tùy chỉnh trong tương lai gần. Ngoài ra nếu bạn muốn lớp của bạn rõ ràng có ngữ nghĩa chính xác nhưList<T> đối với tuổi thọ của API thì nó cũng có thể ổn.

Tôi thường thấy nhiều người làm hàng tấn việc làm thêm chỉ vì quy tắc FxCop nói như vậy hoặc blog của ai đó nói rằng đó là một thực tế "xấu". Nhiều lần, điều này biến mã thành thiết kế kỳ lạ của palooza. Cũng như nhiều hướng dẫn, coi nó là hướng dẫn có thể có ngoại lệ.


4

Mặc dù tôi không có một so sánh phức tạp như hầu hết các câu trả lời này, tôi muốn chia sẻ phương pháp của mình để xử lý tình huống này. Bằng cách mở rộng IEnumerable<T>, bạn có thể cho phép Teamlớp của mình hỗ trợ các tiện ích mở rộng truy vấn Linq mà không cần hiển thị công khai tất cả các phương thức và thuộc tính của List<T>.

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}

3

Nếu người dùng lớp của bạn cần tất cả các phương thức và thuộc tính ** Danh sách có, bạn nên lấy lớp của mình từ đó. Nếu họ không cần chúng, hãy đóng Danh sách và tạo các hàm bao cho các phương thức mà người dùng lớp của bạn thực sự cần.

Đây là một quy tắc nghiêm ngặt, nếu bạn viết API công khai hoặc bất kỳ mã nào khác sẽ được nhiều người sử dụng. Bạn có thể bỏ qua quy tắc này nếu bạn có một ứng dụng nhỏ và không quá 2 nhà phát triển. Điều này sẽ giúp bạn tiết kiệm thời gian.

Đối với các ứng dụng nhỏ, bạn cũng có thể cân nhắc chọn ngôn ngữ khác, ít nghiêm ngặt hơn. Ruby, JavaScript - bất cứ điều gì cho phép bạn viết ít mã hơn.


3

Tôi chỉ muốn nói thêm rằng Bertrand Meyer, người phát minh ra Eiffel và thiết kế theo hợp đồng, sẽ Teamđược thừa hưởng List<Player>mà không cần nhiều đến việc đánh một mí.

Trong cuốn sách của mình, Xây dựng phần mềm hướng đối tượng , ông thảo luận về việc triển khai hệ thống GUI trong đó các cửa sổ hình chữ nhật có thể có các cửa sổ con. Ông chỉ đơn giản là có sự Windowkế thừa từ cả hai RectangleTree<Window>để sử dụng lại việc thực hiện.

Tuy nhiên, C # không phải là Eiffel. Cái sau hỗ trợ nhiều kế thừa và đổi tên các tính năng . Trong C #, khi bạn phân lớp, bạn kế thừa cả giao diện và cài đặt. Bạn có thể ghi đè lên việc thực hiện, nhưng các quy ước gọi được sao chép trực tiếp từ siêu lớp. Tuy nhiên, trong Eiffel, bạn có thể sửa đổi tên của các phương thức công khai, do đó bạn có thể đổi tên AddRemovethành HireFiretrong Team. Nếu một phiên bản của Teamupcast trở lại List<Player>, người gọi sẽ sử dụng AddRemovesửa đổi nó, nhưng các phương thức ảo của bạn HireFiresẽ được gọi.


1
Ở đây, vấn đề không phải là nhiều kế thừa, mixins (vv ..) hoặc làm thế nào để đối phó với sự vắng mặt của chúng. Trong bất kỳ ngôn ngữ nào, ngay cả trong ngôn ngữ của con người, một nhóm không phải là một danh sách những người mà bao gồm một danh sách những người. Đây là một khái niệm trừu tượng. Nếu Bertrand Meyer hoặc bất cứ ai quản lý một nhóm bằng cách phân loại Danh sách đang làm sai. Bạn nên phân lớp Danh sách nếu bạn muốn một loại danh sách cụ thể hơn. Mong bạn đồng ý.
Mauro Sampietro 7/07/2015

1
Điều đó phụ thuộc vào sự kế thừa đối với bạn. Chính nó, nó là một hoạt động trừu tượng, nó không có nghĩa là hai lớp trong một mối quan hệ "là một loại đặc biệt", mặc dù đó là cách hiểu chính thống. Tuy nhiên, sự kế thừa có thể được coi là một cơ chế để tái sử dụng nếu thiết kế ngôn ngữ hỗ trợ nó, mặc dù thành phần là sự thay thế được xem xét hiện nay.
Alexey

2

Tôi nghĩ rằng tôi không đồng ý với khái quát của bạn. Một đội không chỉ là một tập hợp các cầu thủ. Một đội có rất nhiều thông tin về nó - tên, biểu tượng, bộ sưu tập nhân viên quản lý / quản trị viên, bộ sưu tập đội ngũ huấn luyện, sau đó là bộ sưu tập cầu thủ. Vì vậy, đúng, lớp FootballTeam của bạn nên có 3 bộ sưu tập và bản thân nó không phải là một bộ sưu tập; nếu nó là để mô hình đúng thế giới thực.

Bạn có thể xem xét một lớp PlayerCollection, giống như StringCollection chuyên biệt cung cấp một số phương tiện khác - như xác thực và kiểm tra trước khi các đối tượng được thêm vào hoặc xóa khỏi cửa hàng nội bộ.

Có lẽ, khái niệm về một Betters PlayerCollection phù hợp với phương pháp ưa thích của bạn?

public class PlayerCollection : Collection<Player> 
{ 
}

Và sau đó FootballTeam có thể trông như thế này:

public class FootballTeam 
{ 
    public string Name { get; set; }
    public string Location { get; set; }

    public ManagementCollection Management { get; protected set; } = new ManagementCollection();

    public CoachingCollection CoachingCrew { get; protected set; } = new CoachingCollection();

    public PlayerCollection Players { get; protected set; } = new PlayerCollection();
}

2

Thích giao diện hơn các lớp học

Các lớp nên tránh xuất phát từ các lớp và thay vào đó thực hiện các giao diện tối thiểu cần thiết.

Kế thừa phá vỡ Đóng gói

Xuất phát từ các lớp phá vỡ đóng gói :

  • tiết lộ chi tiết nội bộ về cách bộ sưu tập của bạn được thực hiện
  • khai báo một giao diện (tập hợp các chức năng và thuộc tính công cộng) có thể không phù hợp

Trong số những thứ khác, điều này làm cho việc tái cấu trúc mã của bạn trở nên khó khăn hơn.

Các lớp học là một chi tiết thực hiện

Các lớp là một chi tiết triển khai nên được ẩn khỏi các phần khác của mã của bạn. Tóm lạiSystem.List việc triển khai cụ thể một kiểu dữ liệu trừu tượng, có thể có hoặc không phù hợp trong hiện tại và trong tương lai.

Về mặt khái niệm, thực tế là System.Listkiểu dữ liệu được gọi là "danh sách" là một chút cá trích đỏ. MộtSystem.List<T> là một bộ sưu tập có thứ tự có thể thay đổi, hỗ trợ các hoạt động O (1) được khấu hao để thêm, chèn và xóa các phần tử và các hoạt động O (1) để lấy số lượng phần tử hoặc nhận và thiết lập phần tử theo chỉ mục.

Giao diện càng nhỏ Mã càng linh hoạt

Khi thiết kế cấu trúc dữ liệu, giao diện càng đơn giản thì mã càng linh hoạt. Chỉ cần nhìn vào LINQ mạnh mẽ như thế nào cho một minh chứng về điều này.

Cách chọn giao diện

Khi bạn nghĩ "danh sách", bạn nên bắt đầu bằng cách tự nói với mình, "Tôi cần đại diện cho một bộ sưu tập các cầu thủ bóng chày". Vì vậy, hãy nói rằng bạn quyết định mô hình hóa điều này với một lớp. Điều bạn nên làm trước tiên là quyết định số lượng giao diện tối thiểu mà lớp này sẽ cần phơi bày.

Một số câu hỏi có thể giúp hướng dẫn quy trình này:

  • Tôi có cần phải có số lượng? Nếu không xem xét thực hiệnIEnumerable<T>
  • Bộ sưu tập này sẽ thay đổi sau khi nó được khởi tạo? Nếu không cân nhắc IReadonlyList<T>.
  • Điều quan trọng là tôi có thể truy cập các mục theo chỉ mục? Xem xétICollection<T>
  • Là thứ tự mà tôi thêm các mục vào bộ sưu tập quan trọng? Có lẽ nó là một ISet<T>?
  • Nếu bạn thực sự muốn những điều này thì hãy tiếp tục và thực hiện IList<T>.

Bằng cách này, bạn sẽ không ghép các phần khác của mã với các chi tiết triển khai của bộ sưu tập cầu thủ bóng chày của bạn và sẽ được tự do thay đổi cách triển khai miễn là bạn tôn trọng giao diện.

Bằng cách thực hiện phương pháp này, bạn sẽ thấy mã trở nên dễ đọc, tái cấu trúc và tái sử dụng hơn.

Lưu ý về việc tránh nồi hơi

Việc thực hiện các giao diện trong một IDE hiện đại nên dễ dàng. Nhấp chuột phải và chọn "Thực hiện giao diện". Sau đó chuyển tiếp tất cả các triển khai cho một lớp thành viên nếu bạn cần.

Điều đó nói rằng, nếu bạn thấy bạn đang viết nhiều bản tóm tắt, điều đó có khả năng là do bạn đang phơi bày nhiều chức năng hơn mức bạn nên làm. Đó là cùng một lý do bạn không nên kế thừa từ một lớp học.

Bạn cũng có thể thiết kế các giao diện nhỏ hơn có ý nghĩa cho ứng dụng của mình và có thể chỉ là một vài chức năng mở rộng của trình trợ giúp để ánh xạ các giao diện đó tới bất kỳ giao diện nào khác mà bạn cần. Đây là cách tiếp cận tôi đã thực hiện trong IArraygiao diện của riêng mình cho thư viện LinqArray .


2

Các vấn đề với tuần tự hóa

Một khía cạnh bị thiếu. Các lớp kế thừa từ Danh sách <> không thể được tuần tự hóa chính xác bằng XmlSerializer . Trong trường hợp đó, DataContractSerializer phải được sử dụng thay thế hoặc cần phải thực hiện tuần tự hóa.

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

Đọc thêm: Khi một lớp được kế thừa từ Danh sách <>, XmlSerializer không tuần tự hóa các thuộc tính khác


0

Khi nào nó được chấp nhận?

Để trích dẫn Eric Lippert:

Khi bạn đang xây dựng một cơ chế mở rộng List<T>cơ chế.

Ví dụ, bạn mệt mỏi vì không có AddRangephương thức trong IList<T>:

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
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.