How do you design a class for inheritance?
Asked Answered
A

5

12

I've heard it said that it is "difficult" to "design for inheritance", but I've never found that to be the case. Can anyone (and by anyone, I mean Jon Skeet) explain why this is allegedly difficult, what the pitfalls/obstacles/issues are, and why mere mortal programmers should not attempt it and just make their classes sealed to protect the innocent?

ok, i kid about the latter - but i am curious to see if anyone (including Jon) really has difficulties "designing for inheritance". I have really never considered it an issue but perhaps I am overlooking something that I take for granted - or screwing something up without realizing it!

EDIT: thanks for all the excellent answers so far. I believe the consensus is that for typical application classes (WinForm subclasses, one-off utility classes, et al) there is no need to consider reuse of any kind, much less reuse via inheritance, while for library classes it is critical to consider reuse via inheritance in the design.

I don't really think about, say, a WinForm class to implement a GUI dialog, as a class that someone might reuse - I sort of think of it as a one-off object. But technically it is a class and someone might inherit from it - but it's not very likely.

A lot of the larger-scale development that I've done has been class libraries for base libraries and frameworks, so designing for reuse by inheritance was critical - I just never considered it to be "difficult", it just was. ;-)

But I also never considered it in contrast to the 'one-off' classes for common application tasks like WinForms et al.

More tips and pitfalls of designing for inheritance are welcome, of course; I'll try to throw in some, too.

Algy answered 17/1, 2009 at 19:33 Comment(0)
W
19

Rather than rehashing it too much, I'll simply answer that you should have a look at the problems I had when subclassing java.util.Properties. I tend to avoid many of the problems of designing for inheritance by doing so as rarely as possible. Here are a few ideas of the problems though:

  • It's a pain (or potentially impossible) to implement equality properly, unless you limit it to "both objects must have exactly the same type". The difficulty comes with the symmetric requirement that a.Equals(b) implies b.Equals(a). If a and b are "a 10x10 square" and "a red 10x10 square" respectively, then a might well think that b is equal to it - and that may be all you actually want to test for, in many cases. But b knows that it has a colour and a doesn't...

  • Any time you call a virtual method in your implementation, you've got to document that relationship, and never, ever change it. The person deriving from the class needs to read that documentation, too - otherwise they might implement the call the other way round, quickly leading to a stack overflow of X calling Y which calls X which calls Y. This is my main problem - in many cases you have to document your implementation which leads to a lack of flexibility. It's mitigated significantly if you never call one virtual method from another, but you still have to document any other calls to virtual methods, even from non-virtual ones, and never change the implementation in that sense.

  • Thread safety is hard to achieve even without some unknown code being part of your execution-time class. You not only need to document the threading model of your class, but you may also have to expose locks (etc) to derived classes so they can be thread-safe in the same way.

  • Consider what sort of specialization is valid while keeping within the contract of the original base class. In what ways can methods be overridden such that a caller shouldn't need to know about the specialization, just that it works? java.util.Properties is a great example of bad specialization here - callers can't just treat it as a normal Hashtable, because it should only have strings for keys and values.

  • If a type is meant to be immutable, it shouldn't allow inheritance - because a subclass can easily be mutable. Oddities could then abound.

  • Should you implement some sort of cloning ability? If you don't, it may make it harder for subclasses to clone properly. Sometimes a memberwise clone is good enough, but other times it may not make sense.

  • If you don't provide any virtual members, you may well be reasonably safe - but at that point any subclasses are providing extra, different functionality rather than specializing the existing functionality. That's not necessarily a bad thing, but it doesn't feel like the original purpose of inheritance.

Many of these are much less of a problem for application builders than class library designers: if you know that you and your colleagues are going to be the only people ever to derive from your type, then you can get away with a lot less up-front design - you can fix the subclasses if you need to at a later date.

These are just off-the-cuff points, by the way. I may be able to come up with more if I think for long enough. Josh Bloch puts it all very well in Effective Java 2, btw.

Wateriness answered 17/1, 2009 at 20:3 Comment(19)
One which is called polymorphically. In most cases, this is the same as "can be overridden" - i.e. in Java terminology, non-final.Wateriness
All object methods are virtual in Java. It means that the target of a member function call is always determined by the object referenced rather than the type of the reference at the call site. This isn't always the case in C++ or C#Frog
In java, only non-private non-final methods are 'virtual'.Soliloquize
Non-private, non-final, non-static methods :)Wateriness
I don't really get the final exemption. If you have inheritance A,B,C,D and method foo is defined in B and overriden in C and made final in D, and you have D x = new B(); x.foo(); doesn't it call B's foo()? If so then foo is virtual by any reasonable definition. If not then Java is mad.Frog
No, it calls D's foo - but yes, it's virtual in terms of execution, but not virtual in terms of "overridable". See the second comment. It doesn't come up very often that virtual methods are overridden and made final, in my experience - but it's certainly a reasonable thing to do.Wateriness
thanks for the insights jon - i still don't think designing for inheritance is hard but maybe that's because its sort of second nature by now (I expect other developers to subclass my classes, that's kind of the point of using OOP). Perhaps you should stick with structured programming ;-)Algy
as for the equality issue, two objects are equal if and only if they are the same physical object (the same pointer/reference) or if they have the same type and property values; anything more than that is equivalence, not equality!Algy
seriously, i think the distinction between making classes for applications and making classes for class libraries is where the disconnect comes in. It is unlikely that MyWinForm1 will ever be subclassed, but something fundamental like Thread probably will (except that we CAN'T due to MS arrogance)Algy
LOL - java.util.Properties should have contained an instance of Hashtable rather than inherit from it, since they restricted the key and value types! This is not so much a problem with designing for inheritance, this is the direct result of a poor design choice by one of the Java library programmersAlgy
@Steven: I design for inheritance where I believe it's appropriate, but I don't think it's appropriate for a large proportion of my classes. The inappropriate inheritance of Properties from Hashtable was only part of the problem - but it shows what happens when people jump to inheritance by default.Wateriness
@[Jon Skeet]: I think I understand what you're getting at now; I never really even though about inheritance for typical application classes, that's not what they're for. Most of my large-scale class work has been for reusable libraries but I never found designing for reuse to be difficult...Algy
Don't forget that "reuse" != "inheritance". I "reuse" code in the framework all the time - List<T>, string, etc - in that I don't rewrite them myself. That doesn't mean I have to derive from those classes.Wateriness
@[Jon Skeet]: here I mean reuse via inheritance - i consciously design in 'flex points' where i think it is most likely that other developers will need/want to extend the base code, and i make everything virtual that can be so as not to restrict their options. To do otherwise undermines OO, IMHO ;-)Algy
And do you document every time you call any of these methods from within your code? To do otherwise undermines the ability of other developers to be confident in when and how their code will be called.Wateriness
@[Jon Skeet]: Yes! The flex-points are documented, as are the callers. For example, ChangeSomething will change some state and call OnSomethingChanged. The comments for ChangeSomething will note that it uses OnSomethingChanged, and the comments for OnSomethingChanged will note that it is intended for augmentation or override in subclasses.Algy
@Steven: So to me, that's reducing your ability to change the implementation later, as well as making the code more complicated and requiring more documentation. I find that the number of times I really want to use inheritance doesn't usually justify that pain. Sometimes there are good reasons, of course - but I find it's usually obvious when I will want to specialise a class with inheritance, and design for it in those particular cases and only those cases.Wateriness
@JonSkeet You wrote "The difficulty comes with the reflexive requirement that a.Equals(b) implies b.Equals(a). If a and b are "a 10x10 square" and "a red 10x10 square" respectively, then a might well think that b is equal to it - and that may be all you actually want to test for, in many cases. But b knows that it has a colour and a doesn't..." I think the correct terminology here is symmetric and not reflexive. PLease correct If I am wrong. Reflexive would be a.equals(a)==true.Homicide
@Geek: Yes, you're right - I meant symmetric there. Fixed, thanks.Wateriness
S
5

I think the main point is in the second-to-last paragraph in Jon's response: Most people think of "designing for inheritance" in terms of application design, i.e. they just want it to work, and if it doesn't, it can be fixed.

But it's a whole different beast to design an inheritable class as an API for other people to work with - because then the proteced fields and a whole lot of implementation details implicitly become part of the API contract which people using the API have to understand, and which cannot be changed without breaking the code using the API. If you make a mistake in the design or the implementation, chances are you can't fix it without breaking code that depends on it.

Shultz answered 17/1, 2009 at 20:14 Comment(3)
+1 yes BUT many times design errors and oversights in base classes can easily be corrected in subclasses UNLESS the designer is arrogant enough to assume that he got everything complete and right for all time, i.e. the base class is sealed or the main methods are non-virtual!Algy
The problem is that such "fixes" of the base class tend to be extremely fragile in regard to changes in that base class.Shultz
@Michael Borgwardt]: agreed, but that's far better than making such fixes impossible, which non-virtual methods and sealed classes tend to do!Algy
S
3

I don't think that I ever design a class for inheritance. I just write as little code as possible, and then when it's about time to copy and paste to create another class, I bump that one method into a super class (where it makes more sense than composition). So I let the Do Not Repeat Yourself (DRY) principle dictate my class design.

As for the consumers, it's up to them to act sanely. I try not to dictate how anyone can use my classes, although I do try to document how I intended them to be used.

Scherzando answered 19/1, 2009 at 2:10 Comment(2)
so for you inheritance is a byproduct of refactoring and not pre-planned; an interesting perspectiveAlgy
And that's what I do too.. Long live Resharper... :) Steve, if you haven't invested in ReSharper yet, you should... It's a fantastic tool.Devious
T
2

One more possible problem: When you call 'virtual' method from constructor in base class, and subclass overrides this method, subclass may use unitialized data.

Code is better:

class Base {
  Base() {
    method();
  }

  void method() {
    // subclass may override this method
  } 
}

class Sub extends Base {
  private Data data;

  Sub(Data value) {
    super();
    this.data = value;
  }

  @Override
  void method() {
    // prints null (!), when called from Base's constructor
    System.out.println(this.data);  
  }
}

This is because constructor of base class must always finish before constructor of subclass.

Summary: don't call overridable methods from constructor

Torrez answered 17/1, 2009 at 20:33 Comment(4)
+1 but i would strengthen your summaryto : Do not call ANY methods from the constructor. Have an explicit Initialize method for that sort of thing if you really need it.Algy
@Steven: That makes it really, really hard to make immutable types which do anything non-trivial. (Yes, you can have a static method which calls the constructor and then initialize, and make the type privately mutable, but I'd prefer "fully" immutable with readonly fields etc.)Wateriness
@[Jon Skeet]: you have to @ my entire name otherwise I don't see it in the responses tab. I am absolutely ROFLMAO that you don't know this already ;-) And i think immutable types are a rare exceptionAlgy
@[Steven A. Lowe]: I try to design most of data classes as immutable whenever possible. They are hardly a rare exception.Soliloquize
E
1

Sometimes when creating a baseclass I’m unsure about whether I should expose certain members to subclasses, e.g. objects I use for synchronization. I usually end up making them all private and change them to protected whenever the need arises.

Eviaevict answered 17/1, 2009 at 20:3 Comment(1)
synchronizing objects should be protected so that subclasses can (and should!) use them tooAlgy

© 2022 - 2024 — McMap. All rights reserved.