CSC/ECE 517 Fall 2011/ch17 5b br

From Expertiza_Wiki
Jump to navigation Jump to search

When to use Inheritance: Lecture 17

What is Inheritance

In object-oriented programming (OOP), inheritance is a way to reuse code of existing objects, establish a subtype from an existing object, or both, depending upon programming language support. In classical inheritance where objects are defined by classes, classes can inherit attributes and behavior from pre-existing classes called base classes or superclasses or parent classes or ancestor classes. The new classes are known as derived classes or subclasses or child classes. The relationships of classes through inheritance gives rise to a hierarchy.

Different Perspectives on Inheritance

In this article we will consider different perspectives on when to use inheritance and when not to use inheritance. Let us look at the following reasons that are considered when using inheritance:

Code Reuse

A milestone on the pathway to an elegant implementation of inheritance is software reuse. Software reuse, better known as code reuse, is the practice of using existing software or software knowledge to build new software programs.<ref>Frakes, W.B. el al, 2005 </ref> A software library is a practical example of code reuse. Whether it is a large corporation or a collegiate engineering project, programmers utilize existing code repositories to reduce time spent during the development phase. In a well-designed multi-tier software program where inheritance is properly implemented code reuse is achieved through using the Don't Repeat Yourself design principle. And although inheritance provides this capability a few things should be considered during the design process: <ref> Tips for Effective Software Reuse </ref>

  1. Focus on well-defined general aspects of the software program,
    • look for areas within the program where a library can be utilized or possibly developed for future reuse
  2. Name software assets appropriately
    • Package names should be a category classification that encapsulating all the data
    • Interface names should be an unifying theme that groups classes together
    • Class names should be proper nouns
    • Method names should be simple action verbs
  3. Being consistent is critical for reusing code over time
    • This is most common with products that have a long life-span and continually evolve in functionality

The items listed above are not a comprehensive guideline for the proper implementation of inheritance through code reuse.They are merely provided as a starting point for a programmer to consider during the preliminary states of software development. Any although inheritance provides the ability to reuse any code; the practically of that implementation is strictly dependent on the programmer and the requirements of the application. For instance the below example displays the guideline presented in the above list.


// The following code is used at a small organization to determine what the 
// shipping cost of their packages are. 
// This code demonstrates how inheritance can be used effectively to expand the 
// functionality of a given program. 
// Also observe the how the above guideline are followed. 


class Parcel_Dimension {
    double width;
    double height;
    double depth;
    
    // constructor when all dimension specified
    Parcel (double w, double h, double d){
        weight = w;
        height = h;
        depth = d;
    }
        
}

class ParcelWeight extends Parcel_Dimension {
    double weight; // weight of parcel

    // constructor for parcel weight
    ParcelWeight(double w, double h, double d, double m)
        width = w;
        height = h;
        depth = d;
        weight = m;
}

class Shipping_Cost extends ParcelWeight {
    double cost;
    
    // constructor for shiping price
    Shipping_Cost(double w, double h, double d, double m, double c)

    super(w, h, d, m);
        cost c;
}

class Shipment_Printout{
    public static void main(String args[]){
        Shipment_cost letter1 = new Shipment_cost (10, 20, 15, 10, 3.66)
        
        System.out.println(" Weight of letter is: " + letter1.weight);
        System.out.println(" Shipping cost: " + letter1.cost);
}

IS-A

Inheritance allows a new class to extend an existing class. The new class inherits the members of the class it extends. When one object is a specialized version of another object, there is an "is a<ref> When to Use Inheritance </ref> " relationship between them. Here are a few examples of the "is a" relationship:

  1. A car is a vehicle.
  2. A rectangle is a shape.

When an "is a" relationship exists between objects, it means that the specialized object has all of the characteristics of the general object, plus additional characteristics that make it special. In object-oriented programming, inheritance is used to create an "is a" relationship among classes. This allows you to extend the capabilities of a class by creating another class that is a specialized version of it.

Consider the following implementation of the Circle and Ellipse classes<ref> Inheritance - Proper Inheritance and Substitutability</ref> using inheritance. In the implementation Circle will be the sub-classclass of the Ellipse, hence it doesn’t need to implement anything but a constructor. From a geometrical perspective, every circle “is a” ellipse. Furthermore, there are certainly good opportunities for code reuse between them. Therefore, it seems natural to make the Circle class a subclass of the Ellipse class. But is this a good decision?

public class Ellipse
{
private int x, y, width, height;
public Ellipse(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 void setSize(int w, int h) { width = w; height = h; }
}

public class Circle extends Ellipse
{
public Circle(int x, int y, int width) {
super(x, y, width, width);
}
}

Because of the fact that subclasses inherit all methods of their superclasses, the Circle class now inherits a setSize method that has two parameters. A call to this method can make the width and height of the Circle unequal, a rather undesirable outcome. A setSize method for Squares should just take one parameter. This can be avoided by nullifying the negative effects of the inherited setSize method by overriding it in the subclass. For example, we might add the following method to the Circle class:

public void setSize(int w, int h) 
{
width = h;
height = h; 
}

thereby allowing users to modify the size of the circle.

Our problem here is that the subclass does not have behavior consistent with the behavior of its superclass. Such consistent behavior is necessary for elegant code. This thereby ensure the Principle of Least Astonishment.

Let us consider the inconsistent behavior between the Ellipse Class and the Circle class, with respect to the Liskov's Substitution Principle<ref> Liskov's Substitution Principle </ref>. The setSize method of the Ellipse class has the behavior of modifying the width independently of the height. A setSize method of the Cirlce class can- not have that behavior and still preserve squareness, and therefore the Circle class’ setSize method does not do everything that the Ellipse class’ setSize method does. The conclusion is that Circle should not be a subclass of Ellipse.

Public Interfaces

If class S responds to all the messages that class C responds to, and then some, it seems appropriate for S to be a subclass of C. Let us consider the following case: A video store application which has a Person class, with name, address fields. It also has a Staff class, which has name, address fields and a Customer Class which also name, address fields. A simple object oriented approach would be to say that a Customer "is a" Person also Staff “is a” Customer, therefore create classes as so.

public class Person
{
private String name;
private String address;
public String getAddress() 
{
  return address; 
}
…
}
public class 
public class Customer extends Person
{
private String CustomerID;
...
}

public class Staff extends Person
{
private String StaffID;
...
}

This system works fine until we have someone who is both a customer and a member of staff. Assuming that we don't really want our everyone list to have the same person in twice, once as a Customer and once as a Staff, we make an arbitrary choice between: It would be overly complex and difficult to maintain if you attempt to have a set of derived classes that implement the People. This is especially true given that the above example is very simple - in most real applications, things will be more complex. In this case, we would go with different approach. I would implement the Person class and include in it a collection of "roles". Each person could have one or more roles such as "Customer" and "Staff”. This will make it easier to add roles as new requirements are discovered. For example, you may simply have a base "Role" class, and derive new roles from them. In this way, the Person object can be considered to exist permanently, but the person’s roles can come and go. Furthermore, there is no duplication of data.

Polymorphism

Polymorphism, Greek for “many forms” is a feature that allows one interface to be used for a general class of actions. Moreover, the concept of polymorphism is often expressed by the phrase, “one interface, multiple methods,” which means implies that a general interface should be associated with a group of related actives. <ref> Java: The Complete Reference</ref>This in turn provides a situation that aids in the reduction of a programs complexity by allowing the same interface to be used throughout different situations The responsibility of knowing which method needs to be evoke is left up to the compiler. In respect to inheritance polymorphism should be used when a programmer needs to deal in generalities and let the execution-time environment handle the specifics. <ref>Software Engineering Observations </ref> Proper use of polymorphism promotes extensibility, and software that invokes polymorphic behavior is independent of the object types to which messages are sent.

// Using run-time polymorphism
class Figure_2D{
           double dim1;
           double dim2;
           
           Figure_2D(double a, double b){
                       dim1 = a;
                       dim2 = b;
           }
           
           double area(){    
                       System.out.println("Area for Figure is undefined.");
                       return 0;
           }
}

class Rectangle extends Figure_2D{
           Rectangle (double a, double b){
                       super(a, b);
           }
           // overide area for rectangle
           double area(){    
                       System.out.println("Inside Area for Rectangle.");
                       return dim1*dim2;
           }
}           
class Triangle extends Figure_2D{
           Triangle (double a, double b){
                       super(a, b);
           }
           // overide area for right triangle
           double area(){    
                       System.out.println("Inside Area for Triangle.");
                       return dim1*dim2/2;
           }
}           
           
          
class FindAreas{
           public static void main (String args[]) {
                       Figure r new Rectangel (9, 5);
                       Triangle t = new Triangle(10, 8);
                       Figure figref;
                       
                       figref = r;
                       System.out.println("Area is" + figref.area());
                       figref = t;
                       System.out.println("Area is" + figref.area());        
           }
}

Costs of Using Inheritance

There are even more costs to be considered when using inheritance, in addition to those we’ve already addressed.

One problem with inheritance, especially a deep inheritance tree with many generations, is that the code for the methods of a class low in the tree is spread out among all its ancestors higher in the tree, which makes it harder for the reader of the code to follow the flow of execution. That is, suppose someone is reading code and sees that a method foo is invoked on an object. If the object’s class does not implement foo, then the reader needs to look to the object’s immediate superclass. If that class does not implement foo, then a further search up the inheritance hierarchy needs to be made. To complicate matters, foo may invoke another method bar on the same object. There need be little relationship between the locations of foo and bar in the inheritance tree, and so the reader again needs to start at the object’s class and search up the inheritance tree, to find the implementation of bar. Matters are even worse if the reader is not sure of the object’s class and knows, for example, only that the object could belong to any of the subclasses of a given class. In such cases, it is impossible to figure out exactly which method body of which class gets executed at any given time.

Another problem with inheritance is that all subclasses are very tightly tied with their superclasses. This coupling comes from the fact that, to guarantee certain behavior in a subclass, that subclass needs to know significant parts of the implementation of the methods of the superclasses.

Eg: Let's examine the superclass and subclass coupling problems. The following Stack class extends Java's ArrayListclass to make it behave like a stack:

// Even a class as simple as this one has problems. Consider what happens when a user leverages
// inheritance and uses theArrayList's clear() method to pop everything off the stack:

class Stack extends ArrayList
{   private int stack_pointer = 0;
   public void push( Object article )
   {   add( stack_pointer++, article );
   }
   public Object pop()
   {   return remove( --stack_pointer );
   }
   public void push_many( Object[] articles )
   {   for( int i = 0; i < articles.length; ++i )
           push( articles[i] );
   }
}

Stack a_stack = new Stack();
a_stack.push("1");
a_stack.push("2");
a_stack.clear();

The code successfully compiles, but since the base class doesn't know anything about the stack pointer, the Stack object is now in an undefined state. The next call to push() puts the new item at index 2 (the stack_pointer's current value), so the stack effectively has three elements on it—the bottom two are garbage. (Java's Stackclass has exactly this problem; don't use it.)

Inheritance vs Delegation

Delegation is an alternative to inheritance for reusing code among multiple classes. Inheritance uses the IS‐A relationship for re‐use; delegation uses the HAS‐A reference relationship to do the same. Inheritance and delegation have the same kind of relationship that, both are alternatives for fixing a problem, with one more appropriate than the other in some situations. In this chapter, we will study the nature of delegation, see how we can convert an inheriting class to a delegating one, compare the advantages and disadvantages of the two approaches, and identify scenarios in which they should be used.

The main reason, as mentioned, before, for using inheritance is sharing of code among multiple classes. However, as shown in the figure, while inheritance implies code reusability, code reusability does not require inheritance. Delegation is an alternative approach to allow multiple classes to share code. In the case of inheritance, a reusing class has an IS‐A relationship with a reused class. Thus, it inherits code from the reused class. In the case of delegation, the reused class HAS‐A reference to the reused class. This reference allows it to delegate tasks to the reused class. We shall consider the same case of the Stack class extending the Java's ArrayListclass to make it behave like a stack. As discussed before, the problem is that the base class doesn't know anything about the stack pointer, the Stack object is now in an undefined state. One solution to the undesirable method-inheritance problem is for Stack to override all ArrayList methods that can modify the array's state, so the overrides either manipulate the stack pointer correctly or throw an exception.

This approach has two disadvantages. First, if you override everything, the base class should really be an interface, not a class. There's no point in implementation inheritance if you don't use any of the inherited methods. Second, and more importantly, you don't want a stack to support all ArrayList methods. That pesky removeRange() method isn't useful, for example. The only reasonable way to implement a useless method is to have it throw an exception, since it should never be called. This approach effectively moves what would be a compile-time error into run-time.

A better solution to the base-class issue is encapsulating the data structure instead of using inheritance. Here's a new-and-improved version of Stack:

             
class Stack
{  
    private int stack_pointer = 0;
   private ArrayList the_data = new ArrayList();
   public void push( Object article )
   {   the_data.add( stack_pointer++, article );
   }
   public Object pop()
   {   return the_data.remove( --stack_pointer );
   }
   public void push_many( Object[] articles )
   {   for( int i = 0; i < o.length; ++i )
           push( articles[i] );
   }
}

Let us consider different scenarios under which inheritance and delegation fares each other:

Scenarios Inheritance Delegation
Polymorphism If B is a subclass of A, then subtype polymorphism is possible and so an object of class B can be used anywhere an object of class A . If B is composed with A, then this subtype polymorphism does not apply to B.
Interface If B is a subclass of A, then B inherits all methods of A, and so the interface of B must include all the methods in the interface of A, whether B wants them all or not. Furthermore, it is usually not appropriate to “void out” or nullify the methods of A that B doesn’t want if B is composed with A, then the public interface of B need not be related at all to the public interface of A, and so you have the flexibility to design B exactly the way you want.
Efficiency If B is a subclass, there is direct execution of any inherited methods. if B forwards requests to A, then the methods of B must call methods of A, which results in slightly higher overhead costs.
Amount of Code If B is a subclass of A, you need to implement in B only the methods of B that aren’t already inherited from A. If B forwards requests to A, then you must implement all of B’s methods yourself. Although many of these methods might merely call a corresponding method in A, there is still more code to write and therefore more chance of errors.
Dynamic Changeability If B is a subclass of A, then at run-time, there is no way to change the behavior of the inherited methods. If B forwards requests to A, then at run-time, B can change the object of class A or a subclass of A to which it forwards requests.

Conclusion

Just having a similar interface is not sufficient for subclass/superclass or class/interface relationship; consistent behavior is also required. In particular, a sub class should not modify the behavior of the super class.Consider the Liskov's Substitution Principle <ref> Liskov's Substitution Principle </ref>to make a class B a subclass of class A or to make an implementer of interface A. 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 the sub class “is an” object of the super class, and 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. Inheritance, especially a deep inheritance tree is that the code for the methods of a class low in the tree is spread out among all its ancestors higher in the tree, which makes it hard to code and to follow the flow of execution.If you have a class with behavior that applies to only some of the objects of the class, then con- sider splitting the class into two classes associated by inheritance either directly or through a common abstract class or interface. When designing a class B that will be very similar to an existing class A, use referencing if you want the functionality of A but not the interface,and use inheritance only when you want the functionality and interface of A.

References

Citation Notes

<references/>

Full Reference Information

Frakes, W.B. and Kyo Kang, (2005), "Software Reuse Research: Status and Future", IEEE Transactions on Software Engineering, 31(7), July, pp. 529-536.

Tips for Effective Software Reuse. http://www.infoq.com/articles/vijay-narayanan-software-reuse, Oct. 2011.

When to Use Inheritance. http://msdn.microsoft.com/en-us/library/27db6csx(v=vs.80).aspx, Oct 2011.

Inheritance — Proper Inheritance and Substitutability. http://www.kev.pulo.com.au/pp/RESOURCES/C++-FAQ/proper-inheritance.html#, Oct 2011.

Liskovs Substitution. Principle http://javaboutique.internet.com/tutorials/JavaOO, Oct 2011.

Scildt, herbert. Java: The Complete Reference, McGraw Hill, 2007.

Software Engineering Observations. http://faculty.inverhills.mnscu.edu/speng/cs1126/Notes/Polymorphism/What%20is%20Polymorphism.htm, Oct 2011.

Additional Resources