Inheritance
From Ssdlpedia
Contents |
[edit] Motivation
- The story so far:
- We've considered an object and the class from which it was instantiated.
- We've described several stereotypes of methods
- State: mutability, copying strategies
- In copying strategies we talked a bit about inheritance, but still we stayed in the single-class settings.
- Inheritance is a mechanism for composing a new class from an old ones.
- A fundamental OOP mechanism. Usually considered to be a synonym to OOP
- Why do we need inheritance
- (recall)a class fixes these aspects of its objects: forge, mill, structure, protocol, behavior
- => If you need to have a new object that is different from an existing object in any of these aspects, you need to define a new class
- Only if the change is in the state, then we can use the existing class
- In all other cases, we need to create slightly different version of an existing class
- In Duplicate code elimination we used two abstraction techniques: proceduralization, parameterization to eliminate the duplicated code.
- Inheritance is an even stronger technique. It allows a programmer to capture similarities between classes and not just between methods/functions
- (recall)a class fixes these aspects of its objects: forge, mill, structure, protocol, behavior
[edit] The Points Example
public class Point2d { private int x, y; public Point2d(int x0, int y0) { x = x0; y = y0; } public int getX() { return x; } public int getY() { return y; } public int moveRight(int dx) { x += dx; } public int moveUp(int dy) { y += dy; } @Override public String toString() { return "[" + x + "," + y + "]"; } public boolean isOrigin() { return x == 0 && y == 0; } }
- What's identical?
- two int fields, moveRight(), moveUp(), getX(), getY()
- What's similar ?
- Constructor, toString(), isOrigin()
- What's new ?
- z field, getZ(), moveIn()
- If there is a repetition let's capture it.
public class Point3d extends Point2d { private int z; public Point3d(int x0, int y0, int z0) { super(x0, y0); z = z0; } public int getZ() { return z; } public int moveIn(int dz) { z += dz; } @Override public String toString() { return "[" + x + "," + y + "," + z + "]"; } public boolean isOrigin() { return super.isOrigin() && z == 0; } }
[edit] Definition
The semantics of inheritance: The protocol + behavior + structure of the superclass is copied into the the subclass (see also: Copying principle). Of course the language has to specify what happens if there is a collision between a definition in the subclass and a copied definition.
In this example, our new class Point3d (subclass) uses many of the code of Point2d (superclass) Each member in Point3d was created by exactly one of these steps:
- An introduced member (no equivalent definition in the superclass). field z; methods: getZ(), moveIn()
- An inherited member (subclass accepts a superclass definition as-is): E.g.: fields x and y; methodss: getX(), getY(), moveRight(), moveUp(),
- A redefined member (both subclass and superclass define the same member): toString(), isOrigin()
Comments
- In most language a subclass is also a subtype. Thus a variable of type superclass, can be assigned with values of type subclass (Assuming the language allow a class to be used as a type).
- In most languages (i.e.: Java, C#) superclass members are inherited by the subclass (unless they are redefined). Constructors are an exception to this rule: A constructor of the superclass is not inherited by the subclass. Thus, a constructor taking two int parameters is not part of the forge of Point3d despite the fact that such a constructor is defined by Point2d.
- In most languages an inherited definition can be inherited from an indirect superclass (e.g.: the superclass of the superclass) and not just from the immediate superclass.
- Thus, the inherited aspects are: protocol, behavior, structure
The most interesting members are the redefined ones: a single name/signature can be bound to two competing definitions. A language can take one of several approaches to resolve this issue. The most popular approach is that of overriding.
[edit] Graphical notation
The rest of the discussion focuses primarily on redefined members.
[edit] Inclusion Polymorphism
- Employee Manager example.
thisis polymorphic.- Automatic up casting==
- Array polymorphism
[edit] Dynamic Binding
[edit] Meaning
- Binding of super and this/self.
- Binding of default arguments in C++.
- Binding of static methods in C++/Java.
- Reminder: overloading vs. overriding.
[edit] Implementation in C++/Java Classes
[edit] Implementation in Smalltalk
[edit] Refinement
- super.myMethod(...)
- static binding of super vs. dynnamic binding of this
- Precursor
- Alpha refinement
- Beta refinement
- Method combinators
[edit] Subclassing vs. subtyping
- Three reasons for subclassing without subtyping:
- Subclass has stricter expectation
- Subclass has a richer protocol
- Classes with similar behavior/structure/protocol, which do not should not be compatible (minutes, hours)
- Code reuse vs. polymorphism
- Poly: same piece of code can work with objects of different classes
- The most typical example: invoking methods of an interface
- example
- Code reuse: we want the behavior of a class to be avaiable in another class
- the most typical example: Strict inheritance
- example: Printer class (formats integers and strings), and MyPrinter(formats program specific objects)
public class Printer { public void printObject(o) { printString(o.toString()); } public void printString(String s) { ... } public void printInt(int n) { ... } ... } public class MyPrinter extends Printer { public void printPoint(Point p) { ... } public void printDouble(double d, int digitsLeft, int digitsRight) { ... } public void printList(String sep, Iterable<Object> os) { String temp = ""; for(Object o : os) { printString(temp); temp = sep; printObject(o); } } }
If a program defines a MyPrinter class it is likely that the code will use MyPrinter directly (and not through its superclass, Printer). This will enable the code to invoke the printDouble() or printList() methods which are not available at the super class. So, in this example, we actually do not need MyPrinter to be a sub-type of Printer.
In Minutes, Hours we do not want polymorphism, yet we do want to share the code.
- We (statically) know
- Seconds and Minutes classes
- Private inheritance in C++
- New Eiffel trick
- Java: Interfaces vs. classes
- Theoretical solution: Interfaces for variable types, a class is not a type
[edit] Phylogenesis and Onthogenesis
[edit] To subclass or to change the current class: that is the question
This may seem like a simple question, but it is rather complicated. The simple answer is that whenever we need a new behavior/modified behavior/type we should define a new class. A deeper examination of the behavioral aspect reveals that every behavioral change can be implemented within an existing class, without introducing new classes. A slight change in the object's state can adapt the behavior of a method. Thus, instead of intrudcing a subclass all that is needed is to change the object's state.
Here's is a system of three classes: an abstract superclass and two concrete subcalsses. The different behavior of the step() method is achieved by overriding the checkOverflow() method.
public abstract class Time { protected int n; public void step() { n += 1; checkOverflow(); } protected abstract checkOverflow(); } public class Minutes extends Time { protected abstract checkOverflow() { if(n >= 60) n = 0; } } public class Hours extends Time { protected abstract checkOverflow() { if(n >= 24) n = 0; } }
Here is the same program, this time implemented as a single class whose behavior is determined by its state:
public class Time { protected final int limit; protected int n; <br> public Time(int limit) { this.limit = limit; } <br> public void step() { if (++n >= limit) n = 0; } } ... Time hours = new Time(24); Time minutes = new Time(60);
A more general approach is the switch on field approach. We use a dedicated field in the class to determine the behavior of a method.
public class List { private final int flag; private final Object[] os; public List(int n, int removeFlag) { flag = removeFlag; os = new Object[n]; } public Object get(int index) { return os[index]; } public void set(int index, Object o) { os[index] = o; } public void remove(int index) { if(flag == 0) { os[index] = null; return; } for(int i = index + 1; i < os.length; ++i) os[i-i] = os[i]; } } ... List x = new List(100, 0); x.set(0,"a"); x.set(0,"b"); x.remove(0); x.get(0); // Result: null List y = new List(100, 1); y.set(0,"a"); y.set(0,"b"); y.remove(0); y.get(0); // Result: "b"
the Delegation approach achieves a similar effect, with a somewhat cleaner design. On the other hand, with delegation, the code introduces a new command class (for each new required behavior) so the subclassing is just moved from one location to another.
The bottom line is that there's never a need to introduce a new subclass. A class can always be reengineered to support whatever change of behavior that is needed without introducing a new subclass.
As the number of supported behaviors grows, it usually the case that a program that takes the subclassing approach is cleaner, more coherent, and more readable than a program that takes the delegation approach.
Another example
Parsing: Subclass to get a new recognizer vs. configure the recognizer with a predicate object.
Discussion: Transferring responsibility: recognizer calls the InputReader or InputReader feeds the recognizer
[edit] Binding issues
Overriding combined with inclusion polymorphism raises a binding question.
Suppose that a class D inherits from a class B, and that
method m which is implemented in B is overridden in D.
Consider now a polymorphic variable v whose static type is B,
but, thanks to inclusion polymorphism, has an instance of D.
Now, which version of m should be invoked if a message m
is sent to v?
- Static binding policy dictates that in this case,
B's version ofmis invoked. - On the other hand, dynamic binding policy dictates that
D's version ofmis invoked.
As it turns out, in almost all cases the dynamic binding semantics is the desired one. In fact, this is the default semantics in Java and most other languages. Curiously, in C++, Simula and a handful of other languages, the default semantics is static. This is explained by the fact that static binding can be implemented more efficiently. The programmer has to indicate explicitly that the dynamic binding semantics is desired.
[edit] Choosing an implementation
- Overloading
- Overriding
- Multi-dispatch
