为什么不从List <T>inheritance?
在计划我的计划时,我经常从一系列的想法开始:
足球队只是一个足球运动员的名单。 因此,我应该用下面的代表:
var football_team = new List<FootballPlayer>();
该列表的sorting表示球员列入名单的顺序。
但是后来我意识到球队也有其他属性,除了球员名单之外,还必须logging下来。 例如,本赛季得分总数,当前预算,统一颜色,代表球队名称的string
等。
那么我想:
好吧,一个足球队就像一个球员列表,另外,它有一个名字(一个
string
)和一个总分数(一个int
)。 .NET不提供存储足球队的课程,所以我将自己创build课程。 最相似和最相关的现有结构是List<FootballPlayer>
,所以我将inheritance它:class FootballTeam : List<FootballPlayer> { public string TeamName; public int RunningTotal }
但事实certificate, 一个指引说,你不应该从List<T>
inheritance 。 这个指导方针在两方面都让我感到十分困惑。
为什么不?
显然List
是以某种方式优化性能的 。 怎么会这样? 如果我扩展List
会导致哪些性能问题? 什么会打破?
我看到的另一个原因是List
是由Microsoft提供的,我无法控制它,所以在暴露“public API”之后我不能再改变它 。 但我很难理解这一点。 什么是公共API,我为什么要关心? 如果我目前的项目没有,也不可能拥有这个公共API,我可以放心地忽略这个指南吗? 如果我从List
inheritance,事实certificate我需要一个公共API,那么我会遇到什么困难?
为什么它甚至重要? 列表是一个列表。 什么可能改变? 我可能想要改变什么?
最后,如果微软不想让我inheritanceList
,他们为什么不把这个类sealed
?
还有什么我应该使用?
显然,对于自定义集合,Microsoft提供了一个应该扩展而不是List
的Collection
类。 但是这个类是非常AddRange
,并没有很多有用的东西, 比如AddRange
。 jvitor83的答案提供了该特定方法的性能原理,但是如何缓慢的AddRange
不比没有AddRange
?
从Collection
inheritance比从List
inheritance更有效,我看不出有什么好处。 毫无疑问,微软不会让我无缘无故做额外的工作,所以我不禁感觉到我在某种程度上误解了某些东西,并且inheritanceCollection
实际上并不是我的问题的正确解决scheme。
我看到了诸如实施IList
build议。 就是不行。 这是几十行样板代码,没有任何收获。
最后,有人build议把这个List
包装在一些东西里面:
class FootballTeam { public List<FootballPlayer> Players; }
这有两个问题:
-
它使我的代码不必要地冗长。 我现在必须调用
my_team.Players.Count
而不是只是my_team.Count
。 值得庆幸的是,用C#我可以定义索引器来使索引透明,并且转发内部List
所有方法…但是这是很多代码! 我能从中得到什么? -
这只是简单的没有任何意义。 足球队没有“拥有”球员名单。 这是玩家的名单。 你不会说“John McFootballerjoin了SomeTeam的球员”。 你说“Johnjoin了SomeTeam”。 您不要将字母添加到“string的字符”,您将字母添加到string。 你不会把一本书添加到图书馆的书籍中,而是将一本书添加到一个图书馆。
我意识到“底层”所发生的事情可以说是“把Yjoin到Y的内部清单”,但这似乎是一种非常直观的思考世界的方式。
我的问题(总结)
什么是正确的C#代表一个数据结构的方式,“逻辑地”(也就是说,“对于人类的头脑”)只是一个有几个花里胡哨的things
的list
?
从List<T>
inheritance是不可接受的? 什么时候可以接受? 为什么/为什么不呢? 在决定是否inheritanceList<T>
时,程序员必须考虑什么?
这里有一些很好的答案。 我会添加以下几点。
什么是正确的C#代表一个数据结构的方式,“逻辑地”(也就是说,“对于人类的头脑”)只是一个有几个花里胡哨的事情的列表?
请任何十位熟悉足球存在的非计算机程序员填写空白处:
A football team is a particular kind of _____
有没有人说过“有一些花里胡哨的足球运动员名单”,还是都说“运动队”,“俱乐部”或“组织”? 你的观点是,足球队是一种特殊的球员列表,只有你的人类思想和人类的思想。
List<T>
是一个机制 。 足球队是一个业务对象 – 也就是说,代表某个概念的对象在程序的业务领域 。 不要混合这些! 足球队是一种团队, 它有一个名册,一个名册是一个球员名单 。 名册不是一种特殊的球员名单 。 名单是一个球员名单。 所以创build一个名为Roster
的属性是一个List<Player>
。 当你在这里的时候,把它ReadOnlyList<Player>
,除非你相信每个了解足球队的人都会从队名中删除球员。
从
List<T>
inheritance是不可接受的?
谁不能接受? 我? 没有。
什么时候可以接受?
当你构build一个扩展List<T>
机制的机制时 。
在决定是否inheritance
List<T>
时,程序员必须考虑什么?
我是build立一个机制还是业务对象 ?
但是这是很多代码! 我能从中得到什么?
你花了更多的时间来input你的问题,它会让你为List<T>
的相关成员编写50次转发方法。 你显然不怕冗长,我们在这里只是谈论很less的代码。 这是几分钟的工作。
UPDATE
我给了它更多的想法,还有一个原因,就是没有把足球队build模成一个球员列表。 事实上,把一个足球队build模成一个球员名单也许是个不错的主意。 一个球队的问题是拥有一个球员名单,你所得到的只是一个球队的快照 。 我不知道你们这个class的商业案例是什么,但是如果我有一个代表足球队的课,我想问一个问题,比如“2003年到2013年有多less海鹰队员因伤缺席比赛? 或者“之前效力于另外一支球队的丹佛球员的年增长率是多less?” 或者“ 猪头们今年一路走来了吗? ”
也就是说,一个足球队在我看来被很好地塑造成了一个历史事实的集合,比如当一个球员被招募,受伤,退役等等。很明显,当前的球员名单是一个重要的事实,中心,但可能还有其他有趣的事情,你想要做这个对象,需要更多的历史观点。
最后,有人build议把这个List包装在一些东西里面:
这是正确的方法。 “不必要的罗嗦”是看这个不好的方法。 当你写my_team.Players.Count
时,它有明确的含义。 你想要统计玩家。
my_team.Count
没有任何意义。 算什么?
一个团队不是一个清单 – 不仅仅是一个球员列表。 一个球队拥有球员,所以球员应该是其中的一员(一名成员)。
如果你真的担心它过于冗长,你可以随时暴露团队的属性:
public int PlayerCount { get { return Players.Count; } }
成为:
my_team.PlayerCount
这现在有意义,并遵守得墨忒耳定律 。
您还应该考虑遵守复合重用原则 。 通过从List<T>
inheritance,你说的是一个团队是一个玩家列表,并暴露出不必要的方法。 这是不正确的 – 正如你所说,一个团队不仅仅是一个球员名单:它有一个名字,经理,董事会成员,教练,医务人员,工资帽等等。通过让你的团队类别包含一个球员名单,你“一个球队有一个球员名单”,但它也可以有其他的东西。
哇,你的文章有很多问题和观点。 你从微软获得的大部分推理都是正确的。 我们从List<T>
-
List<T>
被高度优化。 主要用法是作为对象的私有成员使用。 - 微软并没有密封它,因为有时你可能想创build一个有更友好名称的
class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
:class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
。 现在就像做var list = new MyList<int, string>();
。 - CA1002:不要暴露通用名单 :基本上,即使你打算使用这个应用程序作为唯一的开发者,它是值得开发与良好的编码实践,使他们灌输到你和第二性质。 如果您需要任何消费者拥有索引列表,您仍然可以将该列表作为
IList<T>
公开。 这让我们稍后更改类中的实现。 - 微软使
Collection<T>
非常通用,因为它是一个通用的概念…名字说明了一切; 它只是一个集合。 有更精确的版本,如SortedCollection<T>
,ObservableCollection<T>
,ReadOnlyCollection<T>
等,每个实现IList<T>
而不是List<T>
。 -
Collection<T>
允许成员(即添加,删除等)由于它们是虚拟的而被覆盖。List<T>
不。 - 你的问题的最后一部分是现货。 一个足球队不仅仅是一个球员列表,所以它应该是一个包含这个球员列表的类。 构思与inheritance 。 一个足球队有一个球员名单(名册),这不是一个球员名单。
如果我正在编写这个代码,这个类可能看起来像这样:
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. }
class FootballTeam : List<FootballPlayer> { public string TeamName; public int RunningTotal }
以前的代码意思是:一群街上踢足球的人,他们碰巧有一个名字。 就像是:
无论如何,这个代码(从我的答案)
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. }
意思是说:这是一个拥有pipe理,球员,pipe理员等的足球队员。例如:
这是你的逻辑如何在图片中呈现…
这是组合与inheritance的典型例子。
在这个特定的情况下:
球队是否有增加行为的球员名单?
要么
球队是否属于自己的对象,恰好包含了球员列表。
通过扩展List,您可以通过多种方式来限制自己:
-
您不能限制访问权限(例如,阻止人员更改名册)。 你得到所有的List方法,无论你是否需要/需要它们。
-
如果您想要列出其他内容,会发生什么情况。 例如,球队有教练,经理,球迷,装备等。其中一些很可能是自己的名单。
-
你限制你的selectinheritance。 例如,您可能需要创build一个通用的Team对象,然后再从中inheritanceBaseballTeam,FootballTeam等。 为了inheritanceList,你需要inheritanceTeam的inheritance关系,但是这意味着所有不同types的团队都被迫拥有与该名单相同的实现。
构图 – 包括一个对象,在对象中给予你想要的行为。
inheritance – 您的对象成为具有所需行为的对象的实例。
两者都有其用途,但这是一个明确的情况下,组成是可取的。
正如大家所指出的,一个球员队伍不是一个球员名单。 这个错误是由许多人在任何地方,也许在不同层面的专业知识。 这个问题通常是微妙的,有时甚至是非常严重的。 这样的devise是不好的,因为这违反了里斯科替代原则 。 互联网有许多好的文章解释这个概念,例如http://en.wikipedia.org/wiki/Liskov_substitution_principle
总之,在父母/子女关系中要保留两个规则:
- 孩子应该不需要比完全定义父母的特征更less的特征。
- 除了完全定义孩子的内容之外,父母不应该要求任何特征。
换句话说,父母是孩子的必要定义,孩子是父母的充分定义。
这里有一个方法来思考一个解决scheme,并应用上述原则,应该有助于避免这样的错误。 我们应该通过validation父类的所有操作在结构和语义上对派生类是否有效来testing一个假设。
- 足球队是足球队的名单吗? (一个列表中的所有属性是否适用于同样意义的团队)
- 一个团队是一个同质实体的集合吗? 是的,团队是一个玩家的集合
- 球员是否包含描述球队状态的顺序?球队是否确保顺序被保留,除非有明确的改变? 不,不,
- 球员是否期望根据他们在球队中的排名位置被包括/放弃? 没有
正如你看到的,只有列表的第一个特征适用于一个团队。 因此,一个团队不是一个名单。 列表将是你如何pipe理你的团队的实现细节,所以它应该只用于存储玩家对象,并用Team类的方法来操作。
在这一点上,我想说的是,在我看来,Team类应该甚至不能用List来实现; 它应该在大多数情况下使用Set数据结构(例如HashSet)来实现。
如果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; } }
作为一个经验法则,如果您想要从集合inheritance,请命名类SomethingCollection
。
你的SomethingCollection
语义上是有意义的吗? 只有在您的types是 “ Something
的集合时才这样做。
在FootballTeam
的情况下,这听起来不对。 一个Team
不仅仅是一个Collection
。 其他答案指出,一支Team
可以有教练,训练师等。
FootballCollection
听起来像是一个FootballCollection
的集合,或者是一个足球用具的集合。 TeamCollection
,一组团队。
FootballPlayerCollection
听起来像是一个玩家的集合,如果你真的想这么做的话,它将是一个从List<FootballPlayer>
inheritance的类的有效名称。
真正的List<FootballPlayer>
是一个非常好的types来处理。 也许IList<FootballPlayer>
如果你从一个方法返回它。
综上所述
问你自己
-
X
是Y
? 或者有X
Y
? -
我的class级名称是什么意思?
首先,它与可用性有关。 如果使用inheritance,则Team
类将公开纯粹为对象操作devise的行为(方法)。 例如, AsReadOnly()
或CopyTo(obj)
方法对团队对象没有意义。 而不是AddRange(items)
方法,你可能需要一个更具描述性的AddPlayers(players)
方法。
如果你想使用LINQ,实现一个通用的接口,如ICollection<T>
或IEnumerable<T>
会更有意义。
如前所述,构图是正确的方法。 只需将玩家列表作为私有variables。
devise>实施
你公开哪些方法和属性是一个devise决定。 你inheritance的基类是一个实现细节。 我觉得值得回到前者。
一个对象是数据和行为的集合。
所以你的第一个问题应该是:
- 这个对象在我创build的模型中包含哪些数据?
- 这个对象在这个模型中performance出什么样的行为?
- 未来如何改变?
请记住,inheritance意味着一个“isa”(是)关系,而构成意味着一个“有一个”(hasa)的关系。 根据您的观点select适合您情况的产品,并记住随着您的应用程序的发展可能会出现的情况。
在用具体types思考之前先考虑界面思维,因为有些人发现这样更容易把他们的大脑置于“devise模式”。
这不是每个人都在日常编码中有意识地在这个层面上做的事情。 但是,如果你正在研究这样的话题,那么你正在devise水域中行走。 意识到它可以解放。
考虑devise细节
在MSDN或Visual Studio上查看List <T>和IList <T>。 看看他们揭露了什么方法和属性。 在你看来,这些方法是否看起来像是某个人想要对橄榄球队做的事情?
footballTeam.Reverse()对你有意义吗? footballTeam.ConvertAll <TOutput>()看起来像你想要的东西吗?
This isn't a trick question; the answer might genuinely be "yes". If you implement/inherit List<Player> or IList<Player>, you're stuck with them; if that's ideal for your model, do it.
If you decide yes, that makes sense, and you want your object to be treatable as a collection/list of players (behaviour), and you therefore want to implement ICollection or IList, by all means do so. Notionally:
class FootballTeam : ... ICollection<Player> { ... }
If you want your object to contain a collection/list of players (data), and you therefore want the collection or list to be a property or member, by all means do so. Notionally:
class FootballTeam ... { public ICollection<Player> Players { get { ... } } }
You might feel that you want people to be able to only enumerate the set of players, rather than count them, add to them or remove them. IEnumerable<Player> is a perfectly valid option to consider.
You might feel that none of these interfaces are useful in your model at all. This is less likely (IEnumerable<T> is useful in many situations) but it's still possible.
Anyone who attempts to tell you that one of these it is categorically and definitively wrong in every case is misguided. Anyone who attempts to tell you it is categorically and definitively right in every case is misguided.
Move on to Implementation
Once you've decided on data and behaviour, you can make a decision about implementation. This includes which concrete classes you depend on via inheritance or composition.
This may not be a big step, and people often conflate design and implementation since it's quite possible to run through it all in your head in a second or two and start typing away.
A Thought Experiment
An artificial example: as others have mentioned, a team is not always "just" a collection of players. Do you maintain a collection of match scores for the team? Is the team interchangable with the club, in your model? If so, and if your team isa collection of players, perhaps it also isa collection of staff and/or a collection of scores. Then you end up with:
class FootballTeam : ... ICollection<Player>, ICollection<StaffMember>, ICollection<Score> { .... }
Design notwithstanding, at this point in C# you won't be able to implement all of these by inheriting from List<T> anyway, since C# "only" supports single inheritance. (If you've tried this malarky in C++, you may consider this a Good Thing.) Implementing one collection via inheritance and one via composition is likely to feel dirty. And properties such as Count become confusing to users unless you implement ILIst<Player>.Count and IList<StaffMember>.Count etc. explicitly, and then they're just painful rather than confusing. You can see where this is going; gut feeling whilst thinking down this avenue may well tell you it feels wrong to head in this direction (and rightly or wrongly, your colleagues might also if you implemented it this way!)
The Short Answer (Too Late)
The guideline about not inheriting from collection classes isn't C# specific, you'll find it in many programming languages. It is received wisdom not a law. One reason is that in practice composition is considered to often win out over inheritance in terms of comprehensibility, implementability and maintainability. It's more common with real world / domain objects to find useful and consistent "hasa" relationships than useful and consistent "isa" relationships unless you're deep in the abstract, most especially as time passes and the precise data and behaviour of objects in code changes. This shouldn't cause you to always rule out inheriting from collection classes; but it may be suggestive.
A football team is not a list of football players. A football team is composed of a list of football players!
This is logically wrong:
class FootballTeam : List<FootballPlayer> { public string TeamName; public int RunningTotal }
and this is correct:
class FootballTeam { public List<FootballPlayer> players public string TeamName; public int RunningTotal }
It depends on the context
When you consider your team as a list of players, you are projecting the "idea" of a foot ball team down to one aspect: You reduce the "team" to the people you see on the field. This projection is only correct in a certain context. In a different context, this might be completely wrong. Imagine you want to become a sponsor of the team. So you have to talk to the managers of the team. In this context the team is projected to the list of its managers. And these two lists usually don't overlap very much. Other contexts are the current versus the former players, etc.
Unclear semantics
So the problem with considering a team as a list of its players is that its semantic depends on the context and that it cannot be extended when the context changes. Additionally it is hard to express, which context you are using.
Classes are extensible
When you using a class with only one member (eg IList activePlayers
), you can use the name of the member (and additionally its comment) to make the context clear. When there are additional contexts, you just add an additional member.
Classes are more complex
In some cases it might be overkill to create a extra class. Each class definition must be loaded through the classloader and will be cached by the virtual machine. This costs you runtime performance and memory. When you have a very specific context it might be OK to consider a football team as a list of players. But in this case, you should really just use a IList
, not a class derived from it.
Conclusion / Considerations
When you have a very specific context, it is OK to consider a team as a list of players. For example inside a method it is completely OK to write
IList<Player> footballTeam = ...
When using F#, it can even be OK to create a type abbreviation
type FootballTeam = IList<Player>
But when the context is broader or even unclear, you should not do this. This is especially the case, when you create a new class, where it is not clear in which context it may be used in the future. A warning sign is when you start to add additional attributes to your class (name of the team, coach, etc.). This is a clear sign that the context where the class will be used is not fixed and will change in the future. In this case you cannot consider the team as a list of players, but you should model the list of the (currently active, not injured, etc.) players as an attribute of the team.
Let me rewrite your question. so you might see the subject from a different perspective.
When I need to represent a football team, I understand that it is basically a name. Like: "The Eagles"
string team = new string();
Then later I realized teams also have players.
Why can't I just extend the string type so that it also holds a list of players?
Your point of entry into the problem is arbitrary. Try to think what does a team have (properties), not what it is .
After you do that, you could see if it shares properties with other classes. And think about inheritance.
Does allowing people to say
myTeam.subList(3, 5);
make any sense at all? If not then it shouldn't be a List.
It depends on the behaviour of your "team" object. If it behaves just like a collection, it might be OK to represent it first with a plain List. Then you might start to notice that you keep duplicating code that iterates on the list; at this point you have the option of creating a FootballTeam object that wraps the list of players. The FootballTeam class becomes the home for all the code that iterates on the list of players.
It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count. Thankfully, with C# I can define indexers to make indexing transparent, and forward all the methods of the internal List… But that's a lot of code! What do I get for all that work?
封装。 Your clients need not know what goes on inside of FootballTeam. For all your clients know, it might be implemented by looking the list of players up in a database. They don't need to know, and this improves your design.
It just plain doesn't make any sense. A football team doesn't "have" a list of players. It is the list of players. You don't say "John McFootballer has joined SomeTeam's players". You say "John has joined SomeTeam". You don't add a letter to "a string's characters", you add a letter to a string. You don't add a book to a library's books, you add a book to a library.
Exactly 🙂 you will say footballTeam.Add(john), not footballTeam.List.Add(john). The internal list will not be visible.
Just because I think the other answers pretty much go off on a tangent of whether a football team "is-a" List<FootballPlayer>
or "has-a" List<FootballPlayer>
, which really doesn't answer this question as written.
The OP chiefly asks for clarification on guidelines for inheriting from List<T>
:
A guideline says that you shouldn't inherit from
List<T>
. 为什么不?
Because List<T>
has no virtual methods. This is less of a problem in your own code, since you can usually switch out the implementation with relatively little pain – but can be a much bigger deal in a public API.
What is a public API and why should I care?
A public API is an interface you expose to 3rd party programmers. Think framework code. And recall that the guidelines being referenced are the ".NET Framework Design Guidelines" and not the ".NET Application Design Guidelines". There is a difference, and – generally speaking – public API design is a lot more strict.
If my current project does not and is not likely to ever have this public API, can I safely ignore this guideline? If I do inherit from List and it turns out I need a public API, what difficulties will I have?
Pretty much, yeah. You may want to consider the rationale behind it to see if it applies to your situation anyway, but if you're not building a public API then you don't particularly need to worry about API concerns like versioning (of which, this is a subset).
If you add a public API in the future, you will either need to abstract out your API from your implementation (by not exposing your List<T>
directly) or violate the guidelines with the possible future pain that entails.
Why does it even matter? A list is a list. What could possibly change? What could I possibly want to change?
Depends on the context, but since we're using FootballTeam
as an example – imagine that you can't add a FootballPlayer
if it would cause the team to go over the salary cap. A possible way of adding that would be something like:
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!"); } } }
Ah…but you can't override Add
because it's not virtual
(for performance reasons).
If you're in an application (which, basically, means that you and all of your callers are compiled together) then you can now change to using IList<T>
and fix up any compile errors:
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 */ }
but, if you've publically exposed to a 3rd party you just made a breaking change that will cause compile and/or runtime errors.
TL;DR – the guidelines are for public APIs. For private APIs, do what you want.
There are a lot excellent answers here, but I want to touch on something I didn't see mentioned: Object oriented design is about empowering objects .
You want to encapsulate all your rules, additional work and internal details inside an appropriate object. In this way other objects interacting with this one don't have to worry about it all. In fact, you want to go a step further and actively prevent other objects from bypassing these internals.
When you inherit from List
, all other objects can see you as a List. They have direct access to the methods for adding and removing players. And you'll have lost your control; 例如:
Suppose you want to differentiate when a player leaves by knowing whether they retired, resigned or were fired. You could implement a RemovePlayer
method that takes an appropriate input enum. However, by inheriting from List
, you would be unable to prevent direct access to Remove
, RemoveAll
and even Clear
. As a result, you've actually disempowered your FootballTeam
class.
Additional thoughts on encapsulation… You raised the following concern:
It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count.
You're correct, that would be needlessly verbose for all clients to use you team. However, that problem is very small in comparison to the fact that you've exposed List Players
to all and sundry so they can fiddle with your team without your consent.
You go on to say:
It just plain doesn't make any sense. A football team doesn't "have" a list of players. It is the list of players. You don't say "John McFootballer has joined SomeTeam's players". You say "John has joined SomeTeam".
You're wrong about the first bit: Drop the word 'list', and it's actually obvious that a team does have players.
However, you hit the nail on the head with the second. You don't want clients calling ateam.Players.Add(...)
. You do want them calling ateam.AddPlayer(...)
. And your implemention would (possibly amongst other things) call Players.Add(...)
internally.
Hopefully you can see how important encapsulation is to the objective of empowering your objects. You want to allow each class to do its job well without fear of interference from other objects.
What is the correct C# way of representing a data structure…
Remeber, "All models are wrong, but some are useful." – George EP Box
There is no a "correct way", only a useful one.
Choose one that is useful to you and/your users. 而已。 Develop economically, don't over-engineer. The less code you write, the less code you will need to debug. (read the following editions).
— Edited
My best answer would be… it depends. Inheriting from a List would expose the clients of this class to methods that may be should not be exposed, primarily because FootballTeam looks like a business entity.
— Edition 2
I sincerely don't remember to what I was referring on the “don't over-engineer” comment. While I believe the KISS mindset is a good guide, I want to emphasize that inheriting a business class from List would create more problems than it resolves, due abstraction leakage .
On the other hand, I believe there are a limited number of cases where simply to inherit from List is useful. As I wrote in the previous edition, it depends. The answer to each case is heavily influenced by both knowledge, experience and personal preferences.
Thanks to @kai for helping me to think more precisely about the answer.
This reminds me of the "Is a" versus "has a" tradeoff. Sometimes it is easier and makesmore sense to inherit directly from a super class. Other times it makes more sense to create a standalone class and include the class you would have inherited from as a member variable. You can still access the functionality of the class but are not bound to the interface or any other constraints that might come from inheriting from the class.
Which do you do? As with a lot of things…it depends on the context. The guide I would use is that in order to inherit from another class there truly should be an "is a" relationship. So if you a writing a class called BMW, it could inherit from Car because a BMW truly is a car. A Horse class can inherit from the Mammal class because a horse actually is a mammal in real life and any Mammal functionality should be relevant to Horse. But can you say that a team is a list? From what I can tell, it does not seem like a Team really "is a" List. So in this case, I would have a List as a member variable.
My dirty secret: I don't care what people say, and I do it. .NET Framework is spread with "XxxxCollection" (UIElementCollection for top of my head example).
So what stops me saying:
team.Players.ByName("Nicolas")
When I find it better than
team.ByName("Nicolas")
Moreover, my PlayerCollection might be used by other class, like "Club" without any code duplication.
club.Players.ByName("Nicolas")
Best practices of yesterday, might not be the one of tomorrow. There is no reason behind most best practices, most are only wide agreement among the community. Instead of asking the community if it will blame you when you do that ask yourself, what is more readable and maintainable?
team.Players.ByName("Nicolas")
要么
team.ByName("Nicolas")
真。 Do you have any doubt? Now maybe you need to play with other technical constraints that prevent you to use List<T> in your real use case. But don't add a constraint that should not exist. If Microsoft did not document the why, then it is surely a "best practice" coming from nowhere.
What the guidelines say is that the public API should not reveal the internal design decision of whether you are using a list, a set, a dictionary, a tree or whatever. A "team" is not necessarily a list. You may implement it as a list but users of your public API should use you class on a need to know basis. This allows you to change your decision and use a different data structure without affecting the public interface.
When they say List<T>
is "optimized" I think they want to mean that it doesn't have features like virtual methods which are bit more expensive. So the problem is that once you expose List<T>
in your public API , you loose ability to enforce business rules or customize its functionality later. But if you are using this inherited class as internal within your project (as opposed to potentially exposed to thousands of your customers/partners/other teams as API) then it may be OK if it saves your time and it is the functionality you want to duplicate. The advantage of inheriting from List<T>
is that you eliminate lot of dumb wrapper code that is just never going to be customized in foreseeable future. Also if you want your class to explicitly have exact same semantics as List<T>
for the life of your APIs then also it may be OK.
I often see lot of people doing tons of extra work just because of FxCop rule says so or someone's blog says it's a "bad" practice. Many times, this turns code in to design pattern palooza weirdness. As with lot of guideline, treat it as guideline that can have exceptions.
If your class users need all the methods and properties** List has, you should derive your class from it. If they don't need them, enclose the List and make wrappers for methods your class users actually need.
This is a strict rule, if you write a public API , or any other code that will be used by many people. You may ignore this rule if you have a tiny app and no more than 2 developers. This will save you some time.
For tiny apps, you may also consider choosing another, less strict language. Ruby, JavaScript – anything that allows you to write less code.
I just wanted to add that Bertrand Meyer, the inventor of Eiffel and design by contract, would have Team
inherit from List<Player>
without so much as batting an eyelid.
In his book, Object-Oriented Software Construction , he discusses the implementation of a GUI system where rectangular windows can have child windows. He simply has Window
inherit from both Rectangle
and Tree<Window>
to reuse the implementation.
However, C# is not Eiffel. The latter supports multiple inheritance and renaming of features . In C#, when you subclass, you inherit both the interface and the implemenation. You can override the implementation, but the calling conventions are copied directly from the superclass. In Eiffel, however, you can modify the names of the public methods, so you can rename Add
and Remove
to Hire
and Fire
in your Team
. If an instance of Team
is upcast back to List<Player>
, the caller will use Add
and Remove
to modify it, but your virtual methods Hire
and Fire
will be called.
I think I don't agree with your generalization. A team isn't just a collection of players. A team has so much more information about it – name, emblem, collection of management/admin staff, collection of coaching crew, then collection of players. So properly, your FootballTeam class should have 3 collections and not itself be a collection; if it is to properly model the real world.
You could consider a PlayerCollection class which like the Specialized StringCollection offers some other facilities – like validation and checks before objects are added to or removed from the internal store.
Perhaps, the notion of a PlayerCollection betters suits your preferred approach?
public class PlayerCollection : Collection<Player> { }
And then the FootballTeam can look like this:
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(); }