Design Hints for Inheritance in Java

Design Hints for Inheritance in Java

Some hints that we have found useful when using inheritance.

I. Place common operations and fields in the superclass.

Instead of replicating each field in subclasses, we should put common fields and methods on the superclass.

II. Don’t use protected fields

Some programmers think it is a good idea to define most instance fields as protected, “just in case” so that subclasses can access these fields if they need to. However, the protected mechanism doesn’t give much protection, for two reasons:

  • The set of subclasses is unbounded - anyone can form a subclass of your classes and then write code that directly accesses protected instance fields instead of using getter methods, thereby breaking encapsulation.
  • In Java, all classes in the same package have access to protected fields, whether or not they are subclasses.

III. Use inheritance to model the “is–a” relationship.

Inheritance is a handy code-saver, but sometimes people overuse it. For example, suppose we need a Contractor class. Contractors have names and hire dates, but they do not have salaries. Instead, they are paid by the hour, and they do not stay around long enough to get a raise.

There is the temptation to form a subclass Contractor from Employee and add an hourlyWage field.

public class Employee {
   private double salary;
   private LocalDate hiredDate;
}

public class Contractor extends Employee {
   private double hourlyWage;
   . . .
}

Look good, right?

But this is not a good idea, because now each contractor object has both a salary and hourly wage field.

The contractor-employee relationship fails the “is–a” test. A contractor is not a special case of an employee.

IV. Don’t use inheritance unless all inherited methods make sense.

If you find any method in superclass which is not appropriate or doesn't make sense in the subclass, inheritance is not appropriate.

V. Don’t change the expected behavior when you override a method.

The substitution principle applies not just to the syntax but, more importantly, to behavior. When you override a method, you should not unreasonably change its behavior. The compiler can’t help you - it cannot check whether your redefinitions make sense.

Technical debt and bugs come from here :)))

VI. Use polymSourceorphism, not type information.

Whenever you find code of the form

if (x is of type 1)
  action1(x);
else if (x is of type 2)
  action2(x);

think about polymorphism.

Do action1 and action2 represent a common concept? If so, make the concept a method of a common superclass or interface of both types. Then, you can simply call:

x.action();

Code that uses polymorphic methods or interface implementations is much easier to maintain and extend than code using multiple type tests.

VII. Don’t overuse reflection.

The reflection mechanism lets you write programs with amazing generality, by detecting fields and methods at runtime. This capability can be extremely useful for systems programming, but it is usually not appropriate in applications. Reflection is fragile—with it, the compiler cannot help you find programming errors. Any errors are found at runtime and result in exceptions.

(Source : Core Java Volumn I - Fundamentals)