Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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.



"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)?"

For those who don't know, that's the Liskov Substitution Principle [1]

[1] http://en.wikipedia.org/wiki/Liskov_substitution_principle


[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".




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: