- Single inheritance means it's unnatural to inherit from the player list which is just one aspect of a team.
- It's a leak of implementation detail. Tomorrow you may want the team to contain a sorted collection, or a set, or a sorted set.
- It gives an unnatural syntax to team.count vs team.players.count. Again, composition wins in clarity
- You can still expose any part of a collection interface in your Team class, e.g. team.Add(player). The details of what this does should be hidden. Adding an interface like IEnumerable<Player> to the team gives a nice syntax like "foreach(Player p in team)" without breaking encapsulation.
- A list is a simple data structure, a team is not. Extending something to completely change what it is usually means you have an unnatural inheritance. A special list (sorted, cirular...) can extend list in a natural way, a business object should probably "have a" rather than "be a" list.
>> Adding an interface like IEnumerable<Player> to the team gives a nice syntax like "foreach(Player p in team)"
It would be interesting to allow _private_ inheritance, i.e. a class inherits methods from a superclass but they're all private and you need to add interfaces to expose them. That way you could inherit an implementation without leaking it into the public interface.
So the question basically drills down to why one should use composition instead of inheritance. Most OOP languages only allow inheriting from a single class, and that alone is reason enough to prefer composition in most cases. It's one of the reasons I'm particularly fond of traits/modules.
I don't really know of any OOP language that doesn't have some concept of multiple defined interfaces (eg HasPlayers, HasHomeCity), which is really what you'd want in this case rather than traits or modules.
Java just might become the Fortran of the 21st century -- embarrassingly out of date, but due to vast amounts of legacy code, impossible to get rid of.
Edit: I misread you and I interpreted that you meant Java was already a legacy and out of date language. So my answer is a little out of place, although I don't think there's any harm in keeping it here.
I disagree. I know that Java gets a lot of hate, here and in a lot of places, and I won't deny that a lot of it is indeed very well deserved, but it's far from being a "legacy" language. A quite large amount of software is being developed nowadays in Java, and in many cases, there are good reasons for that.
About the "embarrassingly out of date" part, I also think that's too exagerated. Java was never intended to be a cutting edge language, but a reliable set of well-proven techniques. Java doesn't want to be Haskell, doesn't want to be Ruby, and doesn't want to be Lisp; and explicitly so. Java has modest design goals; but it does fulfill them, which is why it's such a successful language even if it's not suitable for a lot of projects that you, myself or the rest of HN visitors may be interested in. But, on top of that, Java actually tries to incorporate functionality that has been successfully proven in different, and more innovative, lenguages (at an admittedly slow pace). Conservative, yes; out of date, I don't think so.
I'm not really fond of Java, and if I had to choose, I think there's probably no project for which I'd take it instead of some other language; however, its virtues are there.
> I know that Java gets a lot of hate, here and in a lot of places, and I won't deny that a lot of it is indeed very well deserved, but it's far from being a "legacy" language.
No, not yet, and I understand that you've edited your reply. I'm only saying that over time Java has more and more properties that are (a) out of date and (b) impractical to change because of legacy considerations.
The last time the Java developers made a major, code-invalidating change was when they decided that their event processing method needed to be replaced. It was a beneficial change, but a painful transition including a lot of "deprecated and to be removed" notifications and rewriting of existing code.
Since that time, no further major changes have been made that would invalidate existing code, which is as it should be, but this means Java inevitably becomes more and more out-of-date as time passes.
I emphasize that I write a lot of Java -- most of my best-known current programs are in Java, just because it will run on most platforms without any fuss.
> ... it's far from being a "legacy" language.
Like growing old, this doesn't happen all at once. :)
> Fortran is still evolving (albeit even more slowly than Java) and still used for new projects.
If it weren't for the fact that there is a huge trove of valuable legacy Fortran code, if Fortran were introduced today as a new language with no archive of existing Fortran programs, on its intrinsic merits it would be laughed off the stage. On that thought experiment's basis, Fortran continues to exist in order to support legacy code.
There are systems present in society of very high value that require Fortran to remain available, and some of them have famously undertaken to replace their Fortran code base at very high cost, and failed. So we have Fortran, and will continue to have Fortran, simply because of the very ambitious projects that were written in Fortran that we still need.
Yes, as somebody paying the bills mostly through Java, I can affirm that Java is the new COBOL.
While compensating for the excess of C++, too many good ideas went out the window, and we're left with something that extends COBOL by letting our copybooks, er, beans, have multiple instances, and letting out procedures, er, methods, have parameters, local variables and explicit return values.
There's a few other things Java does a bit better than COBOL, but it's used just like "COBOL with separate compilation" in standard "enterprise" practice.
Class invariant?
Closures?
Immutability?
Fuggedaboudit.
On the bright side, other languages are starting to highjack the JVM.
Composition making more sense is exactly why you'd use an interface, as the function inside the class depends on the internal implementation of how you'd store the players most efficiently while still giving a common method to access them. If you used a trait it would lock in the list implementation across all types that HadPlayers etc.
Yep. Team HAS-A list of players, not team IS-A list of players.
While we're at it, "List <? extends Player>" might be a better type for the roster, since it allows players to be subclassed and still fit into the list.
I don't think that's correct. He thinks that a team IS a group of players. Just like a list IS a group of items. team.players.count is like list.items.count
But that's a small point, in general I agree that he should not be inheriting from List here.
A sports team is a state-machine that evolves in time; a list, or indeed even a value object, doesn't itself express this.
That said, the pithy, smug, common and unenlightening answer is that inheritance breaks encapsulation. But really, that's a terrible reason not to extend a collection class if you really want to. Just remain aware of the trade-offs and do it.
I think this could be boiled down as: List<T> is a data structure. Anything that inherits from List<T> should be a data structure too. So LinkedList<T> could reasonably extend List<T>, but Team should not.
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.
Strictly speaking you're probably correct. But the problem is that OP is giving as an example a team, without strictly defining the expected behaviour of that class, so everyone starts to imagine his own set of behaviour ovverides, invariants, and so on, and in many cases they are right (the salient point of subclassing is method overriding).
The problem really starts when an invariant supported by a superclass is broken by it subclass. For instance, the idea that a player is unique in a team is obvious, so as said elsewhere in this thread, the Add method, whose invariant is to increment by one the size of the list (because duplicates are allowed, and pretty much no predicate other than equality is applicable to a generic type), would be broken by that Team subclass, and thus any function relying on that implicit behaviour (afaik, c# doesn't implement contracts) would break.
That Liksov substituton principle is tough indeed.
Do you want to be able to pass in Team for every function that currently asks for List<T>? Because that is what you are stating through the act of subclassing. At some point some part of your program may have a List<T>, but actually have a Team.
For starters, most teams I know don't have cloning technology, and thus can't have the same member of a team multiple times, but List<T> absolutely allows this. There are no restrictions on uniqueness in a List<T>. So then you may proceed to override Add() to first check if the team member is already in the team:
void Add(Player aPlayer)
{
if (Contains(aPlayer))
return;
base.Add(aPlayer);
}
Wonderful! But now you're a List<T> by name only, because you don't actually behave like a List. You don't follow the most basic postcondition of the API. Every single method that adds an item to a List<T> expects the List<T> to be 1 item larger afterward. So basically, you've created a List<T> that has undefined behavior with every existing function that ever calls Add(), because it doesn't work the way it was originally sold as working, so what is the point of going around calling yourself a List? You are a crash waiting to happen. You are not a List, you are a list manager.
Now is when someone will probably say, no no no. Subclassing isn't the problem. The problem is you didn't subclass the right thing. You should have subclassed Set<T>. Because teams don't have a natural ordering and contain unique players. But now you're just going to run into new problems. Like the fact that you have to check if a player is first in a sane state before adding them:
void Add(Player aPlayer)
{
if (aPlayer.team != null)
return; // need to be a free agent!
base.Add(aPlayer);
}
We can argue whether this should throw, or whether it should do it unconditionally, but the point is that the virtue of needing logic means its not this thing, its something managing this thing. Someone already wrote Set<T> and List<T> code that was almost certainly smarter than you. You wouldn't go poking around their original source to make your program a tiny bit easier to write at risk of ruining other things, so subclassing it is just a way of cheating and only poking around their source only for specific instances, but still causing all the same potential problems for those instances.
Remember, you should ask yourself "Can I guarantee every function that expects a T to behave exactly the same way when I pass a P instead (where P is a subclass of T)?". Almost always the answer is NO, since that's why you subclassed it to begin with! So you shouldn't do it. If the answer is YES, then you've probably only added an ivar or new methods in which case its equivalent to composition. The only thing subclassing gives you over composition is the ability to modify the behavior of existing methods, which breaks contracts with every other part of the code.
[n.b: I'm used to duck typed languages, where you can get away with far more shenanigans.]
> "Can I guarantee every function that expects T to behave exactly the same way when I pass P (where P is a subclass of T)". Almost always the answer is NO, that's why you subclassed it to begin with! So you shouldn't do it.
The entire point of subclassing is to be able to answer that question "no". By your logic, we should never subclass anything, because it will behave differently!
It seems to me you're not really trying to argue against subclassing here, but rather against making specific container types subclasses of generic containers types with extra logic (that breaks the container's documented guarantees) added, instead of containing the logic and the container as class attributes.
I'm reminded of the basic inheritance examples that show up all over, where Dog inherits from Animal: this makes sense because a Dog is a type of Animal. A Team is neither a type of List<T> nor a type of Set<T>, so it should not inherit from them. A team can, however, contain a roster represented by a List or Set alongside its other data.
> The entire point of subclassing is to be able to answer that question "no". By your logic, we should never subclass anything, because it will behave differently!
100% Correct, that is indeed my argument: the very purpose of subclassing is contradictory. Subclassing exists solely to mask the mutation of existing behavior, that is the only feature it adds to composition.
Composition is perfectly capable of ADDING and SUBTRACTING behavior, but to modify behavior it must do so transparently. You can't for example change the behavior of Add, you must call another method that internally calls Add on a child object and makes it explicit to the user code that the behavior is thus different.
There is no point in having a type system if the types become meaningless through subclass manipulation. I make a class T that has method M with postcondition P. People write functions that accept T with these expectations. When you create a subclass of T that modifies the behavior of M, the existing functions can no longer be trusted to behave correctly -- this is just a fact.
It's not at all just about "container classes". We see this broken behavior all the time in UI classes like UITableView extending UIView. UIView theoretically has a publicly mutable subviews array, but it stops being so by certain subclasses. So all of a sudden you have a UIView subclass that can't be passed in as an argument to functions with UIView parameters. This may be obvious to people familiar with the framework, but to novices, it now becomes meaningless to see the parameter UIView. Maybe it will do the right thing, maybe it won't, depends on the subclass. Now you have to go read the documentation (hopefully its documented).
The reason inheritance is taught with ridiculous examples like Dog and Animal (or even worse, Triangle and Shape) is precisely because it is so hard to find legitimate examples. You always end up just overriding things like Speak() (which does nothing in Animal and prints "woof" in Dog). "Good" (re: concrete) examples are usually really convoluted and hard to understand (or end up being basically just interfaces like NSResponder which is 99% empty methods).
Duck typing environments are saner in my opinion because they are really just interfaces. You are not making the strong statement "I am an X" (and thus creating the expectation of static behavior), but instead "I have loose behaviors A,B, and C" (in the same way as interfaces).
I came in expecting to fully agree with you here, but I think I see a flaw in your logic, unless I'm misunderstanding you.
> Duck typing environments are saner in my opinion because they are really just interfaces. You are not making the strong statement "I am an X" (and thus creating the expectation of static behavior), but instead "I have loose behaviors A,B, and C" (in the same way as interfaces).
I've taken this to mean that you should not subclass unless you can guarantee that the external behavior will be the same, i.e. you will always get the same output for the same input, although its performance characteristics may be different. This contrasts with implementing an interface, where you can return different results, as long as the results are valid. Just trying to paraphrase at this point -- tell me if I'm misrepresenting you.
Where I'm confused is trying to figure out why a violation of the former property would be an issue. If your code block instantiates a subclass, then it knows it isn't dealing with the base class, and it expects the behavior of the subclass -- no problem there. However, when you write a function that asks for the base class as a parameter, that strikes me as a law of demeter violation. The function should not care what the passed object does, as long as it does it legally.
At the risk of straw-manning, I assume your response would be that you should be asking for the interface in that case, not the class. And that is my point. You should never need a particular concrete data type, although you might ask for one to avoid the boilerplate of creating a thousand one-off interfaces.
So this gets back to what people are actually trying to do by subclassing List<T> or Set<T>. They want a good default implementation of IEnumerable<T>, IList<T> or ISet<T>, but with some of the behaviors switched out. They'll still fulfill the base interface requirements, and that's all any other function should ask for, so really, what's the problem?
I knew that sentence would require a much longer and deeper discussion (and I was hoping not to leave OOP to explain it). But in a nutshell, to me interfaces are meant precisely to represent "unknown behavior" and classes for "known behavior".
If we take a second to temporarily forget classes altogether and look at functional programming, you may have something like map(). map() takes a list (well known construct) and an iterator function (unknown outside behavior). The API itself makes it clear what can be changed and what can't. The scope of what is dynamic about the function and what isn't is immediately obvious from the types themselves, completely transparent.
To me, interfaces serve this same role in non-functional programming: they represent outside and dynamic behavior. Classes on the other hand represent known behavior. The problem with "good default implementations" is that they mix these two (opposite) concepts together, which regularly leads to completely "consistent" but absurd outcomes, that are usually attempted to be fixed with language-bandaids. Let me provide two examples.
The first is NSArray and NSMutableArray in Cocoa/Cocoa Touch (apologies for the use of these, it is my strongest background, and ironically enough, one of the closest "correct" subclassing examples in my opinion). NSMutableArray is a subclass of NSArray. This makes no sense. Granted, it makes complete sense from an "implementation" view, but when it comes to user code it makes the classes meaningless: if I make a method that takes an NSArray, the whole point is I'm saying its immutable. But I can pass in a mutable array and none of my expectations will be valid: the array could change right under me. And yet the compiler will be perfectly happy because it is a correct statement, NSMutableArrays are NSArrays. This should theoretically be an edge case: using a subclass to provide the OPPOSITE behavior of the original superclass, rendering the type-system absurd, and yet it is a widely used construct. I am thus forced to defensively copy the immutable array because it may very well be a mutable -- something completely "consistent" in this world. This to me shows this fundamental confusion of interface vs. classes. To get back to what you were saying, the problem with subclassing is precisely that it is incredibly difficult to state what is done "legally".
If you have a pure interface, with no existing "default" behavior, its just like a lambda: anything goes. If you have a class, its static. If you have both (subclassing), it gets incredibly tricky and hard to predict the interaction. The entire API of the class becomes surface area for mutation.
Now let me give you the second example: UIView. UIView has a -subviews method. Why can't I override -subviews to return a static list of views if I never want it to change (thus rendering addSubview: and removeSubview null)? The documentation does NOT list this method as non-overridable (And many people in fact override it to do perfectly legitimate things btw). And if I do override it, I still abide by all the postconditions of the method as defined by the API. And in fact, OTHER parts of the framework totally allow (And sometimes encourage!) this type of "override instead of setting"-style programming. As a novice using the framework, its a completely logical expectation that this should work. So why does UIKit completely break down?
Because the real postconditions are incredibly more complicated than just "Return your subviews". You have part of the code written as if nothing will ever change (internally UIView relies on the _subviews ivar), but another that acts like an interface advertising that it might change (possible UIView subclasses). And half the time, the changes don't upset any delicate balance. But other times, it does. Things like a final keyword wouldn't fix the above problem because again, sometimes its legitimate to override. You can absolutely start writing your code in such a way where things "should" work no matter what method a subclass overrides, but that is very hard to get right (and very hard to test), whereas you get this for free by just forcing everyone to use the external interface! (This is what is meant by breaking encapsulation: you work so hard to provide a sane API, that then anyone can break by changing in otherwise completely legitimate ways). The real problem is that you are saying two contradictory things. What you actually want is to do ONE of the following:
1. Force everyone to use addSubview: and removeSubview: since its a concrete class in a hypothetical non-subclassable Objective-C world. No absurd results because, again, everyone is using the API you worked so hard on and properly tested (i.e. there is no way to futz around with -subviews) OR
2. Have -subviews be a method of some sort of Drawable interface, such that every other piece of code is now known to work with the expectation that it calls that instead of some internal ivar or something, allowing all the shenanigans you want.
Hopefully that makes my views a little clearer, but I'm super tired since its 2AM here so perhaps some of this was more verbose or not as clear as it could be.
I think you've got the right idea, but you've overgeneralized the critique of subclassing by suggesting that it is contradictory.
Here's where you're 100% correct: a subclass should not change the specified behavior of its superclass. To do so would be a violation of the contract established by its superclass's API.
This does not, however, apply to all use of subclassing. Rather, a superclass's API can be specified in such a way that subclasses may change behavior without breaking the contract. Dog and Animal fit this mold -- the Animal API may specify that Speak() will "cause the animal to speak", intentionally leaving this behavior up to subclassers. This is core to how subtyping-based polymorphism works -- the superclass underconstrains its API in order to allow subclasses to later tighten those constraints while still providing valid implementations of the superclass's contract.
Good API design requires that interface behaviors are properly constrained. Overconstrain them, and you box yourself (and consumers of the API) out of useful abstractions. Underconstrain them and it becomes impossible to work with the APIs because consumers of the API can't count on subclasses to have the proper behavior.
It's very, very easy to accidentally underconstrain an API, as you aptly pointed out. UIView does this with its subviews array. By exposing this mutable list, it broadcasts to its consumers a huge set of APIs that make promises they can't keep. If more thought had been put into how UIViews should be subclassed, they might have further constrained it so that (for example) a UIView is responsible for maintaining and keeping internally consistent its set of subviews. This is a promise that subclasses can keep (probably by not publicly exposing subviews at all, since making it public is likely to, again, result in underconstrained APIs and more API promises it can't keep), and assumptions that are useful for consumers of the API (e.g. "I don't have to worry about managing subviews that aren't explicitly called out in the contract" or "a layout container can't accidentally mess up its contents' subviews").
FWIW, XAML's UI framework does draw these types of distinctions, making _most_ of its subtyping sane (take a look at UIElement, FrameworkElement, Control, ContentControl, and Panel). Every now and then you can find examples where the abstractions are leaky due to under/overconstraining the APIs (e.g. the subtle differences between UserControl and ContentControl... UserControl looks like you could use it instead of ContentControl in most cases because the Content property is publicly settable and probably shouldn't be -- an API decision that every UserControl subclass has to live with and usually chooses to ignore).
Anyway, my point is this: subclassing is _not_, at its core, contradictory. But API designers _must_ design classes to be subclassed. In order to properly answer the "is X a Y?" question, subclassers must also be able to assert that X's implementation meets all of the constraints of Y's API.
Someone already wrote Set<T> and List<T> code that was
almost certainly smarter than you.
I am so very tired of this weak argument in favor of adopting conventions.
It doesn't persuade me at all. In fact, I see through it for what it really is: An invitation to adhere to someone else's rules.
Whether or not I choose to adhere to List's contract of always incrementing size() by one, after calling add(Object) has nothing to do with whomsoever might be "smarter." It's purely a decision in terms of adhering to the potentially popular expectations of other programmers, or defying them. This is a social requirement, not a technical one.
In other words, you'll catch flak from other developers, who might try to use your code, even if it functions perfectly, for the circumstances it was developed to be used under.
This type of clash of unnecessary, pedantic demands is idomatic to the culture of Java (particularly enterprise Java), much in the way that bizarre, unexpected parsing results are idomatic to PHP, or the hellish nightmare of cross-browser support is idiomatic to JavaScript.
Honestly, if you're not designing an API, and it's not intended for broad use, this sort of rote adherence to established convention can be safely ignored. If your code won't have a very large audience, is well-isolated, tests well, and re-use is not anticipated, who cares?
And listen, don't get me wrong. I'm glad that Java's language specifications are pedantic, strongly typed and designed and written according to properly conceived, well-thought-out contracts. This is the primary benefit of Java over other languages.
On the other hand, if I decide to write a class, while subclassing interfaces from util, while completely ripping the guts out of every method I override, and violating every normal convention the world at large might expect from such a subclass, it has nothing to do with whether the existing interfaces were written by people "smarter" than me. Maybe I just like the semantics of the names of the methods. Now if you'll please excuse me, I have to apply another coat of wax to my ego.
Your whole argument seems to base around List<T> allowing for duplicate entries while a Team would not. I don't think this is a very good argument, because you could just extend from Set<T> instead which ensures uniqueness - but would still be the wrong choice.
This is a joke right? I literally cover this word for word. I think you read the first paragraph of my argument, stopped reading, rolled your eyes, and decided you understood my "whole argument".
OO is a very hard art. It took me a good 10 years to be effective at it. But, I learned in the 90s, when there was a lot less knowledge available. These days, it should be a lot easier - so many good resources.
Anyway, I would love to try other ways of doing it. I see functional programming often sold as the better alternative. However, I've never seen a large code base written entirely in functional language.
Anyone have an examples of large functional programs code bases that are good to learn from?
As another mentioned, GHC is a good example. Another one that might be a bit better to look at is xmonad. It's only a little over 1kLOC and is a fully functional window manager.
EDIT: I see another poster arrived at a conclusion of 200KLoC from the same source. I'm guessing the discrepancy is about the ~40KLoC which comprise the runtime system.
Much like most things in life, it takes practice and intelligence.
The negative aspects are primarily because people haven't practiced enough or are being idiots.
TDD is absolutely fine with OO. We have no problems with ~2 million lines of C#. It works pretty well to be honest. If you're finding something hard to test, it's usually broken in some way and usually because concerns aren't separated properly.
"The negative aspects are primarily because people haven't practiced enough or are being idiots."
Oh, I forgot, OO is perfect. Way to be condescending.
"TDD is absolutely fine with OO"
I don't mean OO in general (and it makes it easier in some aspects) I mean inheritance.
The more your inheritance is deep the more the classes down the hierarchy (does that inherit the most) will have non trivial testing. Because of all the dependencies/initializations/creation of the parents. Oh I am sure I'm doing wrong and I have to use a VisitatorSingletonFactory I wouldn't have had a problem and "I'm being an idiot", of course.
And surely, the problem is not usually within the codebase, but in external libraries/utilities, those that you need to interface in order for your system to do things.
I think it's fundamentally wrong to model a team as a list of players. It isn't a list of players, and having a data structure representing the players as a property of the team seems more meaningful in terms of communicating the idea of team members. Even then, the most useful data structure to represent the member players would probably be a set, in my opinion. Then, the whole history of the team could be expressed in terms of simple operations on that set.
> Microsoft did not seal it because sometimes you might want to create a class that has a friendlier name
I expect multiple-language support may have been a stumbling block here but compromising your API (instead of sealing the class, document it as "you shouldn't subclass this") for something that could be done with type aliasing seems like a really bad trade-off. Is the C# using keyword a johnny come lately, or would it not support this functionality?
I'd rather link to the first answer (http://stackoverflow.com/a/21694054/247441). The “update” section looks like a nice example of how to step back and think about data modeling of this sort.
> a football team seems to me to be well modeled as a collection of historical facts such as when a player was recruited, injured, retired, etc
Would be interesting to hear what would be the functional stand on this.
In a way a team may not need the players. Maybe teams are just identifiers, as are players, and we have a list of event relating players to teams. Then a function can get these events and compute the current list of players in a given team.
One way of doing it would be to use a record type, one part of which would be the list of players. This is analogous to the composition strategy in OO. You could then manipulate values of that record type using lenses.
The important question is what you actually want to do with this data. The thing that has always seemed crazy to me about OO is how you can have page-long discussions like this one without even talking about what the data is being used for. The notion instead is to "model the real world", which sends you off on a wild goose chase that is almost always unnecessary.
Classic functional programming doesn't cover oop: there's no subclassing, thus no issue with using a list to keep your players. Furthermore, pure functional code doesn't mutate, so invariants can't really be broken, unless you meddle with the internal representation, in a way which would confuse the existing codebase to handle lists.
So if you pick list as the implementation detail of your team type, then it will essentially be a list, and may be passed as such to list handling functions without issues. It'll be up to your code to handle the aftermath if your invariants were somehow broken in the process, which would be the proper thing to do. From the outside though, just like for any type, the implementation details shouldn't be exposed. For weakly typed languages, it should be documented to not rely on it.
Edit: I was just handling the original question from an functional stand point. Of course nbouscal solution is much more usable and robust. What would probably happen, if you follow an agile methodology, or some similarly incremental process of development, is that you'd probably start with a list, and as new functionalities would be added, the type would evolve to a tuple or a record including the list. Any function would then have to be updated accordingly. But at least the team type should be made abstract somehow, to hide implementation details.
IMO the easiest way to poke a hole in the argument is to say: ok, now what happens when you want to add the coach of the team? and the owner? Adding them to the Players list would be problematic considering they're not really players. If you tried to rig it anyway, your Count method would return incorrectly, and you'd have to override it to exclude coaches. While we're at it, where are you storing the team's name? A variable name alone isn't going to help you display that info to the end-user.
Granted, none of this matters if the OP's problem domain stays where they have it now. OOP, however, shines when your domain is subject to change and developers are willing to sacrifice terseness for flexibility.
In part, this is a confusion caused by semantics and inherent difficulties with any kind of abstraction. The notion of a 'team' in real life is a very flexible one, and as soon as we try to represent it using a concrete data structure, we're bound to run into problems. The questioner shouldn't be blamed for thinking a team and a list of players are one and the same - there are many real life examples in which the two are used interchangeably. If anything, this highlights the challenge of OOP: real life objects and their relationships are just really difficult to model in a deterministic way.
class Team {
List<Player> players = new List<Player>();
public void AddPlayer(Player player) {
// rule checks here
players.add(player);
}
public IEnumerable<Player> Players {
get { return players; }
}
}
Covers SOLID, DDD, the lot.
One should never inherit the list. Encapsulate perhaps but the list is there as a fundamental structure and to inherit from it would break the encapsulation of the domain model you are constructing and increase efferent coupling.
Or use a better language ;-). E.g. in Scala you can use the LinearSeq trait, Ruby the Enumerable mixin to get all the benefits without the drawbacks of Java style subtyping.
TLDR: Class hierarchies should only be used for truly hierarchical objects, which is rare. Composition is almost always the correct structure. When in doubt, use composition. You can almost never go wrong this way.
Another way to look at it, by subclassing List, any method that takes a list can also take a Team. That makes no sense. A method that takes a List should take a list of team members.
I don't think every player is a single property of team. However list of players is. It is more logic to create an object for team which has list of players, name, etc. Although your example doesn't explain your problem, there may be some cases that we need to inherit from List.
- Single inheritance means it's unnatural to inherit from the player list which is just one aspect of a team.
- It's a leak of implementation detail. Tomorrow you may want the team to contain a sorted collection, or a set, or a sorted set.
- It gives an unnatural syntax to team.count vs team.players.count. Again, composition wins in clarity
- You can still expose any part of a collection interface in your Team class, e.g. team.Add(player). The details of what this does should be hidden. Adding an interface like IEnumerable<Player> to the team gives a nice syntax like "foreach(Player p in team)" without breaking encapsulation.
- A list is a simple data structure, a team is not. Extending something to completely change what it is usually means you have an unnatural inheritance. A special list (sorted, cirular...) can extend list in a natural way, a business object should probably "have a" rather than "be a" list.