CSC/ECE 517 Fall 2011/ch17 5b uo

From Expertiza_Wiki
Jump to navigation Jump to search

When to use Inheritance?

Inheritance is a useful programming concept, but it is easy to abuse this concept. Often interfaces [1] or delegation [2] are better options. In this wiki chapter, we will discuss the situations when to use and when not to use inheritance. In general, the following cases 'almost' justify the use of Inheritance

  • Representing "is-a" relationship
  • Reusing code
  • Implementing polymorphism
  • Public Interface


These points are discussed in detail below

Representing "Is-a" Relationship

A "Is-a" relationship can be represented by inheritance in most cases.For example cat "Is-a" animal. Lets consider a scenario of an organization where a person can either be a manager or an employee. Thus a employee "Is-a" person. Can this "Is-a" relationship be represented by inheritance? Let us assume that an employee can be represented as a subclass of person. What happens when the employee is promoted to become a manager? In this case when a employee is promoted to become a manager, all the references of the employee has to be removed which can be cumbersome. Hence the employee "Is-a" person relation cannot be represented by inheritance. So how do we represent this relationship? This problem can be solved by using referencing in the following way:

public class Person
{
   private String name;
   private String gender;
   private String address;
   public String getAddress()
   {
       return address;
   }
   
   ...other methods and data...
}
public class employee
{
   private Person me;
   public String getAddress() { return me.getAddress(); }
   public float getSalary() {}
   ...other methods and data...
}

Hence a class x shouldn't be a super class of class y based on a role played by y that is modeled by x.
A "Is-a" relationship between a super class S and a subclass T can be represented by inheritance only if S and all its references can be replaced by T without losing correctness of the program. This is called Liskov's Substitution Principle (LSP) [3].

An example where LSP determines the fate of a "Is-a" relationship is the well-known square is a rectangle scenario. Even though a square "is-a" rectangle there could be scenarios where we cannot use inheritance. Consider a rectangle class that has the following two methods

public void scaleLength(int scalefactor){length = length*scalefactor}
public void scaleWidth(int scalefactor){width = width*scalefactor}

While these two methods are perfectly fine for a rectangle, using these method on a square will result in unexpected behavior (change the square into a rectangle. Thus, in order to use inheritance to implement a class B which is identical to class A except that it has extra restrictions on its state should not be a subclass of A, both classes should be immutable (objects of the class cannot have their state changed in a way that is noticeable to outside objects)

Code Reuse

One of the popular reasons to use inheritance is to take the advantage of code reuse. The idea of reusability is one of the key components of Object-oriented (OO) design. OO Programming encapsulates attributes and methods that manipulates the data into one small unit, a unit that can be instantiated multiple times, hence supporting reusabilty of code. If the classes are well-designed classes, they can be debugged once and used over and over as a basis for new classes. Inheritance enables reusabilty of code and removal of redundant code by confirming to the DRY (Dont repeat yourself) [4] (aka DIE - Duplicating is Evil) principle. In inheritance the subclass inherits all the attributes and methods of the superclass, hence, removing the need of defining common attributes and methods in two different classes. But representing the relationship between objects having some common attributes and/or methods by inheritance might not be appropriate all the time. The rule of thumb would be to evaluate if the relationship satisfies the same two principles required by a "Is-a" relationship to be represented as inheritance. Furthermore, there are other ways of achieving code reuse such as composition. Consider an example below

Case 1: Using Inheritance for code reuse

class Fruit {
    public int peel() {
        System.out.println("Peeling is appealing.");
        return 1;
    }
}

class Apple extends Fruit {
}

class Example1 {
    public static void main(String[] args) {
        Apple apple = new Apple();
        int pieces = apple.peel();
    }
}
Case 2: Using Composition
class Fruit {
    public int peel() {
        System.out.println("Peeling is appealing.");
        return 1;
    }
}

class Apple {

    private Fruit fruit = new Fruit();
    public int peel() {
        return fruit.peel();
    }
}

class Example2 {

    public static void main(String[] args) {

        Apple apple = new Apple();
        int pieces = apple.peel();
    }
}

In both the cases we have used DRY principle. In the first case, the subclass Apple automatically inherits the implementation of peel method in the superclass Fruit. In the second case, the subclass Apple becomes the front-end class and superclass Fruit becomes the back-end class and Apple class explicitly invokes the Peel method in the back end class from its own implementation of the method. This explicit call is sometimes called "forwarding" or "delegating" the method invocation to the back-end object.

Inheritance-based Polymorphism

Polymorphism [5] meaning 'many forms' has the same meaning in Object Oriented programming language as it is the ability to have an attribute, method or an object in more than one form. Operator overloading [6] is a good example of polymorphism. In java a "+" operator adds two integer type operands while it concatenates two string type operands. Hence the "+" operator can exist in multiple forms. Inheritance provides an opportunity for polymorphism by leveraging method overloading or method overriding. Method overriding is a means by which a subclass can override methods inherited from superclass. The next question that follows is when and how method overriding can be useful. Lets assume a duck class and a hen class. Duck "Is-a" bird. Hen also "Is-a" bird. They have a lot in common but they produce different sounds. So, having bird as a superclass with a talk method that is overridden by the subclasses hen and duck would make a good program.

public class bird {
        public String name;
 
        public bird(String name) {
                this.name = name;
        }
 
        public String sounds() {
                return "Unknown bird!!";
        }
}

public class duck extends bird {
 
        public duck(String name) {
                super(name);
        }
 
        @Override
        public String sounds() {
                return "quack!" ;
        }
}
public class hen extends bird {
 
        public hen(String name) {
                super(name);
        }
 
        @Override
        public String sounds() {
                return "cocock!";
        }
}
public class overridingLeveragedByPolymorphism {
 
        public static void main(String[] args) {
                ArrayList<bird> birds = new ArrayList<bird>();
                birds.add(new hen("chicken"));
                birds.add(new duck("duckling"));
 
                for (int i=0;i<birds.size();i++) {
                        thisBird = birds.get(i); 
                        System.out.println(thisBird.name + " sounds " + thisBird.sounds());
                }
        }
 
}


//output
//chicken sounds cocock!
//duckling sounds quack!

Public Interface

Public Interface define the behavior/properties of a class that is visible to other objects. In the classic Squares and Rectangles example, the reason why we could not use inheritance was because, the public methods of rectangle class were not all appropriate for square class. Consider an example below

public class Rectangle
{
   private int x, y, width, height;
   public Rectangle(int x, int y, int w, int h) {
      this.x = x; this.y = y; width = w; height = h;
   }
   public int getWidth() { return width; }
   public int getHeight() { return height; }
   public int getArea() { return width * height; }
   public int getPerimeter() { return 2 * (width + height); }
   public void setSize(int w, int h) { width = w; height = h; }
   public void stretch(Rectangle r, int dx,){
     r.setSize(r.getWidth() + dx, r.getHeight());
   }
}

public class Square extends Rectangle
{
   public Square(int x, int y, int side) {
      super(x, y, side, side);
   }
}

In this example, most of the methods that are meant for rectangle can be used by square. Method stretch is a public method that stretches one of the sides of the rectangle. If we use this method for square, the square ceases to be a square and becomes a rectangle. This is an undesirable behavior. However, we can fix this by deleting the stretch method or by setting stretch as a private method. Upon doing so, we would not be able to invoke the stretch method for squares and all of the other methods would result in similar/expected behavior.

Composition vs Inheritance

Composition simply means using instance variables that are references to other objects. As an example, we can look at the code below

class Animal{
...
}

class Cat{
    private Animal animal= new Animal();
    ...
}

In the example above, class Catis related to class Animal by composition, because Cat has an instance variable that holds a reference to a Animal object. In this example, Cat can be called as the front-end class and Animal can be referred to as the back-end class. In a composition relationship, the front-end class holds a reference in one of its instance variables to a back-end class.

Traditionally, composition is used to represent "has-a" relationship and inheritance is used to represent "is-a" relationship. However, the problem of choosing the right representation depends hugely on the implementation details. Some of the differences are briefly mentioned below

  • It is easier to change the interface of a back-end class (composition) than a superclass (inheritance) because any change to a superclass's interface can not only ripple down the inheritance hierarchy to subclasses, but can also ripple out to code that uses just the subclass's interface whereas though a change in the interface of a back-end class necessitates a change to the front-end class implementation, it does not require you to change the front-end interface.
  • It is also easier to change the interface of a front-end class (composition) than a subclass (inheritance)because when changing a subclass's interface you have to confirm that it is compatible with that of its superclasses.
  • Composition allows you to delay the creation of back-end objects until (and unless) they are needed, as well as changing the back-end objects dynamically throughout the lifetime of the front-end object. With inheritance, you get the image of the superclass in your subclass object image as soon as the subclass is created, and it remains part of the subclass object throughout the lifetime of the subclass.
  • It is easier to add new subclasses (inheritance) than it is to add new front-end classes (composition), because inheritance comes with polymorphism. Inhertance promotes better reuse of code.
  • The explicit method-invocation forwarding (or delegation) approach of composition will often have a performance cost as compared to inheritance's single invocation of an inherited superclass method implementation.
  • With both composition and inheritance, changing the implementation (not the interface) of any class is easy. The ripple effect of implementation changes remain inside the same class.

Drawbacks of inheritance

Inheritance is not without its own set of drawbacks. If inheritance is applied without due consideration problems can arise. In some situations it can:

  • Reduce the comprehensibility of code.
  • Make maintenance harder.
  • Make further development harder.
  • Reduce reliability of code.
  • Reduce overall reuse.

Conclusion

We have considered the benefits and drawbacks of inheritance in an object oriented programming environemnt. The following pointers summarize what we have discussed above

  • A similar interface (that is, similar method signatures) is not sufficient for an elegant subclass/superclass or class/interface relationship; consistent behavior is also required. In particular, a class B should not inherit class A’s methods and then nullify them or change their behavior to do something completely different.
  • Liskov Substitution Principle should be adhered to when using interface. i.e. It is acceptable to make a class B a subclass of class A or to make B an implementer of interface A only if, for every method in both A’s and B’s interfaces, B’s method accepts as input all the values that A’s method accepts (and possibly more) and does everything with those values that A’s method does (and possibly more).
  • A class B that is identical to another class A except that it has extra restrictions on its state should not be a subclass of A unless both classes are immutable.
  • If class B models a role played by class A, especially a temporary role, then B should not be a subclass of A. Instead referencing should be used.
  • Inheritance between a superclass A and a subclass B is appropriate if it promotes code reuse,each object of class B “is an” object of class A, the public interface of class B includes the interface of class A and the behavior of the methods in this interface in both classes is similar in both classes, and there is a need for polymorphism to allow a variable of type A to refer to an object of type B. Otherwise, inheritance is probably inappropriate.
  • Inheritance makes it hard to follow the flow of execution through the code of a program, and it makes it hard for the programmer to change one of the classes in the hierarchy without affecting the others.

As such, the reasons for using inheritance are modularity, separation of concerns, clear representation of concepts, categorization and polymorphism.

References