Чому б не успадкувати зі списку <T>?


1398

Плануючи свої програми, я часто починаю такий ланцюжок думок:

Футбольна команда - це лише список футболістів. Тому я мушу представляти це:

var football_team = new List<FootballPlayer>();

Упорядкованість цього списку представляє порядок, у якому гравці занесені до списку.

Але згодом я усвідомлюю, що команди мають інші властивості, крім простого списку гравців, які потрібно записати. Наприклад, загальна кількість балів у цьому сезоні, поточний бюджет, рівномірні кольори, stringвідображення назви команди та ін.

Тож я думаю:

Гаразд, футбольна команда подібно до списку гравців, але крім того, вона має назву (а string) та загальну кількість балів (an int). .NET не пропонує класу для зберігання футбольних команд, тому я зроблю свій клас. Найбільш схожа і відповідна існуюча структура List<FootballPlayer>, тому я успадкую її:

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

Але виявляється, що керівництво говорить, що не слід успадковуватиList<T> . Мене цією настановою я глибоко плутаю в двох аспектах.

Чому ні?

Мабуть, Listце якось оптимізовано для продуктивності . Як так? Які проблеми з працездатністю я викличу, якщо продовжу List? Що саме зламається?

Ще одна причина, яку я бачив, - це те, що Listнадається корпорацією Майкрософт, і я не маю контролю над цим, тому я не можу її змінити пізніше, після викриття "публічного API" . Але я намагаюся це зрозуміти. Що таке загальнодоступний API і чому я повинен турбуватися? Якщо мій поточний проект не має і, ймовірно, ніколи не матиме цей публічний API, чи можу я сміливо ігнорувати цю інструкцію? Якщо я успадковую List і виявляється, що мені потрібен публічний API, які труднощі виникнуть у мене?

Чому це навіть важливо? Список - це список. Що може змінитися? Що я міг би хотіти змінити?

І нарешті, якщо Microsoft не хотіла, щоб я успадкував List, чому вони не склали клас sealed?

Що ще я повинен використовувати?

Мабуть, для користувацьких колекцій Microsoft запропонувала Collectionклас, який слід розширити замість List. Але цей клас дуже голий і не має багато корисних речей, наприкладAddRange , наприклад. Відповідь jvitor83 забезпечує обґрунтування продуктивності саме для цього методу, але як повільний AddRangeне кращий, ніж ні AddRange?

Наслідування з боку Collection- це набагато більша робота, ніж спадкування List, і я не бачу користі. Напевно, Microsoft не сказав би мені робити зайву роботу без будь-якої причини, тому я не можу не відчути, ніби я якось щось не розумію, і успадкування Collectionнасправді не є правильним рішенням моєї проблеми.

Я бачив такі пропозиції, як реалізація IList. Просто ні. Це десятки рядків кодового коду, які мені нічого не приносять.

Нарешті, деякі пропонують обговорити Listщось:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

З цим є дві проблеми:

  1. Це робить мій код непотрібним багатослівним. Я повинен зараз зателефонувати, my_team.Players.Countа не просто my_team.Count. На щастя, за допомогою C # я можу визначити індексатори, щоб зробити індексацію прозорою та передати всі внутрішні методи List... Але це дуже багато коду! Що я отримую за всю цю роботу?

  2. Це просто не має сенсу. Футбольна команда не має "списку гравців". Це є список гравців. Ви не кажете, що "Джон МакФоотбаллер приєднався до гравців SomeTeam". Ви кажете "Джон приєднався до SomeTeam". Ви не додаєте листа до "символів рядка", ви додаєте лист до рядка. Ви не додаєте книгу до книг бібліотеки, ви додаєте книгу до бібліотеки.

Я усвідомлюю, що те, що відбувається "під кришкою", можна сказати як "додавання X до внутрішнього списку Y", але це здається дуже протиінтуїтивним способом мислення про світ.

Моє запитання (узагальнене)

Що таке правильний C # спосіб представлення структури даних, які, «логічно» (тобто, «для людського розуму») є просто listз thingsз декількома прибамбасами?

Чи успадковування List<T>завжди від неприйнятного? Коли це прийнятно? Чому / чому ні? Що повинен врахувати програміст, приймаючи рішення про спадщину List<T>чи ні?


90
Отже, чи є ваше питання справді, коли використовувати спадщину (квадрат є прямокутником, тому що завжди розумно надати квадрат, коли загальний запит прямокутника) та коли використовувати склад (на додаток у FootballTeam є список FootballPlayers, крім того до інших властивостей, які так само "фундаментальні" для нього, як і ім'я)? Чи не розгубились би інші програмісти, якби в іншому коді ви передали їм FootballTeam, коли вони очікували простого списку?
Політ Одіссеї

77
Що робити, якщо я скажу вам, що футбольна команда - це бізнес-компанія, яка ВІДПОВІДАє список гравців, а не голий Список гравців, які мають додаткові властивості?
STT LCU

75
Це дійсно досить просто. ЧИ футбольна команда - це список гравців? Очевидно, що ні, тому що, як ви кажете, є й інші відповідні властивості. Чи має футбольна команда список гравців? Так, тому він повинен містити список. Окрім цього, композиція просто краща перед успадкуванням взагалі, тому що її простіше змінити пізніше. Якщо ви вважаєте спадщину та склад одночасно логічними, переходьте до складу.
Тобберот

45
@FlightOdyssey: Припустимо, у вас є метод SquashRectangle, який бере прямокутник, вдвічі перевищує його висоту і подвоює його ширину. Тепер перейдіть у прямокутник, який буває 4x4, але має тип прямокутника, і квадрат, що є 4x4. Які розміри об’єкта після виклику SquashRectangle? Для прямокутника явно це 2х8. Якщо квадрат 2х8, то це вже не квадрат; якщо це не 2х8, то одна і та ж операція на одній формі дає різний результат. Висновок полягає в тому, що змінний квадрат не є різновидом прямокутника.
Ерік Ліпперт

29
@Gangnus: Ваше твердження просто помилкове; справжній підклас необхідний для того, щоб він функціонував, що є набором суперкласу. A stringпотрібно зробити все, що objectможна зробити, і більше .
Ерік Ліпперт

Відповіді:


1564

Тут є кілька хороших відповідей. Я б до них додав наступні моменти.

Який правильний спосіб C # представляє структуру даних, яка "логічно" (тобто "людському розуму") - лише список речей з кількома дзвіночками?

Попросіть будь-яких десяти людей, які не є комп'ютерними програмістами, які знайомі з футболом, заповнити бланк:

Футбольна команда є особливим видом _____

Чи хто - небудь , скажімо «список футболістів з декількома прибамбасами», або ж вони всі говорять «спортивна команда» або «клуб» або «організація»? Ваше уявлення про те, що футбольна команда є певним видом переліків гравців, лежить у вашому людському розумі та лише у вашому людському розумі.

List<T>є механізмом . Футбольна команда - це бізнес-об’єкт - тобто об’єкт, який представляє якусь концепцію, що знаходиться в бізнес-області програми. Не змішуйте їх! Футбольна команда - це свого роду команда; у нього є реєстр, реєстр - це список гравців . Список не є певним видом гравців . Реєстр являє список гравців. Тож зробіть властивість, яку називають Rostera List<Player>. І зробіть це, ReadOnlyList<Player>поки ви на ньому, якщо не вірите, що кожен, хто знає про футбольну команду, може видалити гравців із списку.

Чи успадковування List<T>завжди від неприйнятного?

Неприйнятний для кого? Я? Ні.

Коли це прийнятно?

Коли ви будуєте механізм, який розширює List<T>механізм .

Що повинен врахувати програміст, приймаючи рішення про спадщину List<T>чи ні?

Я будую механізм чи бізнес-об’єкт ?

Але це багато коду! Що я отримую за всю цю роботу?

Ви витратили більше часу, набираючи своє запитання про те, що вам потрібно було б написати методи переадресації для відповідних членів у List<T>п'ятдесят разів. Ви явно не боїтеся багатослівності, і ми говоримо про дуже малу кількість коду; це кілька хвилин роботи.

ОНОВЛЕННЯ

Я ще трохи подумав, і є ще одна причина не моделювати футбольну команду як список гравців. Насправді може бути поганою ідеєю моделювати футбольну команду, яка також має список гравців. Проблема з командою, як / з переліком гравців, полягає в тому, що у вас є моментальний знімок команди за момент часу . Я не знаю, що стосується вашого бізнесу для цього класу, але якби у мене був клас, який представляв футбольну команду, я хотів би поставити йому запитання на кшталт "скільки гравців Seahawks пропустили ігри через травму між 2003 та 2013 роками?" або "У якого гравця Денвера, який раніше грав за іншу команду, найбільше за рік збільшилося зростання дворів?" або " Чи цього року Свині пройшли всю дорогу? "

Тобто футбольна команда, як мені здається, добре моделюється як сукупність історичних фактів, таких як, коли гравця було завербовано, поранено, вийшов у відставку тощо. центр, але можуть бути й інші цікаві речі, які ви хочете зробити з цим об’єктом, які потребують більш історичної перспективи.


84
Хоча інші відповіді були дуже корисними, я думаю, що ця проблема стосується моїх проблем безпосередньо. Щодо перебільшення кількості коду, ви праві, що зрештою це не так вже й багато, але я дуже легко плутаюсь, коли включаю якийсь код у свою програму, не розуміючи, чому я його включаю.
Супербест

128
@Superbest: Радий, що можу допомогти! Ви праві вислухати ці сумніви; якщо ви не розумієте, чому ви пишете якийсь код, або розібрайтеся, або напишіть інший код, який ви розумієте.
Ерік Ліпперт

48
@Mehrdad Але ви, здається, забудете золоті правила оптимізації: 1) не робіть цього, 2) не робіть цього ще й 3) не робіть цього, не попередньо зробивши профілі продуктивності, щоб показати, що потрібно оптимізувати.
AJMansfield

107
@Mehrdad: Чесно кажучи, якщо ваша програма вимагає, щоб ви дбали про навантаження на ефективність, скажімо, віртуальних методів, то будь-яка сучасна мова (C #, Java тощо) не є для вас мовою. Тут у мене є ноутбук, який міг би запускати симуляцію кожної аркадної гри, в яку я грав у дитинстві одночасно . Це дозволяє мені не турбуватися про рівень непрямості тут і там.
Ерік Ліпперт

44
@Superbest: Зараз ми точно перебуваємо на кінці спектра "склад не успадковуючий". Ракета складається з частин ракети . Ракета - це не "особливий вид деталей"! Я б вам запропонував, щоб ви її обрізали далі; чому список, на відміну від IEnumerable<T>- це послідовність ?
Ерік Ліпперт

161

Нічого собі, у вашому дописі є цілий ряд питань та питань. Більшість міркувань, які ви отримуєте від Майкрософт, є саме суттєвими. Почнемо з усього, що стосуєтьсяList<T>

  • List<T> є оптимізованим. Основне його використання - використовувати як приватний член об'єкта.
  • Microsoft не має запечатати його , тому що іноді ви можете створити клас , який має ім'я дружнє: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Зараз це так просто, як зробити var list = new MyList<int, string>();.
  • CA1002: Не виставляйте загальних списків . В основному, навіть якщо ви плануєте використовувати цей додаток як єдиний розробник, варто розробитись з хорошими методами кодування, щоб вони прищепилися вам і другого характеру. Вам все ще дозволяється виставити цей список як такий, IList<T>якщо вам потрібен будь-який споживач, який має індексований список. Це дозволить вам змінити реалізацію в класі пізніше.
  • Microsoft зробила Collection<T>дуже загальне, тому що це загальне поняття ... назва говорить усе; це просто колекція. Є більш точні версії , такі як SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>і т.д. , кожен з яких реалізують , IList<T>але НЕ List<T> .
  • Collection<T>дозволяє членам (тобто додавати, видаляти тощо), щоб перекрити, тому що вони віртуальні. List<T>не.
  • Остання частина вашого запитання - це місце. Футбольна команда - це не просто перелік гравців, тому це повинен бути клас, який містить цей список гравців. Подумайте про склад проти успадкування . Футбольна команда має список гравців (реєстр), це не список гравців.

Якби я писав цей код, клас, мабуть, виглядав би приблизно так:

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
вашій посаді є ціла кількість питань " - Ну, я ж сказав, що я був ретельно розгублений. Дякую за посилання на запитання @ Readonly, тепер я бачу, що значна частина мого питання - це особливий випадок загальної проблеми "Склад проти наслідування".
Супербест

3
@Brian: За винятком того, що ви не можете створити загальний за допомогою псевдоніма, усі типи повинні бути конкретними. У моєму прикладі List<T>розширюється як приватний внутрішній клас, який використовує дженерики для спрощення використання та декларування List<T>. Якби розширення було просто class FootballRoster : List<FootballPlayer> { }, то так, я міг би замість цього використати псевдонім. Напевно, варто відзначити, що ви можете додати додаткових членів / функціональних можливостей, але це обмежено, оскільки ви не можете перекрити важливих членів List<T>.
мій

6
Якби це Programmers.SE, я би погоджувався з поточним діапазоном відповідей на голоси, але, оскільки це повідомлення SO, ця відповідь принаймні намагається відповісти на конкретні проблеми C # (.NET), навіть якщо є певні загальні питання дизайну.
Марк Херд

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenТепер , що це хороша причина , щоб не наслідувати зі списку. Інші відповіді мають занадто багато філософії.
Навін

10
@my: Я підозрюю, що ви маєте на увазі "вбивство" питань, оскільки суут - це детектив.
батти

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

Попередній код означає: купа хлопців з вулиці, які грають у футбол, і у них трапляється ім’я. Щось на зразок:

Люди, які грають у футбол

У всякому разі, цей код (з моєї відповіді)

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.
}

Значить: це футбольна команда, яка має керівництво, гравців, адміністраторів тощо.

Зображення із зображенням членів (та інших атрибутів) команди Манчестер Юнайтед

Ось як представлена ​​ваша логіка на фотографіях ...


9
Я б очікував, що ваш другий приклад - це Футбольний клуб, а не футбольна команда. У футбольному клубі є менеджери та адміністратор і т. Д. З цього джерела : "Футбольна команда - це збірна назва, яку дають групі гравців ... Такі команди можуть бути обрані для участі в матчі проти команди, яка виступає проти, та представляти футбольний клуб ... ". Тож, на мою думку, команда - це список гравців (чи, можливо, точніше колекція гравців).
Бен

3
Більш оптимізованийprivate readonly List<T> _roster = new List<T>(44);
SiD

2
Необхідно імпортувати System.Linqпростір імен.
Девіде Канніцо

1
Для наочності: +1
V.7

115

Це класичний приклад композиції проти спадкування .

У цьому конкретному випадку:

Команда - це список гравців з додатковою поведінкою

або

Чи є власна команда об'єктом, який містить список гравців.

Розширюючи Список, ви обмежуєте себе кількома способами:

  1. Ви не можете обмежувати доступ (наприклад, зупиняти людей, що змінюють список). Ви отримуєте всі методи Списку, чи потрібно вам / хотіти їх усіх, чи ні.

  2. Що станеться, якщо ви хочете також мати списки інших речей. Наприклад, команди мають тренерів, менеджерів, вболівальників, обладнання тощо. Деякі з них цілком можуть бути списками.

  3. Ви обмежуєте свої варіанти спадкування. Наприклад, ви можете створити загальний об'єкт Team, а потім мати BaseballTeam, FootballTeam тощо, які успадковують це. Щоб успадкувати зі списку вам потрібно зробити спадщину від Team, але це означає, що всі різні типи команди змушені мати однакову реалізацію цього списку.

Композиція - включаючи об’єкт, що надає потрібну вам поведінку всередині вашого об'єкта.

Успадкування - ваш об'єкт стає екземпляром об'єкта, який має потрібну вам поведінку.

Обидва мають своє використання, але це очевидний випадок, коли композиція є кращою.


1
Розширення на # 2, це не має сенсу для списку , щоб мати-в список.
Cruncher

По-перше, питання не має нічого спільного з композицією проти спадкування. Відповідь: ОП не хоче реалізувати більш конкретний вид списку, тому він не повинен поширювати Список <>. Я перекручений, тому що у вас є дуже високі результати на stackoverflow, і ви повинні це чітко знати, і люди довіряють тому, що ви говорите, тому на сьогоднішній день мінімум 55 людей, які схвалили і чия ідея плутається, вірять, що так чи інакше це нормально будувати система, але явно це не так! # no-rage
Mauro Sampietro

6
@sam Він хоче поведінку у списку. У нього є два варіанти, він може розширити список (успадкування) або він може включити список всередині свого об'єкта (композиції). Можливо, ви неправильно зрозуміли частину запитання чи відповіді, а не 55 людей помилялися, і ви маєте рацію? :)
Тім Б

4
Так. Це вироджений випадок лише з одним супер і суб, але я даю більш загальне пояснення для його конкретного питання. У нього може бути лише одна команда, і вона завжди може мати один список, але вибір все-таки - успадкувати список або включити його як внутрішній об'єкт. Як тільки ви почнете включати декілька типів команди (футбол. Крикет. І т.д.), і вони починають бути просто списком гравців, ви бачите, як у вас є повний склад щодо наслідування. Дивлячись на велику картину важливо у подібних випадках, щоб уникнути неправильного вибору на ранніх стадіях, що означатиме багато рефакторингу пізніше.
Тім Б

3
ОП не запитувало склад, але випадок використання - це чіткий приклад проблеми X: Y, щодо якого один із 4 об'єктно-орієнтованих принципів програмування порушений, неправильно використаний або не повністю зрозумілий. Більш чітка відповідь - це написати спеціалізовану колекцію на зразок стека або черги (яка, здається, не відповідає випадку використання), або зрозуміти склад. Футбольна команда НЕ перелік футболістів. Незалежно від того, запитувала ОП явно це чи ні, не має значення, не розуміючи абстракції, інкапсуляції, спадкування та поліморфізму, вони не зрозуміють відповіді, ерго, X: Y amok
Julia McGuigan

67

Як усі зазначали, команда гравців - це не список гравців. Ця помилка робиться багатьма людьми скрізь, можливо, на різних рівнях знань. Часто проблема є тонкою, а іноді і дуже грубою, як у цьому випадку. Такі конструкції погані, оскільки вони порушують Принцип заміни Ліскова . В Інтернеті є багато хороших статей, що пояснюють це поняття, наприклад, http://en.wikipedia.org/wiki/Liskov_substitution_principle

Підсумовуючи, є два правила, які слід зберігати у відносинах батько / дитина серед класів:

  • Дитина повинна вимагати не менше характеристик, ніж те, що повністю визначає Батька.
  • Батько не повинен мати ніяких характеристик, крім того, що повністю визначає Дитину.

Іншими словами, Батько - це необхідне визначення дитини, а дитина - достатнє визначення Батька.

Ось спосіб продумати рішення та застосувати вищезазначений принцип, який повинен допомогти уникнути такої помилки. Слід перевірити гіпотезу, перевіривши, чи всі операції батьківського класу дійсні для похідного класу як структурно, так і семантично.

  • Чи є футбольна команда переліком футболістів? (Чи всі властивості списку застосовуються до команди в одному значенні)
    • Чи колектив - це сукупність однорідних утворень? Так, команда - це колекція Гравців
    • Чи порядок включення гравців описує стан команди та чи гарантує команда збереження послідовності, якщо явно не змінено? Ні, і Ні
    • Очікується, що гравці будуть включені / відкинуті з урахуванням своєї послідовної позиції в команді? Ні

Як бачите, лише перша характеристика списку застосовна до команди. Отже, команда - це не список. У списку буде деталізована інформація про те, як ви керуєте своєю командою, тому її слід використовувати лише для зберігання об’єктів гравця та маніпулювання методами класу Team.

На цьому етапі я хотів би зазначити, що, на мій погляд, командний клас навіть не повинен реалізовуватися за допомогою Списку; його слід реалізувати, використовуючи структуру даних Set (наприклад, HashSet) у більшості випадків.


8
Хороший вилов у списку проти набору. Це, здається, є помилкою, що надто часто робиться. Коли в колекції може бути лише один екземпляр елемента, якийсь набір або словник повинен бути кращим кандидатом для реалізації. Добре мати команду з двома однойменними гравцями. Не нормально, щоб один гравець включався двічі одночасно.
Фред Мітчелл

1
+1 для деяких хороших моментів, хоча важливіше запитати "чи може структура даних обгрунтовано підтримувати все корисне (в ідеалі коротко і довгостроково)", а не "чи структура даних робить більше, ніж корисна".
Тоні Делрой

1
@TonyD Ну, моменти, які я піднімаю, - це не те, що варто перевірити, "чи структура даних робить більше, ніж корисна". Потрібно перевірити "Якщо структура даних Батьків робить щось нерелевантне, безглузде або інтуїтивно зрозуміле для поведінки, що може мати на увазі клас" Діть ".
Сатьян Райна

1
@TonyD Насправді існує проблема з відсутністю невідповідних характеристик, похідних від Батька, оскільки це призведе до відмови від негативних тестів у багатьох випадках. Програміст міг подовжити людину {eat (); run (); write ();} з горили {eat (); run (); swing ();} думаючи, що немає нічого поганого в людині з додатковою рисою здатності розгойдуватися. І тоді в ігровому світі ваша людина раптом починає обходити всі передбачувані перешкоди, просто перекидаючись на дерева. Якщо чітко не зазначено, практична людина не повинна мати змогу розгойдуватися. Така конструкція залишає api дуже відкритими для зловживань та заплутаності
Satyan Raina

1
@TonyD Я не припускаю, що клас Player також повинен бути похідний від HashSet. Я пропоную класу програвача «у більшості випадків» реалізовувати за допомогою HashSet через Composition, і це повністю деталізація рівня реалізації, а не рівень дизайну (саме тому я згадував це як сторону своєї відповіді). Це дуже добре може бути реалізовано за допомогою списку, якщо є дійсне обгрунтування такої реалізації. Отже, щоб відповісти на ваше запитання, чи потрібно мати пошук O (1) за ключем? Ні. Тому НЕ слід також продовжувати програвач з HashSet.
Сатьян Райна

50

Що робити, якщо у FootballTeamрезервної команди є разом із основною командою?

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

Як би ви це моделювали?

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

Відносини явно мають, а не є .

або RetiredPlayers?

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

Як правило, якщо ви хочете успадкувати колекцію, назвіть клас SomethingCollection.

Чи SomethingCollectionмає сенс ваш сенс? Тільки зробіть це , якщо ваш тип є колекція Something.

У випадку FootballTeamце не правильно звучить. А Team- це більше, ніж а Collection. А Teamможе мати тренерів, тренерів тощо, як вказували інші відповіді.

FootballCollectionзвучить як колекція футболів або, можливо, колекція футбольної атрибутики. TeamCollection, колекція команд.

FootballPlayerCollectionзвучить як колекція гравців, яка була б дійсною назвою для класу, який успадковує, List<FootballPlayer>якщо ви дійсно хотіли цього зробити.

Дійсно List<FootballPlayer>- це ідеально хороший тип для вирішення. Можливо, IList<FootballPlayer>якщо ви повертаєте його з методу.

Підводячи підсумок

Запитайте себе

  1. Є ? або має ?XY XY

  2. Чи означають назви моїх класів, якими вони є?


Якщо тип кожного гравця буде класифікувати його як що належать або DefensivePlayers, OffensivePlayersабо OtherPlayers, можливо , було б законно корисно мати тип , який може бути використаний код , який очікує , List<Player>але також включені члени DefensivePlayers, OffsensivePlayersабо SpecialPlayersтипу IList<DefensivePlayer>, IList<OffensivePlayer>і IList<Player>. Можна використовувати окремий об’єкт для кешування окремих списків, але інкапсуляція їх у межах одного об’єкта, що і основний список, видається чистішим [використовуйте інвалідизацію перелічувача списку ...
supercat

... як підказка на те, що основний список змінився і підсписи потрібно буде відновити під час наступного доступу до них].
supercat

2
Хоча я згоден з висловленим моментом, це дійсно розриває мою душу, бачачи, як хтось дає поради щодо дизайну та пропонує виставити конкретний Список <T> з публічними дітьми та сеттером у бізнес-об'єкті :(
sara

38

Дизайн> Впровадження

Які методи та властивості ви піддаєте, це проектне рішення. Базовий клас, який ви успадковуєте, - це деталь реалізації. Я вважаю, що варто зробити крок назад до колишнього.

Об'єкт - це сукупність даних та поведінки.

Тож вашими першими питаннями мають бути:

  • Які дані містить цей об’єкт у створеній мені моделі?
  • Яку поведінку проявляє цей об’єкт у цій моделі?
  • Як це може змінитися в майбутньому?

Майте на увазі, що успадкування передбачає "isa" (є) відносини, тоді як склад означає, що "має" (hasa) відносини. Виберіть правильний для себе ситуацію на ваш погляд, маючи на увазі, де все може йти в міру розвитку вашої програми.

Подумайте про інтерфейси, перш ніж подумати про конкретні типи, оскільки деяким людям легше перевести свій мозок у «дизайн-режим» таким чином.

Це не те, що кожен свідомо робить на цьому рівні щоденне кодування. Але якщо ви роздумуєте над такою темою, ви ступаєте в дизайнерських водах. Усвідомлення цього може бути визвольним.

Розглянемо особливості дизайну

Погляньте на Список <T> та IList <T> на MSDN або Visual Studio. Подивіться, яким методам та властивостям вони піддаються. Чи всі ці методи схожі на те, що хтось хотів би зробити з FootballTeam на ваш погляд?

Чи має для вас сенс footballTeam.Reverse ()? Чи виглядає footballTeam.ConvertAll <TOutput> () як щось, що ви хочете?

Це не хитрі питання; відповідь справді може бути "так". Якщо ви реалізуєте / успадкуєте Список <Player> або IList <Player>, ви затримаєтесь з ними; якщо це ідеально для вашої моделі, зробіть це.

Якщо ви вирішили так, це має сенс, і ви хочете, щоб ваш об'єкт піддавався лікуванню як колекція / список гравців (поведінка), і, отже, ви хочете реалізувати ICollection або IList, всіма способами це зробіть. Умовно:

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

Якщо ви хочете, щоб ваш об'єкт містив колекцію / список плеєрів (даних), і тому ви хочете, щоб колекція або список були власністю або членом, будь ласка, зробіть це. Умовно:

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

Вам може здатися, що ви хочете, щоб люди могли лише перелічувати набір гравців, а не рахувати їх, додавати до них або видаляти їх. IEnumerable <Player> - цілком правильний варіант, який слід врахувати.

Вам може здатися, що жоден із цих інтерфейсів взагалі не корисний у вашій моделі. Це менш вірогідно (IEnumerable <T> корисний у багатьох ситуаціях), але все ж можливо.

Кожен, хто намагається сказати вам, що один із них категорично і остаточно неправильний у кожному випадку, є помилковим. Кожен, хто намагається сказати вам, що це категорично і остаточно правильно, у кожному випадку є помилковим.

Переходимо до впровадження

Визначившись із даними та поведінкою, ви можете прийняти рішення про впровадження. Сюди входить, від яких конкретних класів ви залежите від спадщини або складу.

Це може бути не великим кроком, і люди часто плутають дизайн та реалізацію, оскільки цілком можливо пропустити все це в голові за секунду-дві і почати друкувати.

Мисленнєвий експеримент

Штучний приклад: як уже згадували інші, команда не завжди є "просто" колекцією гравців. Чи підтримуєте ви збірку результатів матчів для команди? Чи взаємозамінна команда з клубом, за вашою моделлю? Якщо так, і якщо ваша команда - це зібрання гравців, можливо, це також збір персоналу та / або збірка балів. Тоді ви закінчуєте:

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

Незважаючи на дизайн, на даний момент у C # ви не зможете реалізувати все це, успадкувавши з List <T> все одно, оскільки C # "лише" підтримує єдине успадкування. (Якщо ви вже пробували це malarky в C ++, ви можете розглянути це хороша річ.) Реалізація однієї колекції з допомогою наслідування і один з допомогою композиції, ймовірно, відчувати себе брудною. А такі властивості, як Count, стають заплутаними для користувачів, якщо явно не реалізувати ILIst <Player> .Count та IList <StaffMember> .Count тощо, і тоді вони просто болючі, а не заплутані. Ви можете бачити, куди це йде; кишкові відчуття, думаючи про цю алею, цілком можуть сказати тобі, що почуваєшся неправильно рухатися в цьому напрямку (і правильно чи неправильно, твої колеги можуть також, якби ти реалізував це таким чином!)

Короткий відповідь (надто пізно)

Настанова щодо не успадкування класів колекції не є специфікою C #, ви знайдете її у багатьох мовах програмування. Мудрість отримана не законом. Однією з причин є те, що на практиці вважається, що композиція часто перемагає спадщину з точки зору зрозумілості, реалізації та ремонтопридатності. З об'єктами реального світу / домену частіше зустріти корисні та послідовні відносини "hasa", ніж корисні та послідовні відносини "isa", якщо ви не заглиблюєтесь в абстрактне, особливо, коли проходить час і точні дані та поведінка об'єктів у коді зміни. Це не повинно змусити вас завжди виключати спадкування з колекційних класів; але це може бути сугестивним.


34

Перш за все, це стосується юзабіліті. Якщо ви користуєтеся успадкуванням, Teamклас викриє поведінку (методи), призначені виключно для маніпулювання об'єктом. Наприклад, AsReadOnly()або CopyTo(obj)методи не мають сенсу для об'єкта команди. Замість AddRange(items)методу ви, мабуть, хочете більш описовий AddPlayers(players)метод.

Якщо ви хочете використовувати LINQ, реалізація загального інтерфейсу, такого як ICollection<T>або IEnumerable<T>має більше сенсу.

Як було сказано, композиція - це правильний шлях для цього. Просто реалізуйте список гравців як приватну змінну.


30

Футбольна команда - це не список футболістів. Футбольна команда складається зі списку футболістів!

Це логічно неправильно:

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

і це правильно:

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

19
Це не пояснює, чому . ОП знає, що більшість інших програмістів так почувають себе, він просто не розуміє, чому це важливо. Це лише дійсно подання інформації, вже вказане в питанні.
Сервіс

2
Це перше, що я думав над тим, як його дані просто моделювались неправильно. Тому причина вашої моделі даних неправильна.
rball

3
Чому б не успадкувати зі списку? Тому що він не хоче більш конкретного виду списку.
Mauro Sampietro

2
Я не думаю, що другий приклад є правильним. Ви виставляєте стан і таким чином порушуєте інкапсуляцію. Держава повинна бути прихованою / внутрішньою для об'єкта, а спосіб мутації цього стану повинен бути за допомогою публічних методів. Точно, які методи щось слід визначити з бізнес-вимог, але це повинно бути на принципі "потрібно зараз" -основа, а не "може бути зручно деякий час, я не знаю" -основи. Тримайте щільно вашу жировик, і ви заощадите час і сили.
сара

1
Інкапсуляція - це не питання. Я зосереджуюсь на тому, щоб не поширювати тип, якщо ви не хочете, щоб цей тип поводився більш конкретно. Ці поля мають бути автоматичними властивостями в c # та методах get / set іншими мовами, але ось у цьому контексті абсолютно зайвим
Mauro Sampietro

28

Це залежить від контексту

Коли ви розглядаєте свою команду як список гравців, ви проектуєте "ідею" команди з м'ячем для ніг до одного аспекту: ви зменшуєте "команду" до людей, яких бачите на полі. Ця прогноза правильна лише в певному контексті. В іншому контексті це може бути абсолютно неправильним. Уявіть, що ви хочете стати спонсором команди. Тож вам доведеться поговорити з менеджерами команди. У цьому контексті команда проектується до списку її керівників. І ці два списки зазвичай не сильно перетинаються. Інші контексти - поточний проти колишніх гравців тощо.

Незрозуміла семантика

Тому проблема розгляду команди як списку її гравців полягає в тому, що її смисловість залежить від контексту і що вона не може бути розширена, коли контекст змінюється. Крім того, важко виразити, який контекст ви використовуєте.

Заняття розширювані

Коли ви використовуєте клас лише з одним членом (наприклад IList activePlayers), ви можете використовувати його ім’я (та додатково його коментар), щоб зрозуміти контекст. Коли є додаткові контексти, ви просто додаєте додатковий член.

Заняття складніші

У деяких випадках може бути зайвим створити додатковий клас. Кожне визначення класу повинно бути завантажено через завантажувач класів і буде кешоване віртуальною машиною. Це коштує вашої продуктивності та пам’яті. Коли у вас дуже специфічний контекст, може бути нормально розглянути футбольну команду як список гравців. Але в цьому випадку вам слід просто використовувати а IList , а не клас, похідний від нього.

Висновок / міркування

Коли у вас дуже специфічний контекст, добре розглядати команду як список гравців. Наприклад, всередині методу можна повністю написати:

IList<Player> footballTeam = ...

Під час використання F # може бути навіть нормально створити абревіатуру типу:

type FootballTeam = IList<Player>

Але коли контекст ширший або навіть неясний, не варто цього робити. Особливо це відбувається, коли ви створюєте новий клас, контекст якого, в якому він може бути використаний у майбутньому, не є зрозумілим. Попереджувальний знак - це коли ви починаєте додавати додаткові атрибути до свого класу (назва команди, тренера тощо). Це явна ознака того, що контекст, в якому буде використовуватися клас, не є фіксованим і зміниться в майбутньому. У цьому випадку ви не можете розглядати команду як список гравців, але вам слід моделювати список гравців (наразі активних, не травмованих тощо) як атрибути команди.


27

Дозвольте переписати ваше запитання. тож ви можете бачити тему з іншого погляду.

Коли мені потрібно представляти футбольну команду, я розумію, що це в основному назва. Як: "Орли"

string team = new string();

Потім я зрозумів, що команди також мають гравців.

Чому я не можу просто розширити тип рядка, щоб він також містив список гравців?

Ваша точка вступу до проблеми є довільною. Спробуйте думати , що це команда має (властивості), а не те , що вона є .

Після цього ви зможете побачити, чи він поділяє властивості з іншими класами. І подумайте про спадщину.


1
Це хороший момент - команду можна вважати просто назвою. Однак якщо моя заявка спрямована на роботу з діями гравців, то таке мислення трохи менш очевидно. У будь-якому випадку, схоже, питання зводиться в кінцевому рахунку до складу спадщини.
Супербест

6
Це один із найважливіших способів поглянути на це. Як бічна зауваження, будь ласка, врахуйте це: Людина з багатства володіє кількома футбольними командами, він дарує другу в подарунок, друг змінює ім'я команди, звільняє тренера, і замінює всіх гравців. Команда друзів зустрічає чоловічу команду на зеленому, і коли чоловіча команда програє, він каже: "Я не можу повірити, що ти б'єш мене командою, яку я тобі дав!" правдивий чоловік? як би ти це перевірив?
користувач1852503

20

Просто тому, що я думаю, що інші відповіді в значній мірі виходять із дотику того, чи є футбольна команда "є-а" List<FootballPlayer>чи "має-а" List<FootballPlayer>, що насправді не відповідає на це запитання як написано.

В основному ОП просить роз'яснити вказівки щодо спадкування від List<T>:

Посібник говорить, що від вас не слід успадковувати List<T>. Чому ні?

Тому List<T>що немає віртуальних методів. Це менше проблеми у вашому власному коді, оскільки зазвичай ви можете вимкнути реалізацію з відносно невеликим болем - але це може бути значно більша угода в публічному API.

Що таке загальнодоступний API і чому я повинен піклуватися?

Публічний API - це інтерфейс, який ви піддаєте стороннім програмістам. Подумайте рамковий код. І нагадайте, що вказівки, на які посилаються, - це ".NET Framework Design Guidelines", а не ".NET. Настанови щодо дизайну додатків ". Існує різниця, і, загалом кажучи, - дизайн публічного API набагато суворіший.

Якщо мій поточний проект не має і, ймовірно, ніколи не матиме цей публічний API, чи можу я сміливо ігнорувати цю інструкцію? Якщо я успадковую список і виявляється, що мені потрібен публічний API, які труднощі виникнуть у мене?

Досить, так. Можливо, ви хочете розглянути обґрунтування цього питання, щоб зрозуміти, чи стосується воно все-таки до вашої ситуації, але якщо ви не будуєте публічний API, вам не потрібно особливо хвилюватися з приводу проблем API, як-от версії (з них, це підмножина).

Якщо в майбутньому ви додасте загальнодоступний API, вам потрібно буде або абстрагувати свій API від своєї реалізації (не піддаючи List<T>прямого впливу), або порушити вказівки з можливим майбутнім болем, який тягне за собою.

Чому це навіть важливо? Список - це список. Що може змінитися? Що я міг би хотіти змінити?

Залежить від контексту, але оскільки ми використовуємо FootballTeamяк приклад - уявіть, що ви не можете додати, FootballPlayerякщо це призведе до того, що команда перейде за межі зарплати. Можливий спосіб додати це було б щось на зразок:

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

Ах ... але ви не можете, override Addтому що це не так virtual(з міркувань ефективності).

Якщо ви користуєтеся програмою (що, в основному, означає, що ви та всі ваші абоненти зібрані разом), тепер ви можете перейти до використання IList<T>та виправлення будь-яких помилок компіляції:

 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 */
 }

але, якщо ви публічно зазнали впливу третьої сторони, ви просто зробили переломну зміну, яка спричинить компіляцію та / або помилки виконання.

TL; DR - рекомендації щодо публічних API. Для приватних API виконайте все, що завгодно.


18

Чи дозволяє людям сказати

myTeam.subList(3, 5);

взагалі має сенс? Якщо ні, то це не повинен бути список.


3
Можливо, якби ви його назвалиmyTeam.subTeam(3, 5);
Сем Ліч

3
@SamLeach Якщо припустити, що це правда, тоді вам все одно знадобиться композиція, а не спадщина. Як subListнавіть Teamбільше не повернеться .
Cruncher

1
Це переконувало мене більше, ніж будь-яка інша відповідь. +1
FireCubez

16

Це залежить від поведінки вашого об'єкта "команди". Якщо вона поводиться так само, як колекція, можливо, спочатку представляти її звичайним списком. Тоді ви можете почати помічати, що ви зберігаєте дублюючий код, який повторюється у списку; на даний момент у вас є можливість створити об’єкт FootballTeam, який обгортає список гравців. Клас FootballTeam стає домом для всього коду, який повторюється у списку гравців.

Це робить мій код непотрібним багатослівним. Тепер я повинен зателефонувати my_team.Players.Count замість просто my_team.Count. На щастя, за допомогою C # я можу визначити індексатори, щоб зробити індексацію прозорою та переслати всі методи внутрішнього списку ... Але це дуже багато коду! Що я отримую за всю цю роботу?

Інкапсуляція. Ваші клієнти не повинні знати, що відбувається всередині FootballTeam. Для всіх ваших клієнтів відомо, що це може бути реалізовано, переглянувши список гравців в базі даних. Їм не потрібно знати, і це покращує ваш дизайн.

Це просто не має сенсу. Футбольна команда не має "списку гравців". Це список гравців. Ви не кажете, що "Джон МакФоотбаллер приєднався до гравців SomeTeam". Ви кажете "Джон приєднався до SomeTeam". Ви не додаєте листа до "символів рядка", ви додаєте лист до рядка. Ви не додаєте книгу до книг бібліотеки, ви додаєте книгу до бібліотеки.

Рівно :) ви скажете footballTeam.Add (john), а не footballTeam.List.Add (john). Внутрішній список не буде видимим.


1
ОП хоче зрозуміти, як МОДЕЛЮвати РЕАЛЬНІСТЬ, але потім визначає футбольну команду як список футболістів (що концептуально неправильно). У цьому проблема. На мою думку, будь-який інший аргумент вводить його в оману.
Мауро Самп'єтро

3
Я не погоджуюся, що список гравців концептуально неправильний. Не обов'язково. Як хтось ще писав на цій сторінці, "Усі моделі помиляються, але деякі корисні"
xpmatteo

Правильні моделі важко отримати, коли вже зроблено, вони стануть у пригоді. Якщо модель може бути використана неправильно, це концептуально неправильно. stackoverflow.com/a/21706411/711061
Mauro Sampietro

15

Тут є багато відмінних відповідей, але я хочу торкнутися того, чого я не бачив згаданого: Об'єктно-орієнтований дизайн стосується розширення можливостей об'єктів .

Ви хочете скласти всі свої правила, додаткові роботи та внутрішні деталі всередині відповідного об'єкта. Таким чином, інші об'єкти, що взаємодіють з цим, не повинні турбуватися про це все. Насправді, ви хочете піти на крок далі і активно не дозволяти іншим об'єктам обійти ці внутрішні місця.

Коли ви успадковуєте від List, усі інші об'єкти можуть бачити вас як Список. Вони мають прямий доступ до методів додавання та видалення гравців. І ти втратив контроль; наприклад:

Припустимо, ви хочете розрізнитись, коли гравець піде, знаючи, пішов він у відставку, звільнився чи був звільнений. Ви можете реалізувати RemovePlayerметод, який приймає відповідний вхідний перелік. Однак, успадкувавши від List, ви не змогли б запобігти прямий доступ до Remove, RemoveAllі навіть Clear. Як наслідок, ви насправді знешкодили свій FootballTeamклас.


Додаткові думки щодо інкапсуляції ... Ви викликали таке занепокоєння:

Це робить мій код непотрібним багатослівним. Тепер я повинен зателефонувати my_team.Players.Count замість просто my_team.Count.

Ви маєте рацію, це було б непотрібно, щоб усі клієнти могли використовувати вашу команду. Однак ця проблема є дуже малою порівняно з тим, що ви піддаєтесь List Playersусім і всім, тому вони можуть поспілкуватися з вашою командою без вашої згоди.

Ви продовжуєте говорити:

Це просто не має сенсу. Футбольна команда не має "списку гравців". Це список гравців. Ви не кажете, що "Джон МакФоотбаллер приєднався до гравців SomeTeam". Ви кажете "Джон приєднався до SomeTeam".

Ви помиляєтесь у першому біті: киньте слово «список», і насправді очевидно, що в команди є гравці.
Однак ти вдарив цвях по голові другим. Ви не хочете, щоб клієнти телефонували ateam.Players.Add(...). Ви хочете, щоб вони дзвонили ateam.AddPlayer(...). І ваша реалізація (можливо, серед іншого) дзвонить Players.Add(...)внутрішньо.


Сподіваємось, ви зможете побачити, наскільки важливою є інкапсуляція для досягнення розширення можливостей ваших об'єктів. Ви хочете дозволити кожному класу добре виконувати свою роботу, не боячись втручання інших об'єктів.


12

Який правильний спосіб C # представляє структуру даних ...

Ремебер: "Усі моделі помиляються, але деякі корисні". - Джордж ЕП Коробка

Не існує «правильного шляху», лише корисний.

Виберіть той, який корисний вам та / вашим користувачам. Це воно. Розвивайтесь економічно, не перестарайтеся з інженером. Чим менше коду ви пишете, тим менше коду вам знадобиться налагоджувати. (читайте наступні видання).

- Відредаговано

Моя найкраща відповідь була б ... це залежить. Наслідування зі списку може піддавати клієнтів цього класу методам, які, можливо, не повинні піддаватися впливу, насамперед тому, що FootballTeam виглядає як суб'єкт господарювання.

- Видання 2

Я щиро не пам’ятаю, на що я мав на увазі коментар «не зайвий інженер». Хоча я вважаю, що настрій KISS є гарним посібником, я хочу підкреслити, що успадкування бізнес-класу від List створить більше проблем, ніж це вирішить, через витік абстракції .

З іншого боку, я вважаю, що існує обмежена кількість випадків, коли просто успадкувати зі Списку корисно. Як я писав у попередньому виданні, це залежить. На відповідь у кожному випадку сильно впливають як знання, досвід, так і особисті переваги.

Дякую @kai за те, що допомагає мені точніше думати про відповідь.


Моя найкраща відповідь була б ... це залежить. Наслідування зі списку може піддавати клієнтів цього класу методам, які, можливо, не повинні піддаватися впливу, насамперед тому, що FootballTeam виглядає як суб'єкт господарювання. Я не знаю, чи варто редагувати цю відповідь чи публікувати іншу, будь-яку ідею?
ArturoTena

2
Трохи про " успадковування від a Listможе піддавати клієнтів цього класу методам, які, можливо, не повинні піддаватися впливу " - саме те, що робить спадкування від Listабсолютного "ні-ні". Опинившись, вони поступово зловживаються та неправильно використовуються з часом. Економія часу завдяки "економічному розвиткові" може легко призвести до десятикратної економії, втраченої в майбутньому: налагодження зловживань і врешті рефакторинг для виправлення спадщини. Принцип YAGNI також можна розглядати як сенс: Вам не потрібні всі ці методи List, тому не піддавайте їх дії.
Розчарований

3
"не надмірно інженер. Чим менше код ви пишете, тим менше коду вам знадобиться налагоджувати." <- Я думаю, що це вводить в оману. Інкапсуляція та композиція над успадкуванням НЕ є надмірною технікою, і це не викликає більшої потреби в налагодженні. За допомогою інкапсуляції ви обмежуєте кількість способів, які клієнти можуть використовувати (і зловживати) своїм класом, і, таким чином, у вас є менше точок входу, які потребують перевірки та перевірки введення даних. Успадкування Listчерез те, що це швидко і просто, і, таким чином, призведе до меншої кількості помилок, це просто неправильно, це просто поганий дизайн і поганий дизайн призводить до набагато більше помилок, ніж "над технікою".
сара

@kai Я погоджуюся з вами у кожному пункті. Я щиро не пам’ятаю, на що я мав на увазі коментар «не зайвий інженер». Щодо іншого, я вважаю, що існує обмежена кількість випадків, коли просто успадкувати зі Списку корисно. Як я писав у пізнішому виданні, це залежить. На відповідь у кожному випадку сильно впливають як знання, досвід, так і особисті переваги. Як і все в житті. ;-)
ArturoTena

11

Це нагадує мені про компроміс "Є" проти ". Іноді простіше і має сенс успадкувати безпосередньо від суперкласу. В інших випадках має сенс створити окремий клас і включити клас, який би ви отримали у спадок як змінну члена. Ви все ще можете отримати доступ до функціональності класу, але не пов'язані з інтерфейсом або будь-якими іншими обмеженнями, які можуть виникнути внаслідок наслідування від класу.

Що ти робиш? Як і в багатьох справах ... це залежить від контексту. Я б використав посібник, що для того, щоб успадкувати інший клас, справді мають існувати відносини "є". Отже, якщо ви пишете клас під назвою BMW, він може успадкувати від Автомобіля, оскільки BMW справді є автомобілем. Клас Кінь може успадкувати від класу Ссавців, тому що кінь насправді є ссавцем у реальному житті, і будь-яка функція Ссавця повинна відповідати Коні. Але чи можете ви сказати, що команда - це список? З того, що я можу сказати, схоже, що Команда насправді "є" Список. Отже, у цьому випадку я мав би список як змінну члена.


9

Моя брудна таємниця: Мені все одно, що кажуть люди, і я це роблю. .NET Framework поширюється за допомогою "XxxxCollection" (приклад UIElementCollection на початку моєї голови).

Отже, що перешкоджає мені говорити:

team.Players.ByName("Nicolas")

Коли я вважаю це краще, ніж

team.ByName("Nicolas")

Більше того, мій програвач PlayerCollection може використовуватися іншим класом, наприклад "Club", без будь-якого дублювання коду.

club.Players.ByName("Nicolas")

Найкращі практики вчорашнього дня, можливо, не будуть завтрашніми. Немає підстав для більшості найкращих практик, більшість - лише широка згода між громадою. Замість того, щоб запитувати у громади, чи буде вона звинувачувати вас, коли ви це робите, запитайте себе, що можна зрозуміти і піддати?

team.Players.ByName("Nicolas") 

або

team.ByName("Nicolas")

Дійсно. У вас є сумніви? Тепер, можливо, вам потрібно пограти з іншими технічними обмеженнями, які заважають вам використовувати List <T> у вашому реальному випадку використання. Але не додайте обмеження, яке не повинно існувати. Якщо Microsoft не документувала, чому це, то це, безумовно, "найкраща практика", яка приходить з нізвідки.


9
Хоча важливо мати сміливість та ініціативу, щоб кинути виклик прийнятій мудрості, коли це доречно, я вважаю, що розумно спочатку зрозуміти, чому прийнята мудрість стала прийнятою в першу чергу, перш ніж приступати до її оскарження. -1 тому, що "Ігноруйте цю інструкцію!" не є гарною відповіддю на питання "Чому існує ця настанова?" Між іншим, громада не просто «звинувачувала мене» у цій справі, а надала задовільне пояснення, що вдалося мене переконати.
Супербест

3
Насправді, коли ніяких пояснень не робиться, ваш єдиний спосіб - просунути межу і перевірити, не боячись порушень. Тепер. Запитайте себе, навіть якщо Список <T> не був гарною ідеєю. Скільки болю було б змінити спадщину зі Списку <T> на Колекцію <T>? Здогадка: менше часу, ніж публікація на форумі, незалежно від тривалості вашого коду за допомогою інструментів рефакторингу. Зараз це гарне питання, але не практичне.
Ніколя Доріє

Для уточнення: я, звичайно, не чекаю, коли благословення SO отримає спадщину від того, що я хочу, але я цього хочу, але суть мого питання полягала в тому, щоб зрозуміти міркування, які повинні прийняти це рішення, і чому, здається, так багато досвідчених програмістів вирішив протилежне тому, що я зробив. Collection<T>це не те саме, List<T>що може знадобитися зовсім небагато роботи, щоб перевірити великий проект.
Супербест

2
team.ByName("Nicolas")засоби "Nicolas"- це назва команди.
Сем Ліч

1
"не додайте обмеження, яке не повинно існувати" Au contraire, не піддавайте нічого, чого у вас немає вагомих причин. Це не пуризм чи сліпа ревність, а також не остання тенденція дизайнерської моди. Це базовий об'єктно-орієнтований дизайн 101, початковий рівень. Не секрет, чому це «правило» існує, це результат декількох десятиліть досвіду.
сара

8

Вказівки говорять про те, що загальнодоступний API не повинен розкривати внутрішнє дизайнерське рішення про те, використовуєте ви список, набір, словник, дерево чи інше. "Команда" - це не обов'язково список. Ви можете реалізовувати його як список, але користувачі вашого загальнодоступного API повинні використовувати ваш клас за потребою знати. Це дозволяє змінити своє рішення та використовувати іншу структуру даних, не впливаючи на загальнодоступний інтерфейс.


Зрештою, після пояснення @EricLippert та інших, ви насправді дали чудову відповідь на частину мого запитання API - крім того, що ви сказали, якщо я це зробити class FootballTeam : List<FootballPlayers>, користувачі мого класу зможуть сказати, що я ' ве успадкував від List<T>за допомогою відображення, бачачи List<T>методи , які не мають сенсу для футбольної команди, і бути в змозі використати FootballTeamв List<T>, так що я б розкривають деталі реалізації для клієнта (без необхідності).
Супербест

7

Коли кажуть List<T>, що "оптимізовано", я думаю, що вони хочуть означати, що він не має таких функцій, як віртуальні методи, які дорожче. Отже, проблема полягає в тому, що після викриття List<T>у вашому загальнодоступному API ви втрачаєте можливість застосовувати бізнес-правила або налаштовуєте їх функціональність пізніше. Але якщо ви використовуєте цей успадкований клас як внутрішній у межах свого проекту (на відміну від потенційно підданих тисячам ваших клієнтів / партнерів / інших команд як API), можливо, це буде добре, якщо це економить ваш час і це функціонал, який ви хочете дублікат. Перевага наслідування від того List<T>, що ви усуваєте безліч тупих коду обгортки, який просто ніколи не буде налаштований в осяжному майбутньому. Також якщо ви хочете, щоб ваш клас явно мав абсолютно таку саму семантику, якList<T> протягом життя ваших API, тоді також це може бути добре.

Я часто бачу, що багато людей роблять багато зайвих робіт саме тому, що правило FxCop так говорить або хтось у блозі говорить, що це "погана" практика. Багато разів це перетворює код на розробку дивної форми палуозу. Як і багато правил, трактуйте це як правило, яке може мати винятки.


4

Хоча я не маю складного порівняння, як це робить більшість цих відповідей, я хотів би поділитися своїм методом вирішення цієї ситуації. Розширюючи IEnumerable<T>, ви можете дозволити своєму Teamкласу підтримувати розширення запитів Linq, не публічно відкриваючи всі методи та властивості 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

Якщо користувачам вашого класу потрібні всі способи та властивості ** Список, який ви маєте, ви повинні отримати його клас. Якщо вони їм не потрібні, додайте Список та зробіть обгортки для методів, які фактично потребують ваші користувачі класу.

Це суворе правило, якщо ви пишете загальнодоступний API або будь-який інший код, який буде використовуватися багатьма людьми. Ви можете ігнорувати це правило, якщо у вас є крихітний додаток і не більше 2 розробників. Це заощадить певний час.

Для крихітних програм ви також можете обрати іншу, менш сувору мову. Ruby, JavaScript - все, що дозволяє писати менше коду.


3

Я просто хотів додати, що Бертран Мейєр, винахідник Ейфеля та дизайну за контрактом, отримав би у Teamспадок List<Player>без того, як бив повіку.

У своїй книзі « Об’єктно-орієнтована побудова програмного забезпечення» він обговорює реалізацію системи GUI, де прямокутні вікна можуть мати дочірні вікна. Він просто має у Windowспадок від обох Rectangleі Tree<Window>повторно використовувати його.

Однак C # - це не Ейфелева. Останній підтримує багаторазове успадкування та перейменування функцій . У C #, коли ви підклас, ви успадковуєте і інтерфейс, і імплементацію. Ви можете змінити реалізацію, але умови виклику скопіюються безпосередньо з суперкласу. Однак в Ейфелі ви можете змінювати назви загальнодоступних методів, щоб ви могли перейменовувати Addі Removeв, Hireі Fireв свій Team. Якщо екземпляр Teamповернеться назад до List<Player>, абонент буде використовувати Addі Removeмодифікувати його, але ваші віртуальні методи Hireі Fireбудуть викликані.


1
Тут проблема не в багатократному успадкуванні, міксинах (тощо) або в тому, як боротися з їх відсутністю. Будь-яка мова, навіть у людській мові, команда не є списком людей, а складається зі списку людей. Це абстрактне поняття. Якщо Бертран Мейєр чи хтось керує командою шляхом підкласингу, Список робить неправильно. Вам слід підкласирувати список, якщо ви хочете більш конкретний вид списку. Сподіваюся, ви згодні.
Mauro Sampietro

1
Це залежить від того, яка спадщина для вас. Сама по собі це абстрактна операція, це не означає, що два класи знаходяться в "є особливим видом" відносин, навіть якщо це основне тлумачення. Однак успадкування може розглядатися як механізм повторного використання, якщо мовна конструкція його підтримує, навіть якщо композиція є переповненою альтернативою на сьогодні.
Олексій

2

Я думаю, я не згоден з вашим узагальненням. Команда - це не просто колекція гравців. Команда має набагато більше інформації про неї - ім'я, емблема, колекція управлінського / адміністративного персоналу, колекція тренерського екіпажу, потім колекція гравців. Отже, у вашому класі FootballTeam має бути 3 колекції, а не сама колекція; якщо це правильно моделювати реальний світ.

Ви можете розглянути клас PlayerCollection, який, як і спеціалізований StringCollection, пропонує деякі інші засоби - наприклад, перевірку та перевірку, перш ніж об'єкти додаються до внутрішнього магазину або видаляються з них.

Можливо, поняття ставок PlayerCollection відповідає вашому бажаному підходу?

public class PlayerCollection : Collection<Player> 
{ 
}

І тоді FootballTeam може виглядати так:

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

Віддавайте перевагу інтерфейсам над класами

Класи повинні уникати походження з класів, а натомість реалізувати необхідний мінімальний інтерфейс.

Спадщина порушує інкапсуляцію

Виведення з класів перерви інкапсуляції :

  • розкриває внутрішні деталі про те, як реалізується ваша колекція
  • оголошує інтерфейс (набір загальнодоступних функцій та властивостей), який може бути невідповідним

Крім усього іншого, це ускладнює переформатування коду.

Класи - це деталізація впровадження

Заняття - це деталі реалізації, які слід приховувати від інших частин вашого коду. Коротше кажучи System.List, це конкретна реалізація абстрактного типу даних, яка може бути або не підходить зараз і в майбутньому.

Концептуально той факт, що System.Listтип даних називається "список", є трохи червоним оселедцем. A System.List<T>- це мутаційна впорядкована колекція, яка підтримує амортизовані операції O (1) для додавання, вставки та вилучення елементів та O (1) операції для отримання кількості елементів або отримання та встановлення елемента за індексом.

Чим менший інтерфейс, тим гнучкіший код

При проектуванні структури даних, чим простіший інтерфейс, тим гнучкіший код. Подивіться, наскільки потужний LINQ для демонстрації цього.

Як вибрати інтерфейси

Коли ви думаєте, що "список", ви повинні почати з того, щоб сказати собі: "Мені потрібно представити колекцію бейсболістів". Тож скажімо, ви вирішили моделювати це з класом. Перш за все, слід вирішити, який мінімальний об'єм інтерфейсів потрібно буде піддавати цьому класу.

Деякі питання, які можуть допомогти керувати цим процесом:

  • Чи потрібно мені мати рахунок? Якщо не розглянути можливість здійсненняIEnumerable<T>
  • Чи зміниться ця колекція після її ініціалізації? Якщо не враховувати IReadonlyList<T>.
  • Чи важливо я мати доступ до елементів за індексом? РозглянемоICollection<T>
  • Чи важливий порядок, в якому я додаю елементи до колекції? Може, це ISet<T>?
  • Якщо ви справді хочете цього зробити, тоді продовжуйте та впроваджуйте IList<T>.

Таким чином, ви не будете зв'язувати інші частини коду до деталей про реалізацію колекції бейсбольних гравців і зможете змінювати спосіб його реалізації, доки не будете дотримуватися інтерфейсу.

Використовуючи такий підхід, ви побачите, що код стає легше читати, переробляти та використовувати повторно.

Примітки про те, як уникати котельних плит

Реалізація інтерфейсів у сучасному IDE має бути простим. Клацніть правою кнопкою миші та виберіть "Реалізація інтерфейсу". Потім перешліть усі реалізації до класу-члена, якщо потрібно.

Однак, якщо ви виявите, що ви пишете багато котлован, це може бути тому, що ви піддаєте більше функцій, ніж ви повинні бути. Це та сама причина, що вам не слід успадковувати клас.

Ви також можете розробити менші інтерфейси, які мають сенс для вашої програми, а може бути лише пара допоміжних функцій розширення, щоб відобразити ці інтерфейси на будь-які інші, які вам потрібні. Такий підхід я застосував у власному IArrayінтерфейсі для бібліотеки LinqArray .


2

Проблеми з серіалізацією

Один аспект відсутній. Класи, які успадковуються зі списку <>, неможливо правильно серіалізувати за допомогою XmlSerializer . У такому випадку замість цього слід використовувати DataContractSerializer або потрібна власна серіалізаційна реалізація.

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

Подальше читання: Коли клас успадковується зі списку <>, XmlSerializer не серіалізує інші атрибути


0

Коли це прийнятно?

Щоб цитувати Еріка Ліпперта:

Коли ви будуєте механізм, який розширює List<T>механізм.

Наприклад, вам набридло відсутність AddRangeметоду в IList<T>:

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

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.