Почему бы не наследовать от List <T>?

1033

При планировании моих программ я часто начинаю с такой мысли:

Футбольная команда - это всего лишь список футболистов. Поэтому я должен представить его с помощью:

var football_team = new List<FootballPlayer>();

Заказ этого списка представляет собой порядок, в котором игроки перечислены в списке.

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

Итак, я думаю:

Хорошо, футбольная команда точно так же, как и список игроков, но, кроме того, она имеет имя (a string) и общее количество баллов (int)..NET не предоставляет класс для хранения футбольных команд, поэтому я сделаю свой собственный класс. Самая подобная и соответствующая существующая структура List<FootballPlayer>, поэтому я наследую ее:

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

Но оказывается, что руководство говорит, что вы не должны наследовать от List<T>. Я полностью смущен этим руководством в двух отношениях.

Почему бы и нет?

По-видимому List оптимизирован для производительности. Как так? Какие проблемы с производительностью я вызову, если я продолжу List? Что именно сломается?

Другая причина, по которой я видел, заключается в том, что List предоставляется Microsoft, и я не контролирую ее, поэтому я не могу ее изменить позже, после того, как вы откроете "открытый API" ". Но я не могу это понять. Что такое публичный API и почему меня это беспокоит? Если мой текущий проект не имеет и вряд ли когда-либо будет иметь этот общедоступный API, могу ли я смело игнорировать это руководство? Если я наследую от List, и, оказывается, мне нужен публичный API, какие трудности у меня есть?

Почему это даже имеет значение? Список - это список. Что может измениться? Что я могу изменить?

И, наконец, если Microsoft не хочет, чтобы я наследовал от List, почему они не сделали класс sealed?

Что еще я должен использовать?

По-видимому, для пользовательских коллекций Microsoft предоставила класс Collection, который следует расширить вместо List. Но этот класс очень голый и не имеет много полезных вещей, например, AddRange, например. jvitor83 answer обеспечивает обоснование производительности для этого конкретного метода, но как медленный AddRange не лучше, чем AddRange?

Унаследовано от Collection больше, чем наследование от List, и я не вижу никакой выгоды. Конечно, Microsoft не сказала бы мне, чтобы я делал дополнительную работу без причины, поэтому я не могу не чувствовать себя так, как будто я что-то неправильно понимаю, а наследование Collection на самом деле не является правильным решением для моей проблемы.

Я видел такие предложения, как реализация IList. Просто нет. Это десятки строк кода шаблона, который ничего мне не приносит.

Наконец, некоторые предлагают обернуть List во что-то:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Есть две проблемы с этим:

  • Это делает мой код излишне подробным. Теперь я должен называть my_team.Players.Count вместо my_team.Count. К счастью, с С# я могу определить индексаторов, чтобы сделать индексирование прозрачным, и переслать все методы внутреннего List... Но это много кода! Что я получу за все, что работает?

  • Это просто не имеет никакого смысла. Футбольная команда не имеет "списка" игроков. Это список игроков. Вы не говорите: "Джон Макфуталлер присоединился к игрокам SomeTeam". Вы говорите: "Джон присоединился к SomeTeam". Вы не добавляете письмо к "строковым символам", вы добавляете письмо к строке. Вы не добавляете книгу в библиотечные книги, вы добавляете книгу в библиотеку.

Я понимаю, что то, что происходит "под капотом", можно сказать, "добавление X-Y внутреннего списка", но это похоже на очень противоречивый способ мышления о мире.

Мой вопрос (обобщенный)

Каков правильный способ С# для представления структуры данных, которая "логически" (то есть "для человеческого разума" ) является всего лишь List of things с несколькими колокольчиками и свистами?

Наследовать от List<T> всегда неприемлемо? Когда это приемлемо? Почему, почему нет? Что должен программист учитывать при принятии решения о наследовании от List<T> или нет?

  • 68
    Итак, действительно ли вы задаетесь вопросом, когда использовать наследование (квадрат - это прямоугольник, потому что всегда разумно указывать квадрат, когда запрашивается универсальный прямоугольник), и когда использовать композицию (кроме того, у FootballTeam есть список игроков-футболистов) к другим свойствам, которые столь же "фундаментальны" для него, как имя)? Будут ли другие программисты смущены, если в другом месте кода вы передадите им FootballTeam, когда они ожидают простой список?
  • 6
    @FlightOdyssey Похоже, что на практике мой вопрос звучит так: «Почему композиция лучше наследования при расширении функциональности List ?» Я бы посчитал «список игроков» самым фундаментальным свойством футбольной команды - прежде всего других, таких как имя или бюджет. Может существовать команда, у которой нет названия (соседские дети, которые формируют две команды, чтобы играть днем), но команда без игроков не имеет смысла для меня. Что касается удивления, я не знаю. Мне трудно представить, что кого-то удивит такое наследство, но я чувствую, что могу что-то упустить.
Показать ещё 22 комментария
Теги:
oop
inheritance
design

26 ответов

1255
Лучший ответ

Здесь есть хорошие ответы. Я бы добавил к ним следующие моменты.

Каков правильный способ С# представления структуры данных, который "логически" (то есть "для человеческого разума" ) - это всего лишь список вещей с несколькими колокольчиками и свистками?

Спросите любого десяти людей, не являющихся компьютерными программистами, которые знакомы с наличием футбола, чтобы заполнить пробел:

A football team is a particular kind of _____

Кто-нибудь сказал "список футболистов с несколькими колокольчиками", или все они говорили "спортивная команда" или "клуб" или "организация"? Ваше мнение о том, что футбольная команда - это особый список игроков, - это ваш ум и ваш человеческий разум.

List<T> - механизм. Футбольная команда - это бизнес-объект, то есть объект, который представляет собой концепцию, которая находится в бизнес-области программы. Не смешивайте их! Футбольная команда - это своего рода команда; у него есть реестр, список - список игроков. Список не является отдельным списком игроков. Список - это список игроков. Поэтому создайте свойство Roster, которое является List<Player>. И сделайте это ReadOnlyList<Player>, пока вы на нем, если не верите, что все, кто знает о футбольной команде, могут удалить игроков из списка.

Наследовать от List<T> всегда неприемлемо?

Неприемлемо для кого? Меня? Нет.

Когда это приемлемо?

Когда вы создаете механизм, расширяющий механизм List<T>.

Что должен программист учитывать при принятии решения о наследовании от List<T> или нет?

Я создаю механизм или бизнес-объект?

Но это много кода! Что я получу за все, что работает?

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

UPDATE

Я немного подумал, и есть еще одна причина не моделировать футбольную команду как список игроков. На самом деле, может быть, неплохая идея смоделировать футбольную команду как список игроков. Проблема с командой, имеющей список игроков, заключается в том, что у вас есть моментальный снимок команды в момент времени. Я не знаю, каково ваше деловое дело для этого класса, но если бы у меня был класс, представлявший футбольную команду, я бы хотел задать ему вопросы: "Сколько игроков Seahawks пропустили игры из-за травмы в период с 2003 по 2013 год?" или "Какой игрок в Денвере, который ранее играл за другую команду, имел наибольшее увеличение по годам в ярдах?" или "Разве пигеры прошли весь путь в этом году?

То есть, мне кажется, что футбольная команда хорошо смоделирована как сборник исторических фактов, например, когда игрок был завербован, получил травму, вышел на пенсию и т.д. Очевидно, что текущий список игроков является важным фактом, который, вероятно, должен быть впереди -центр, но могут быть другие интересные вещи, которые вы хотите сделать с этим объектом, которые требуют более исторической перспективы.

  • 62
    Хотя другие ответы были очень полезны, я думаю, что этот ответ решает мои проблемы наиболее непосредственно. Что касается преувеличения количества кода, вы правы в том, что в конце концов это не так уж много работы, но я очень запутываюсь, когда включаю какой-то код в свою программу, не понимая, почему я его включаю.
  • 101
    @Superbest: Рад, что смог помочь! Вы правы выслушать эти сомнения; если вы не понимаете, почему пишете какой-то код, либо разберитесь в этом, либо напишите другой код, который вы понимаете.
Показать ещё 41 комментарий
230

Наконец, некоторые предлагают обернуть список в чем-то:

Это правильный путь. "Безусловно многословие" - это плохой способ взглянуть на это. Он имеет явное значение, когда вы пишете my_team.Players.Count. Вы хотите подсчитать игроков.

my_team.Count

.. ничего не значит. Считаете что?

Команда не является списком, состоящим не только из списка игроков. Команда владеет игроками, поэтому игроки должны быть ее частью (членом).

Если вы действительно обеспокоены тем, что это слишком многословный, вы всегда можете выставлять свойства из команды:

public int PlayerCount {
    get {
        return Players.Count;
    }
}

.., который становится:

my_team.PlayerCount

Теперь это имеет смысл и придерживается Закон Деметры.

Вы также должны учитывать принцип Composite reuse. Наследуя от List<T>, вы говорите, что команда является игроком и выставляет из него ненужные методы. Это неверно. Как вы сказали, команда - это больше, чем список игроков: у нее есть имя, менеджеры, члены совета, тренеры, медицинский персонал, зарплата и т.д. Если ваш класс команды содержит список игроков, вы "Команда имеет список игроков", но у нее также могут быть другие вещи.

  • 2
    Я изменил свою формулировку на «многословный», чтобы сделать его более понятным. Я не понимаю, почему вы говорите, что my_team.Count "ничего не значит". Для меня «подсчет команды» является совершенно осмысленным понятием. Я понимаю, почему вы можете настаивать на том, что кто-то действительно «считает игроков команды» - так это философское различие? Или есть практическая причина, почему упаковка лучше? Я жалуюсь на дополнительную работу, потому что каждый раз, когда я возвращаюсь к этому коду, я думаю: «Подожди, почему я завернул это? Почему бы просто не наследовать?» и я не мог придумать комментарий, объясняющий это, либо.
  • 0
    @Superbest: если вы говорите о «дополнительной работе» для управления списком, вы всегда можете выставить свойство «TeamPlayers» и сделать для него AddRange, для этого нет цели наследовать от List <TeamPlayers>
Показать ещё 9 комментариев
123

Ничего себе, ваш пост имеет целую массу вопросов и очков. Большинство аргументов, которые вы получаете от Microsoft, точно на месте. Начните со всего про 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.
}
  • 3
    «В вашем посте есть целая куча вопросов » - ну, я же сказал, что был полностью сбит с толку. Спасибо за ссылку на вопрос @ Readonly, теперь я вижу, что большая часть моего вопроса является частным случаем более общей проблемы «Композиция против наследования».
  • 0
    FootballGroup: List <FootballPlayers>, мне кажется абсолютно приемлемым и очень эффективным. Не было никаких реальных технических причин, объясняющих, почему эта конструкция не заслуживала права на жизнь, только философские аргументы.
Показать ещё 15 комментариев
105
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Предыдущий код означает: кучка парней с улицы, играющих в футбол, и у них, похоже, есть имя. Что-то вроде:

Изображение 966

В любом случае, этот код (из ответа m-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; }
    }

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

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

Значит: это футбольная команда, в которой есть руководство, игроки, администраторы и т.д. Что-то вроде:

Изображение 967

Вот как ваша логика представлена ​​на фотографиях...

  • 50
    Все, что ему нужно сейчас - это метод SackManager () ...
  • 4
    Я ожидаю, что ваш второй пример - футбольный клуб, а не футбольная команда. В футбольном клубе есть менеджеры, администраторы и т. Д. Из этого источника : «Футбольная команда - это собирательное название, данное группе игроков ... Такие команды могут быть выбраны для игры в матче против команды соперника, для представления футбольного клуба». ... ". Так что я считаю, что команда - это список игроков (или, точнее, совокупность игроков).
Показать ещё 2 комментария
80

Это классический пример composition vs inheritance.

В этом конкретном случае:

Является ли команда списком игроков с добавленным поведением.

или

Является ли команда собственным объектом, который содержит список игроков.

Расширяя список, вы ограничиваете себя несколькими способами:

  • Вы не можете ограничить доступ (например, остановить людей, изменяющих список). Вы получаете все методы List, хотите ли вы/хотите их всех или нет.

  • Что произойдет, если вы хотите иметь списки других вещей. Например, в командах есть тренеры, менеджеры, болельщики, оборудование и т.д. Некоторые из них вполне могут быть списками в своем собственном праве.

  • Вы ограничиваете свои варианты для наследования. Например, вы можете создать общий объект Team, а затем получить BaseballTeam, FootballTeam и т.д. Чтобы наследовать из списка, вам нужно наследовать от Team, но это значит, что все команды разных типов вынуждены иметь одну и ту же реализацию этого списка.

Состав - включая объект, дающий поведение, которое вы хотите в своем объекте.

Наследование - ваш объект становится экземпляром объекта, у которого есть поведение, которое вы хотите.

Оба имеют свои применения, но это ясный случай, когда композиция предпочтительнее.

  • 1
    Расширяя # 2, для Списка не имеет смысла иметь Список.
  • 0
    Во-первых, вопрос не имеет ничего общего с композицией против наследования. Ответ: OP не хочет реализовывать более конкретный вид списка, поэтому он не должен расширять List <>. Я запутался, потому что у вас очень высокий балл по стеку и вы должны это четко понимать, и люди доверяют тому, что вы говорите, так что к настоящему моменту минимум 55 человек, которые проголосовали и чья идея смущены, верят, что так или иначе можно строить система, но ясно, что это не так! нет # не-злоба
Показать ещё 6 комментариев
44

Как все отметили, команда игроков - это не список игроков. Эта ошибка совершается многими людьми во всем мире, возможно, на разных уровнях знаний. Часто проблема тонкая, а иногда и очень грубая, как в этом случае. Такие конструкции плохи, потому что они нарушают Принцип замены Лискова. В Интернете есть много хороших статей, объясняющих эту концепцию, например http://en.wikipedia.org/wiki/Liskov_substitution_principle

Таким образом, есть два правила, которые необходимо сохранить в отношениях между родителями и детьми между классами:

  • Ребенок не должен иметь никакой характеристики, которая меньше, чем полностью определяет родителя.
  • Родитель не должен обладать характеристикой в ​​дополнение к тому, что полностью определяет ребенка.

Другими словами, родитель является необходимым определением для ребенка, а ребенок является достаточным определением родителя.

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

  • Есть ли в футбольной команде список футболистов? (Все свойства списка относятся к команде в том же значении)
    • Является ли команда коллекцией однородных объектов? Да, команда - это коллекция игроков.
    • Является ли порядок включения игроков в описание состояния команды и обеспечивает ли команда сохранение последовательности без явного изменения? Нет, и Нет
    • Ожидается ли, что игроки будут включены/сброшены на основе их последовательной позиции в команде? Нет

Как вы видите, только первая характеристика списка применима к команде. Следовательно, команда не является списком. Список будет деталью реализации того, как вы управляете своей командой, поэтому его следует использовать только для хранения объектов игрока и манипулирования ими с помощью методов класса Team.

В этот момент я хотел бы отметить, что класс Team должен, на мой взгляд, даже не реализовываться с использованием List; он должен быть реализован с использованием структуры данных Set (например, HashSet), в большинстве случаев.

  • 7
    Хороший улов в Списке против Сета. Это кажется ошибкой, слишком часто совершаемой. Когда в коллекции может быть только один экземпляр элемента, предпочтительным кандидатом для реализации должен быть какой-то набор или словарь. Можно иметь команду с двумя игроками с одинаковыми именами. Нельзя включать одного игрока дважды одновременно.
  • 0
    +1 для некоторых хороших моментов, хотя более важно спросить «может ли структура данных разумно поддерживать все полезное (в идеале краткосрочное и долгосрочное)», а не «делает ли структура данных больше, чем полезна».
Показать ещё 7 комментариев
38

Что делать, если FootballTeam имеет команду резервов вместе с основной командой?

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

Как бы вы это моделировали?

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

Взаимосвязь, очевидно, имеет a и не является.

или RetiredPlayers?

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

Как правило, если вы когда-либо хотите наследовать коллекцию, назовите класс SomethingCollection.

Является ли ваш SomethingCollection семантически смысл? Выполняйте это только в том случае, если ваш тип представляет собой набор Something.

В случае FootballTeam это звучит неправильно. A Team больше, чем a Collection. A Team могут иметь тренеры, тренеры и т.д., Как указывали другие ответы.

FootballCollection звучит как сборник футбольных мячей или, может быть, коллекция футбольных принадлежностей. TeamCollection, сборник команд.

FootballPlayerCollection звучит как коллекция игроков, которая будет действительным именем для класса, который наследует от List<FootballPlayer>, если вы действительно этого хотели.

Действительно, List<FootballPlayer> - совершенно хороший тип. Может быть, IList<FootballPlayer>, если вы возвращаете его из метода.

В заключение

Спросите себя

  • Является ли X a Y? или имеет X a Y?

  • Знают ли мои имена классов, каковы они?

  • 0
    Если тип каждого игрока классифицирует его как принадлежащий либо DefensivePlayers , OffensivePlayers , либо OtherPlayers , может быть законно полезно иметь тип, который может использоваться кодом, который ожидает List<Player> но также включает члены DefensivePlayers , OffsensivePlayers , или SpecialPlayers типа IList<DefensivePlayer> , IList<OffensivePlayer> и IList<Player> . Можно использовать отдельный объект для кэширования отдельных списков, но инкапсуляция их в том же объекте, что и основной список, может показаться более чистой [использовать недействительность перечислителя списка ...
  • 0
    ... как признак того, что основной список изменился, а подсписки необходимо будет восстановить при следующем обращении].
Показать ещё 1 комментарий
30

Прежде всего, это связано с юзабилити. Если вы используете наследование, класс Team будет выставлять поведение (методы), которые предназначены исключительно для манипулирования объектами. Например, методы AsReadOnly() или CopyTo(obj) не имеют смысла для командного объекта. Вместо метода AddRange(items) вам, вероятно, понадобится более описательный метод AddPlayers(players).

Если вы хотите использовать LINQ, использование универсального интерфейса, такого как ICollection<T> или IEnumerable<T>, будет иметь больше смысла.

Как уже упоминалось, композиция - это правильный путь. Просто реализуйте список игроков как приватную переменную.

26

Дизайн > Реализация

Какие методы и свойства, которые вы раскрываете, являются дизайнерским решением. Какой базовый класс вы наследуете, это деталь реализации. Я чувствую, что стоит сделать шаг назад к первому.

Объект представляет собой набор данных и поведения.

Итак, ваши первые вопросы должны быть:

  • Какие данные содержит этот объект в модели, которую я создаю?
  • Какое поведение демонстрирует этот объект в этой модели?
  • Как это может измениться в будущем?

Имейте в виду, что наследование подразумевает отношение isa (a), тогда как композиция подразумевает отношение "имеет" (hasa). Выберите правильный вариант для вашей ситуации в своем представлении, учитывая, что ситуация может измениться по мере развития вашего приложения.

Рассмотрим мышление в интерфейсах, прежде чем думать о конкретных типах, так как некоторым людям проще разместить свой мозг в "режиме дизайна".

Это не то, что каждый делает сознательно на этом уровне в повседневной кодировке. Но если вы обдумываете такую ​​тему, вы стучите в проектные воды. Осознание этого может быть освобождением.

Рассмотрим спецификацию дизайна

Взгляните на список <T> и IList <T> на MSDN или Visual Studio. Посмотрите, какие методы и свойства они выставляют. У вас все эти методы похожи на то, что кто-то хотел бы сделать с FootballTeam на ваш взгляд?

Имеет ли смысл footballTeam.Reverse() для вас? Будет ли footballTeam.ConvertAll <TOutput> () выглядеть так, как вы хотите?

Это не трюк; ответ может быть действительно "да". Если вы реализуете/наследуете List <Player> или IList <Player> , вы застряли с ними; если этот идеал для вашей модели, сделайте это.

Если вы решите "да", это имеет смысл, и вы хотите, чтобы ваш объект обрабатывался как коллекция/список игроков (поведение), и поэтому вы хотите реализовать ICollection или IList, во что бы то ни стало. Номинально:

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

Если вы хотите, чтобы ваш объект содержал коллекцию/список игроков (данных), и поэтому вы хотите, чтобы коллекция или список являлись свойством или членом, обязательно сделайте это. Номинально:

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

Вы можете почувствовать, что хотите, чтобы люди могли перечислять только список игроков, а не считать их, добавлять к ним или удалять их. IEnumerable < Игрок > является вполне допустимым вариантом для рассмотрения.

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

Любой, кто пытается сказать вам, что один из них является категорически и окончательно неправильным, в каждом случае ошибочен. Любой, кто пытается сказать вам это категорически и окончательно right, в каждом случае ошибочен.

Переход к реализации

Как только вы решите данные и поведение, вы можете принять решение о реализации. Это включает в себя, какие конкретные классы вы будете зависеть от наследования или композиции.

Это не может быть большим шагом, и люди часто объединяют дизайн и реализацию, поскольку вполне возможно пропустить все это в вашей голове через секунду или два и начать печатать.

Эксперимент с мыслями

Искусственный пример: как отмечали другие, команда не всегда является "просто" коллекцией игроков. Поддерживаете ли вы коллекцию партитур для команды? Является ли команда взаимозаменяема с клубом, в вашей модели? Если это так, и если ваша команда является коллекцией игроков, возможно, она также является коллекцией персонала и/или коллекцией баллов. Затем вы получите:

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

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

Короткий ответ (слишком поздно)

Руководство по не наследованию от классов коллекции не является специфичным для С#, вы найдете его на многих языках программирования. Получается мудрость, а не закон. Одна из причин заключается в том, что на практике считается, что композиция часто выигрывает над наследованием с точки зрения понятности, реализуемости и ремонтопригодности. Это более распространено с объектами реального мира/домена, чтобы находить полезные и последовательные отношения "hasa", чем полезные и последовательные отношения "isa", если вы не глубоки в абстрактном, особенно с течением времени и точными данными и поведением объектов в коде изменения. Это не должно заставлять вас всегда исключать наследование из классов коллекции; но это может быть наводящим на размышления.

23

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

Это логически неверно:

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

и это верно:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}
  • 16
    Это не объясняет почему . ОП знает, что большинство других программистов так думают, он просто не понимает, почему это важно. Это только действительно предоставляет информацию уже в вопросе.
  • 1
    Это первое, о чем я тоже подумала, как его данные были просто неправильно смоделированы. Так что причина в том, что ваша модель данных неверна.
Показать ещё 3 комментария
20

Это зависит от контекста

Когда вы рассматриваете свою команду как список игроков, вы проецируете "идею" команды шара ноги на один аспект: вы уменьшаете "команду" до людей, которых вы видите на поле. Эта проекция верна только в определенном контексте. В другом контексте это может быть совершенно неправильно. Представьте, что вы хотите стать спонсором команды. Поэтому вам нужно поговорить с менеджерами команды. В этом контексте команда прогнозируется в списке своих менеджеров. И эти два списка обычно не перекрываются очень сильно. Другими контекстами являются текущие и прежние игроки и т.д.

Нечеткая семантика

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

Классы расширяемы

Когда вы используете класс с одним членом (например, IList activePlayers), вы можете использовать имя члена (и, кроме того, его комментарий), чтобы сделать контекст понятным. Когда есть дополнительные контексты, вы просто добавляете дополнительный элемент.

Классы сложнее

В некоторых случаях может возникнуть избыток для создания дополнительного класса. Каждое определение класса должно быть загружено через загрузчик классов и будет кэшироваться виртуальной машиной. Это требует производительности и памяти во время выполнения. Когда у вас есть очень специфический контекст, может быть, стоит подумать о футбольной команде как о списке игроков. Но в этом случае вы должны просто использовать IList , а не класс, полученный из него.

Заключение/соображения

Когда у вас есть очень специфический контекст, вполне нормально рассматривать команду как список игроков. Например, внутри метода вполне нормально писать

IList<Player> footballTeam = ...

При использовании F # может даже быть ОК, чтобы создать сокращение типа

type FootballTeam = IList<Player>

Но когда контекст шире или даже неясен, вы не должны этого делать. Это особенно важно, когда вы создаете новый класс, где неясно, в каком контексте он может использоваться в будущем. Предупреждающий знак - это когда вы начинаете добавлять дополнительные атрибуты в свой класс (имя команды, тренера и т.д.). Это явный признак того, что контекст, в котором будет использоваться класс, не является фиксированным и изменится в будущем. В этом случае вы не можете рассматривать команду как список игроков, но вы должны моделировать список игроков (в настоящее время активных, а не травмированных и т.д.) В качестве атрибута команды.

19

Позвольте мне переписать ваш вопрос. поэтому вы можете увидеть тему с другой точки зрения.

Когда мне нужно представлять футбольную команду, я понимаю, что это в основном имя. Например: "The Eagles"

string team = new string();

Затем я понял, что у команд также есть игроки.

Почему я не могу просто расширить тип строки, чтобы он также содержал список игроков?

Ваша точка входа в проблему произвольна. Попытайтесь подумать, что имеет команда (свойства), а не то, что она .

После этого вы можете увидеть, разделяет ли он свойства с другими классами. И подумайте о наследовании.

  • 1
    Это хороший момент - можно думать о команде как о названии. Однако, если мое приложение направлено на работу с действиями игроков, то это мышление становится менее очевидным. Во всяком случае, кажется, что проблема сводится к композиции против наследования в конце концов.
  • 5
    Это один из достойных способов взглянуть на это. В качестве примечания, пожалуйста, примите во внимание следующее: богатому человеку принадлежит несколько футбольных команд, он дает один другу в подарок, друг меняет название команды, увольняет тренера, и заменяет всех игроков. Команда друзей встречает мужскую команду на зеленом поле и, когда мужская команда проигрывает, он говорит: «Я не могу поверить, что ты побеждаешь меня с командой, которую я тебе дал!» мужчина прав? как бы ты проверил это?
15

Просто потому, что я думаю, что другие ответы в значительной степени уходят по тангенсу, является ли футбольная команда "is-a" List<FootballPlayer> или "has-a" List<FootballPlayer>, которая на самом деле не отвечает на этот вопрос, как написано.

ОП в основном просит уточнить руководящие принципы для наследования от List<T>:

В руководстве говорится, что вы не должны наследовать от List<T>. Почему бы и нет?

Потому что List<T> не имеет виртуальных методов. Это меньше проблем в вашем собственном коде, так как вы обычно можете отключить реализацию с относительно небольшой болью - но это может быть гораздо больше в публичном API.

Что такое публичный API и почему меня это беспокоит?

Открытый API - это интерфейс, который вы предоставляете сторонним программистам. Подумайте о каркасе. И напомним, что ссылки, на которые ссылаются, - это "Рекомендации по разработке .NET Framework", а не "Рекомендации по разработке приложений .NET". Есть разница, и, вообще говоря, публичный дизайн API намного более строгий.

Если мой текущий проект не имеет и вряд ли когда-либо будет иметь этот открытый API, могу ли я смело игнорировать это руководство? Если я наследую List и получается, мне нужен публичный 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 выполните то, что вы хотите.

14

Позволяет ли людям говорить

myTeam.subList(3, 5);

имеет смысл вообще? Если нет, то это не должно быть списком.

  • 2
    Это могло бы myTeam.subTeam(3, 5); если бы вы назвали это myTeam.subTeam(3, 5);
  • 2
    @SamLeach Если предположить, что это правда, то вам все равно понадобится композиция, а не наследование. Поскольку subList даже не вернет Team .
13

Это зависит от поведения вашего "командного" объекта. Если он ведет себя точно так же, как в коллекции, возможно, это будет нормально представлять его сначала с помощью простого списка. Тогда вы можете начать замечать, что вы продолжаете дублировать код, который повторяется в списке; на данный момент у вас есть возможность создать объект FootballTeam, который обертывает список игроков. Класс FootballTeam становится домом для всего кода, который повторяется в списке игроков.

Это делает мой код излишне подробным. Теперь я должен назвать my_team.Players.Count вместо my_team.Count. К счастью, с С# я могу определить индексаторов, чтобы сделать индексирование прозрачным, и переслать все методы внутреннего списка... Но это много кода! Что я получу за все, что работает?

Герметизация. Ваши клиенты не должны знать, что происходит внутри FootballTeam. Для всех ваших клиентов это может быть реализовано путем просмотра списка игроков в базе данных. Им не нужно знать, и это улучшает ваш дизайн.

Это просто не имеет никакого смысла. Футбольная команда не имеет "списка" игроков. Это список игроков. Вы не говорите: "Джон Макфуталлер присоединился к игрокам SomeTeam". Вы говорите: "Джон присоединился к SomeTeam". Вы не добавляете письмо к "строковым символам", вы добавляете письмо к строке. Вы не добавляете книгу в библиотечные книги, вы добавляете книгу в библиотеку.

Точно:) вы скажете footballTeam.Add(john), а не footballTeam.List.Add(john). Внутренний список не будет виден.

  • 0
    ОП хочет понять, как РЕАЛЬНОСТЬ МОДЕЛИ, но затем определяет футбольную команду как список футболистов (что концептуально неверно). Это проблема. Любой другой аргумент вводит его в заблуждение по моему мнению.
  • 2
    Я не согласен с тем, что список игроков неверен концептуально. Не обязательно. Как кто-то другой написал на этой странице: «Все модели ошибочны, но некоторые полезны»
Показать ещё 1 комментарий
11

Здесь есть много отличных ответов, но я хочу затронуть то, о чем я не упоминал: Объектно-ориентированный дизайн - это расширение прав и возможностей объектов.

Вы хотите инкапсулировать все свои правила, дополнительную работу и внутренние детали внутри соответствующего объекта. Таким образом, другим объектам, взаимодействующим с этим, не нужно беспокоиться обо всем этом. На самом деле, вы хотите сделать шаг дальше и активно предотвращать обход других объектов внутри этих объектов.

Когда вы наследуете от List, все остальные объекты могут видеть вас как Список. Они имеют прямой доступ к методам добавления и удаления игроков. И ты потеряешь контроль; например:

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


Дополнительные мысли об инкапсуляции... Вы подняли следующую озабоченность:

Это делает мой код излишне подробным. Теперь я должен вызывать my_team.Players.Count, а не только my_team.Count.

Вы правы, это было бы излишне подробным для всех клиентов, чтобы использовать вашу команду. Однако эта проблема очень мала по сравнению с тем фактом, что вы подвергли List Players всем и каждому, чтобы они могли играть с вашей командой без вашего согласия.

Вы продолжаете говорить:

Это просто не имеет никакого смысла. Футбольная команда не имеет "списка" игроков. Это список игроков. Вы не говорите: "Джон Макфуталлер присоединился к игрокам SomeTeam". Вы говорите: "Джон присоединился к SomeTeam".

Вы ошибаетесь в первом бит: оставьте слово "список", и на самом деле очевидно, что в команде есть игроки.
Тем не менее, вы ударили ноготь по голове вторым. Вы не хотите, чтобы клиенты вызывали ateam.Players.Add(...). Вы хотите, чтобы они вызывали ateam.AddPlayer(...). И ваша реализация (возможно, между прочим) вызовет Players.Add(...) внутренне.


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

11

Что такое правильный способ С# для представления структуры данных...

Помните: "Все модели ошибочны, но некоторые из них полезны". - Джордж Е. П. Бокс

Нет "правильного пути", только полезного.

Выберите тот, который вам полезен и/ваши пользователи. Это. Развивайте экономически, не переусердствуйте. Чем меньше кода вы пишете, тем меньше кода вам нужно будет отлаживать. (читайте следующие выпуски).

- Отредактировано

Мой лучший ответ будет... это зависит. Наследование из списка приведет к тому, что клиенты этого класса будут подвергнуты методам, которые могут не отображаться, прежде всего потому, что FootballTeam выглядит как бизнес-объект.

- Издание 2

Я искренне не помню, что я имел в виду в комментарии "не переучивайтесь". Хотя я считаю, что KISS мышление - хорошее руководство, я хочу подчеркнуть, что наследование бизнес-класса из списка создаст больше проблем, чем устраняет, из-за утечка абстракции.

С другой стороны, я считаю, что существует ограниченное число случаев, когда просто наследовать из Списка полезно. Как я писал в предыдущем издании, все зависит. Ответ на каждый случай сильно зависит от знания, опыта и личных предпочтений.

Спасибо @kai за то, что помогли мне более точно подумать о ответе.

  • 0
    Мой лучший ответ был бы ... это зависит. Наследование от List подвергло бы клиентов этого класса методам, которые могут быть недоступны, прежде всего потому, что FootballTeam выглядит как бизнес-объект. Я не знаю, должен ли я отредактировать этот ответ или опубликовать другой, есть идеи?
  • 2
    Бит о том, что « наследование от List будет предоставлять клиентам этого класса методы, которые могут быть недоступны », - именно то, что делает наследование от List абсолютным «нет-нет». После того, как они подвергаются воздействию, они постепенно подвергаются насилию и неправильному использованию с течением времени. Экономия времени благодаря «экономическому развитию» может легко привести к десятикратной экономии в будущем: отладке нарушений и, в конечном итоге, рефакторингу для исправления наследства. Принцип YAGNI также может рассматриваться как смысл: вам не нужны все эти методы из List , поэтому не подвергайте их.
Показать ещё 2 комментария
10

Это напоминает мне о том, что "есть" против "имеет" компромисс. Иногда это проще и имеет смысл наследовать непосредственно из суперкласса. В других случаях имеет смысл создать отдельный класс и включить класс, который вы унаследовали бы от переменной-члена. Вы по-прежнему можете получить доступ к функциям класса, но не привязаны к интерфейсу или каким-либо другим ограничениям, которые могут возникнуть в результате наследования класса.

Что вы делаете? Как и во многих вещах... это зависит от контекста. Руководство, которое я хотел бы использовать, состоит в том, что для наследования другого класса действительно должно быть отношение "есть". Так что, если вы пишете класс под названием BMW, он может унаследовать автомобиль, потому что BMW поистине автомобиль. Класс лошади может наследовать от класса млекопитающих, потому что лошадь на самом деле является млекопитающим в реальной жизни, и любая функциональность Млекопитающих должна относиться к Лошади. Но можете ли вы сказать, что команда - это список? Из того, что я могу сказать, это не похоже, что команда действительно "является" списком. Поэтому в этом случае у меня был бы List как переменная-член.

8

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

  • 0
    Оглядываясь назад, после объяснения @EricLippert и других, вы фактически дали отличный ответ на часть API моего вопроса - в дополнение к тому, что вы сказали, если я сделаю class FootballTeam : List<FootballPlayers> , пользователи моего класса Я смогу узнать, что я унаследовал от List<T> , используя отражение, увидев методы List<T> которые не имеют смысла для футбольной команды, и возможность использовать FootballTeam в List<T> , поэтому я будет раскрывать детали реализации для клиента (без необходимости).
8

Мой грязный секрет: мне все равно, что говорят люди, и я это делаю..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 не документировала, почему, то это, безусловно, "лучшая практика", которая приходит из ниоткуда.

  • 7
    Хотя важно иметь смелость и инициативу, чтобы оспаривать принятую мудрость, когда это уместно, я думаю, что разумно сначала понять, почему принятая мудрость стала принятой в первую очередь, прежде чем приступить к ее оспариванию. -1 потому что «Игнорируй это руководство!» не является хорошим ответом на вопрос «Почему существует это руководство?» Между прочим, сообщество не просто «обвиняло меня» в этом деле, но предоставило удовлетворительное объяснение, которое убедило меня.
  • 1
    На самом деле, когда никаких объяснений не дается, ваш единственный путь - раздвинуть границу и проверить, не боясь нарушения. Сейчас. Спросите себя, даже если List <T> не был хорошей идеей. Сколько боли было бы изменить наследование от List <T> на Collection <T>? Предположение: меньше времени, чем размещение на форуме, независимо от длины вашего кода с помощью инструментов рефакторинга. Теперь это хороший вопрос, но не практический.
Показать ещё 5 комментариев
5

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

Я часто вижу, что многие люди делают тонны дополнительной работы только из-за правила FxCop, так или иначе, что блог говорит, что это "плохая" практика. Много раз, это превращает код в дизайн палодовости паттерн. Как и в случае с множеством рекомендаций, относитесь к нему как к руководству, которое может иметь исключения.

3

Хотя у меня нет сложного сравнения, поскольку большинство из этих ответов, я хотел бы поделиться своим методом для обработки этой ситуации. Расширяя 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

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

В своей книге "Объектно-ориентированное программирование" он обсуждает реализацию системы графического интерфейса, в которой прямоугольные окна могут иметь дочерние окна. Он просто имеет Window наследует как от Rectangle, так и Tree<Window>, чтобы повторно использовать реализацию.

Однако С# не является Eiffel. Последний поддерживает множественное наследование и переименование функций. В С#, когда вы выполняете подкласс, вы наследуете интерфейс и реализацию. Вы можете переопределить реализацию, но соглашения о вызовах копируются непосредственно из суперкласса. Однако в Eiffel вы можете изменить имена общедоступных методов, поэтому вы можете переименовать Add и Remove в Hire и Fire в Team. Если экземпляр Team возвращается обратно к List<Player>, вызывающий использует Add и Remove, чтобы изменить его, но будут вызываться ваши виртуальные методы Hire и Fire.

  • 1
    Здесь проблема не в множественном наследовании, миксинах (и т. Д.) Или в том, как бороться с их отсутствием. На любом языке, даже на человеческом языке, команда - это не список людей, а составленный из списка людей. Это абстрактное понятие. Если Бертран Мейер или кто-либо еще управляет командой, используя подклассы List, то он поступает неправильно. Вы должны разделить список на подклассы, если вы хотите более конкретный вид списка. Надеюсь, вы согласны.
  • 1
    Это зависит от того, что наследство для вас. Само по себе это абстрактная операция, это не значит, что два класса находятся в «особом виде» отношений, даже если это основная интерпретация. Однако наследование может рассматриваться как механизм повторного использования реализации, если языковой дизайн его поддерживает, хотя в настоящее время композиция является предпочтительной альтернативой.
3

Если вашим пользователям класса необходимы все методы и свойства ** Список, вы должны извлечь из него свой класс. Если они им не нужны, приложите список и создайте обертки для методов, которые действительно нужны пользователям класса.

Это строгое правило, если вы пишете публичный API или любой другой код, который будет использоваться многими людьми. Вы можете игнорировать это правило, если у вас есть крошечное приложение и не более 2 разработчиков. Это сэкономит вам время.

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

0

Предпочитают интерфейсы над классами

Классы должны избегать получения классов и вместо этого применять минимальные интерфейсы.

Наследование прерывает инкапсуляцию

Вывод из классов прерывает инкапсуляцию:

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

Помимо прочего, это затрудняет реорганизацию вашего кода.

Классы являются частью реализации

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

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

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

При проектировании структуры данных, чем проще интерфейс, тем более гибким является код. Посмотрите, как мощный LINQ для демонстрации этого.

Как выбрать интерфейсы

Когда вы думаете "список", вы должны начать с того, чтобы сказать себе: "Мне нужно представлять коллекцию бейсболистов". Итак, скажем, вы решили смоделировать это с помощью класса. Сначала вы должны решить, какое минимальное количество интерфейсов, которое этот класс должен будет предъявить.

Некоторые вопросы, которые могут помочь в этом процессе:

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

Таким образом, вы не будете связывать другие части кода с деталями реализации вашей коллекции бейсболистов и сможете свободно изменять, как это реализовано, если вы уважаете интерфейс.

Используя этот подход, вы обнаружите, что код становится легче читать, рефакторировать и повторно использовать.

Заметки об исключении котельной

Внедрение интерфейсов в современной среде IDE должно быть простым. Щелкните правой кнопкой мыши и выберите "Использовать интерфейс". Затем переместите все реализации в класс-член, если вам нужно.

Тем не менее, если вы обнаружите, что вы пишете много шаблонов, это потенциально потому, что вы подвергаете больше функций, чем должно быть. Это то же самое, что вы не должны наследовать от класса.

Вы также можете разрабатывать меньшие интерфейсы, которые имеют смысл для вашего приложения, и, возможно, просто несколько вспомогательных функций расширения, чтобы сопоставить эти интерфейсы с любыми другими, которые вам нужны. Это подход, который я использовал в моем собственном интерфейсе IArray для библиотеки LinqArray.

0

Я думаю, что не согласен с вашим обобщением. Команда - это не просто коллекция игроков. У команды есть гораздо больше информации об этом - имя, эмблема, сборник управленческого/административного персонала, сбор коучинговой команды, а затем сбор игроков. Таким образом, ваш класс 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();
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню