DekGenius.com
[ Team LiB ] Previous Section Next Section

6.4 Pitfalls of Inheritance

Inheritance is a troublesome facility; although it is undoubtedly powerful, it can cause a great many problems. To steer clear of these, it is important to understand what it is about inheritance that makes it so easy to go wrong. We have already discussed simple misuse, caused by the failure to understand that inheritance defines an "is a" relationship—if your derived class cannot be substituted for its base class, you will run into difficulties. But even when this design rule has not been broken, inheritance is still potentially dangerous. The fundamental problem with inheritance is that it tends to require the derived class to have an exceptionally close relationship with the base class.

In the autocompletion example above, we needed to know more than is healthy about the way that the TextBox class works. First of all, we needed a pretty detailed understanding simply to determine which methods to override. To implement these overrides correctly, we also needed considerable lateral knowledge of the inner workings of the control, to anticipate issues such as the deletion problem. Inheritance requires knowledge of how the base class works both in breadth and in depth.

This tight coupling between the derived and base classes introduces another problem. If the base class evolves over time, it could easily break classes that derive from it. In the AutoTextBox class, we elected to autocomplete the control's text in our override of OnTextChanged and not in OnKeyPress, because we observed that the former appears to work where the latter appears not to work in this particular case. But this is not a clearly documented feature of the TextBox class; what if the next version behaves slightly differently?

There is a reasonable argument that says authors of base classes simply shouldn't make changes like this—after all, if they made such changes to the public API, it would break all client code, not just deriving classes.[8] But there are two reasons why derived classes are more vulnerable to changes in the base class than normal clients. First, derived classes get to see a larger API—they can access all the protected members, where normal clients only see the public ones. Second, and more importantly, derived classes usually modify the behavior of the base class in some way; modifications are likely to be much more sensitive to implementation details than straightforward usage will be.

[8] In this particular case, the OnTextChanged and OnKeyPress methods are directly associated with the public TextChanged and KeyPress events. Changes to the nature of these protected methods would imply a corresponding change in public behavior, which is one reason we can be reasonably confident that this particular feature of TextBox won't change in future versions of the framework.

This problem is often described as the "fragile base class" issue: you can't touch the base class without breaking something. The main technology offered by .NET to mitigate this is its support for side-by-side deployment—multiple versions of a component can be installed on one machine, so that each application can use the exact version of the component it was built against. But even that can go wrong: the base class author might sneakily issue an update without changing the component's version number, or he might ship a publisher policy with an update, declaring the new version to be fully backwards compatible with the old one—either of these could potentially upset derived classes. And even if this doesn't happen, you might run into trouble when you next rebuild your application—if you have updated the components on your system, you will probably be building against the newer versions.

Given the tight coupling between the base and the derived class, it should come as no surprise to discover that inheritance is often at its most successful when both the base and derived class are written by the same author or team. When derived classes start to do things that the base class author didn't originally anticipate, there are far fewer problems if the same developer wrote both. That developer can then modify the base class to meet the derived class's needs.

The classes you should be most suspicious of are those that have never been used as a base class before. It is extremely difficult to anticipate what requirements derived classes will place on your code. When the base and derived classes are under common ownership, most base classes evolve considerably the first few times they are derived from. One reason the Control class makes such successful use of inheritance is that the Windows Forms team wrote such a large number of classes that derive from it before it was released. You can be sure that Control changed considerably as these derived classes were developed. This work hardening of its design means that it is now pretty mature, and tends to work well as a base class most of the time.

There is a classic observation that despite the best design intentions in the world, no code is reusable until it has been used in at least two different scenarios, preferably many more. This is especially true for as tricky a relationship as inheritance. Despite this, there are some steps you can take to reduce the likelihood of running into certain kinds of problems.

6.4.1 Design Heuristics for Inheritance

The most important fact to bear in mind when considering the use of inheritance is that it never works by accident. There is a widely held but ultimately misguided belief that the support for inheritance built into the CLR means that we will be able to inherit from any control and expect everything to work perfectly. The reality is that inheritance only works when the designer of the base class considered possible inheritance scenarios. It only works really well when the base class has been revised to incorporate the lessons learned from attempts to use inheritance. Deriving from a control that was designed without inheritance in mind will at best lead to a severe case of fragile base class syndrome, but will more likely lead to slightly flaky control syndrome.

So how should you go about designing your classes if you want them to be suitable as base classes? The obvious answer is to try creating a few derived classes to see how it goes. But even without doing this there are several issues you should consider when designing your class.

6.4.1.1 Protection levels

The most obvious inheritance-related aspect of your class is the protection level of its members—which should be public, which should be protected, and which should be private? First, remember that protected members are a part of your class's programming interface even though they are only accessible to derived classes. This means that you should not make all your class's internal workings protected just in case some derived class needs access to them—defining a protected member should be something you think through just as carefully as you would when defining a public member.

It is instructive to look at how the Windows Forms libraries use access specifiers. The framework uses protected for two distinct reasons. One is for methods designed to be overridden. This includes all the OnXxx methods—these are protected to allow you to modify the control's behavior when certain things happen; they are deliberately not public, because only the control itself should be able to decide when to raise an event. The other is for members that are not meant to be overridden, and that only need to be accessible if you are changing the operation of the control. For example, the SetStyle method is protected (but not virtual). Changing any of the control's style flags typically involves providing some corresponding code to deal with the change, so it makes sense only to let derived classes change them.

If a member doesn't fall into one (or both) of these two categories, it should be either private or public. If protected does seem like the right option, you should always ask yourself if you are exposing an implementation detail you might want to change. In an ideal world, the internal workings of a class would be completely invisible to the outside world, and both the public and protected members would present a perfectly encapsulated view. In practice, expediency tends to demand that the protected parts of the interface provide a certain amount of insight into the class's construction. It is hard to come up with any hard and fast rules as to how much information is too much, but you should at least consider how difficult it would be to change the implementation given the existence of each protected member. If it looks as though you might be painting yourself into a corner, you should reconsider your design.

VB.NET defaults to Friend protection level for controls on forms and user controls. (Friend is the equivalent of internal in C#.) This is superficially convenient—it means that you don't need to change the protection level of a control on a form to use it in a derived form, yet it makes the controls inaccessible to external components. However, it is better to make controls private unless you have a good reason not to—you don't wat to restrict your code's scope for change any more than you have to. And if you need to make controls available to a derived class, protected is usually a better choice than Friend.


6.4.1.2 Virtual methods

In .NET, methods and properties can only be overridden if the base class chooses to allow it by marking them as virtual (in C#) or Overridable (in VB). There is a school of thought that says everything should be virtual, the argument being that this is the most flexible approach possible. But the argument is misguided.

All non-private methods represent a kind of contract between the object and its clients. Some methods are designed just to be called (all public non-virtual methods). Here the contract is apparently straightforward—if the client calls the method, the object will do whatever the method is documented to do. When examined in detail, the contract can turn out to be quite subtle—the precise semantics of the method and the full consequences of calling it can be surprisingly extensive once any side effects have been taken into account. This often goes well beyond the documented behavior—it is very common for client code to be dependent on undocumented subtleties in the behavior of a class's programming interface. (For example, the AutoTextBox shown earlier relies on events being processed in a particular order.)

Conversely, some methods are designed just to be overridden. There is no way of enforcing this—such methods are usually defined as protected virtual, but nothing stops a deriving class from calling them as well as deriving them. Most of the OnXxx methods (e.g., OnHandleCreated) fall into this category. Here, the contract is typically fairly straightforward—the framework guarantees to call such method under certain circumstances to allow you to modify the control's behavior, often in a fairly narrowly scoped way.

So what about public virtual methods? These are tricky because they are subject to both sets of issues described above. Moreover, if the derived class overrides a public virtual method, it becomes responsible for preserving the semantics of the original method. It is tempting to think that if you are using your derived class in a controlled environment, it won't matter if you change the way the method behaves. However for controls, you often have much less leeway than you expect, for two reasons. First, at design time, the Forms Designer makes certain assumptions about how the control will behave. (For example, our partially thread-safe TextBox had to take special action because the Designer presumes that the Text property can be used in an environment where the Invoke method is unusable. This constraint is not immediately obvious from the documentation.) Second, the framework may also make use of certain methods or properties when you are not anticipating it, and it may expect behavior you have not supplied. For example, it is not uncommon for controls that manage their own size or layout to interact badly with the ScrollableControl, because it makes certain assumptions about how controls determine their own size.

This is not to say that you should never make public members virtual. But if you do, be prepared to document the full set of requirements that the derived class will be taking on if it overrides the method. It is usually safest to mandate that the derived class calls back into your class's implementation. (The majority of the virtual methods defined by the framework require this.)

6.4.1.3 Event handling

In Windows Forms, every public event will have an associated protected virtual OnXxx method. Overriding such methods is the preferred way for derived classes to handle these events. If you are designing a class with inheritance in mind, you should define similar methods for any public events you add to your class.

So if you add a new event called, say, Highlighted to your class, you should also add an OnHighlighted method. Its signature should be the same as the event delegate signature but without the first object sender or sender As Object parameter—derived classes can just use the this or Me keyword if they need a reference to the object from which the event originates. The OnHighlighted method would be responsible for raising the event through the delegate. You should also make clear in your documentation that deriving classes must call the base class implementation of the OnXxx method for the event to be raised. You should also document whether it is OK for deriving classes to "swallow" the event (i.e., prevent it from being raised) by not calling the base implementation.

Be aware that derived classes may decide to call the OnXxx method directly to raise the event artificially. If for some reason it is difficult for you to accommodate this, document the fact.

    [ Team LiB ] Previous Section Next Section