Hacker News new | past | comments | ask | show | jobs | submit login

In my personal experience, I've never found a true IS-A relationship in my code. I've found lots of interfaces, though.

A lot of the time, you'll think you're defining a superclass when you start writing things like "Square IS-A Shape; Rectangle IS-A Shape". But Shape will turn out to not define any common behavior, just a category you want to restrict your inputs and outputs to; and you'll want to be able to assert that anything is, in addition to whatever else it is, a Shape. So shape is an interface.

A lot of the time, you'll think you're defining a superclass when you start writing things like "User IS-A Person; ProjectOwner IS-A User". But it'll turn out that you want to keep People around even when they stop being Users, and to keep Users around even when they stop being ProjectOwners. So you'll rearrange things and find that you're now asserting "User IS-A Role; ProjectOwner IS-A Role; Person HAS-MANY Roles." And Role turns out to be, again, an interface.

The only example I can think of that does fit single-inheritance is when modelling objects that directly express a genealogy. For example, GithubForkOfProjectA IS-A ProjectA, or Ubuntu IS-A Debian. But these aren't typically things you'd express as types; they're just instances (of, respectively, GithubProject and LinuxDistribution.) Each instance HAS-A clonal-parent-instance.

I guess there's one possibly-practical use of inheritance which I've nearly implemented myself: if you force your database schema migrations to always follow the Open-Closed Principle, and you want to migrate the rows of a table as you encounter them to avoid taking the DB offline, then you could have two separate models for a Foo table, FooV2 and FooV3, where "FooV3 IS-A FooV2". Each row has a version column, and is materialized as the model corresponding to that version. Your code that expected FooV2s would then be satisfied if it was passed a FooV3.

Does anyone actually do this, though? I don't just mean "row-by-row migrations", I mean the "with two models, one version inheriting from the other" part. And, if so, what do you do when you make a change to a model that doesn't obey the Open-Closed Principle: where FooV3s break the FooV2 contract?




The one case where languages without traditional OO inheritance are painful is where you want to do "like this other thing, except for this one particular thing that it does differently". And sure, maybe that's always bad design - but it comes up a lot in real-world business requirements. For all the people saying "you should use alternatives to traditional OO" I've never seen an actual example of how to do this better - you can patch those instances at runtime (urgh), you can create an object that implements the same interface and delegates to an instance of the base type (much less readable in every language I've seen, and effectively reimplementing inheritance without the syntactic sugar).

I think the right solution is simply to have firmer constraints about the relationships between parent and child classes - just like decoupling a class's implementation from its interface, it should be possible to separate out the interface it exposes to child classes as well. The one library/framework I've seen that does this really effectively is Wicket - it makes extensive use of final classes, final methods, and access modifiers to ensure that when you need to extend Wicket classes you can do so via a well-defined interface that won't break when you upgrade your Wicket dependency. It works astonishingly well.


You're correct in that special-case "like this other thing, except for this one particular thing that it does differently" objects happen all the time due to business rules.

But the Decorator pattern is not "inheritance without the syntactic sugar." Decorated objects, unlike subclass instances, are allowed to break the contract of the object they decorate: they don't have to claim to obey any of its interfaces, they can hide its methods, they can answer with arbitrarily different types to the same messages, etc.

If a language made defining decorators simple, I think it'd remove a lot of what people think of as the use-case for inheritance. (I mean, you aren't supposed to use inheritance for Decorator-pattern use-cases--it will likely break further and further as you try--but people will keep trying as long as the first steps are so much easier than the alternative.)


> If a language made defining decorators simple, I think it'd remove a lot of what people think of as the use-case for inheritance. (I mean, you aren't supposed to use inheritance for Decorator-pattern use-cases--it will likely break further and further as you try--but people will keep trying as long as the first steps are so much easier than the alternative.)

I actually agree with this, and I'd be interested to hear of language efforts in that direction. But until there's this easy way to do decorators, just telling people "don't use inheritance" isn't going to work.


> you can create an object that implements the same interface and delegates to an instance of the base type (much less readable in every language I've seen, and effectively reimplementing inheritance without the syntactic sugar).

In case you're hankering for a language that makes delegated composition easier, Go's "embedded fields" are sugar for exactly that: http://golang.org/ref/spec#Struct_types


Class inheritance should be about the data not the behavior. If you're looking for "IS-A" inheritance, you should look at: what parameters does the class require on construction. If your implementation isn't fundamentally based around the same construction parameters, then it shouldn't be a derived class.

For example: HTTPResponseHandler IS-A TCPResponseHandler because its primary role requires a TCP socket on construction. The only inherited methods should be management of the data from construction.

Problems with this only arise when people use subclass to gain the interface but not the data. You should never need to do this and it's a problem with using classes instead of interfaces for your parameter declarations – it's not a problem with class inheritance.


Except that interface are not supposed to have behaviors.

Shape can lay itself out with regards to its enclosing rectangle for example.

From my experience, using IS-A only works on simple concepts, and almost never on more than 2 layers. But it's still useful.

I suppose that if languages had the same "automatic function redirection" to sub-components, such as Golang, then people would use composition a lot more, though. That's actually the thing that seduces me the most about this language.


> Shape can lay itself out with regards to its enclosing rectangle for example.

How so? I mean, sure, Shape can declare, say

    bool is_inside(Rectangle enclosing_rect)
...but how would Shape know how to calculate that? It'd be an abstract method. All of Shape's methods would be abstract methods. Thus, a Shape is an interface: a contract an object makes with the system to say that it has a given API.


But this is an excellent example of where Shape can have concrete code in it though. is_inside is true iff the shape's bounds are inside the rectangle. So assuming you have a means to test rectangle bounds:

    bool is_inside(Rectangle enclosing_rect) {
       return enclosing_rect.contains(this.get_bounds());
    }
Now you don't need to implement cookie cutter is_inside methods everywhere, but rather the simpler get_bounds(). In fact, you can add is_inside after all your shapes are implemented and it will just work, as long as they have get_bounds.

Maybe you want to add some sanity checking to make sure bounds never have negative width, so you implement get_bounds() in Shape, and implement get_left(), get_right(), get_top(), get_bottom() in the child classes. Now you don't have to add cookie-cutter assert(left < right) stuff everywhere, just in one place.

Other similar ideas: if you write an "intersects(Point)" method on each shape, then you can pull a concrete implementation of sampling-based area approximation up to the base Shape class, and leave analytical area calculation to the children. This is useful if you are working with distance-field, parametric or noisy shapes, for example.


The template method pattern is a great example of how you might define some behavior for is_inside, without completely defining it.




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

Search: