300 likes | 417 Vues
This lecture explores the key concepts of inheritance, composition, and interfaces in object-oriented programming. It explains when to use inheritance versus composition, how to structure classes based on shared code or behavior, and the advantages of each approach. The lecture emphasizes encapsulation, flexibility, and the use of design patterns like the Composite Pattern. It also covers the delegation in composition, allowing for more dynamic behavior in program design. Gain insights on best practices and the trade-offs between these fundamental programming concepts.
E N D
Lecture 14 Inheritance vs Composition
Inheritance vs Interface • Use inheritance when two objects share a structure or code relation • Embodies the is_a relation • Should reflect the roles of the objects throughout the program • Use an interface when they share a common behavior spec
Abstract Classes vs Interfaces • Abstract for the inheritance situation above, i.e. sharing code or structure • Interfaces for sharing behavior spec, not code • Example: Consider a Framework • It provides code in the form of methods that are inherited without being overridden->code is inherited • The implementation of an action listener cannot be predicted, so no code is inherited here
Inheritance and Composition • Which to use and when? • Alternative 1: Implement a Stack with inheritance from a Vector class Stack extends Vector{ public Object push(Object item){ addElementAt(size()-1;} public Object pop(){return elementAt (size()- 1);} • Stacks can use protected members of Vector • Stacks can be used where Vectors are used as arguments • Reuse Vector methods, e.g. size, isEmpty • What to do with unwanted Vector methods, e.g. removeAt()?
Inheritance and Composition • Now do this with Composition: class Stack{ private Vector theData; public Stack(){theData=new Vector();} public Object push(Object item){theData.addElement( item); return item;} • Advantages • Can change the implementation without any impact to users of stacks • Interface is narrower: we don’t need to know anything about Vectors
Inheritance and Composition • Advantages to Composition (cont.) • There are no substitutability issues • Stacks and Vectors are different types • One cannot be substituted for the other • Meaningless behavior is not exposed • Inheritance couples base and derived class • Changes do not ripple upwards • Promotes encapsulation • Not dependent on private variables • Can change implementation of composed objects at run-time, not so with inheritance
Composition • Prefer it to inheritance • Used in Java AWT • Uses Components and Containers • An item is a Component • A Container can contain Components and Containers • Obtain a tree-like structure by nesting • Embodied in the Composite Pattern
Composite Pattern • Facilitate the same treatment of composite and primitive objects • Composite object: an object that contains other objects • E.g. lines and polygons are primitive objects, a drawing is composite. • Composite methods are implemented by iterating over the composite object, invoking the appropriate method for each subcomponent
Composite Pattern • Use it when • You want to represent part-whole hierarchies of objects • You want your clients to be able to ignore differences between compisitions of objects and objects themselves • Benefits • Easy to add new kinds of components • Makes clients simpler
Composite Pattern • Liabilities • Hard to restrict the types of components • Clients can do meaningless things to primitive objects at run-time
Example • Consider a simple GUI system: public class window{ Button[] buttons; TextArea[] ta; Menu[] menus; WidgetContainer[] containers; public void updateWindow(){ if (buttons != null){ for(k=0;k<buttons.length();k++) buttons[k].draw(); if (ta != null) ... }
Problem • If you want to add on a new kind of resource the update() method needs to be modified • Way around this is to use a uniform interface • Just do Widgets and WidgetContainers • Now you are programming to an interface • All Widgets support the Widget interface
Another Attempt public class window{ Widget[] widgets; WidgetContainer[] containers; public void updateWindow(){ if (widgets != null) for (k=0; k<widgets.length(); k++){ widgets[k].updateWindow(); if (containers != null) ....
Now Use Composite Pattern Component Button Menu widgetContainer
To Obtain public class window{ Component[] components; public void updateWindow(){ if (components != null) for(k=0;k<components.length();k++) components[k].updateWindow(); } } • Bottom Line: Do not distinguish Widgets and WidgetContainers
Delegation in Composition • It is often convenient to allow a receiving object, e.g. a Window, to further delegate certain operations to another object--its delegate--e.g. a Rectangle • This is better than making Window a subclass of Rectangle, the Window class may reuse the behavior of Rectangle and delegate rectangle-specific behavior to it
Window Rectangle Rectangle area() area() width height return Rectangle->area() return width*height
Comments • Here Window has_a Rectangle • It is easy to compare behavior at run-time • You can reuse Rectangle as a black box (not white-box as in inheritance) • You can use polymorphism to achieve dynamic behavior--employ an interface. This is the Strategy Design Pattern • Trade-off: harder to understand, much more flexible than inheritance • Visitor uses delegation
Combining Inheritance and Composition • Main use: simplify an inheritance hierarchy • Not achievable everywhere, but it is very powerful where you can • Main example: Java Stream Wrappers • Add capabilities to a stream by “wrapping” it in another object that provides the desired capabilities • Makes a bigger, better version of a base class
InputStream Hierarchy • InputStream • ByteArrayInputStream • FileInputStream • PipedInputStream • SequenceInputStream • ObjectInputStream • FilterInputStream • BufferedInputStream • DataInputStream differ in the source of data values
The Way It Works • Start with an InputStream, i.e. try to read a stream of bytes in sequence • Use it polymorphically • Add functionality, depending on your data • This new functionality is called a “wrapper” • Just add a new and better interface to the old one, getting/sending the result from/to the same place, i.e. InputStream, resp. OutputStream • Subclassing provides the new interface
Example class FilterInputStream extends InputStream ... protected InputStream in; FilterInputStream(InputStream in){ this.in = in;} ...} • Filter is a wrapper • Builds on InputStream • First obtain the sequence of bytes from the InputStream and then do the filtering • Use composition on the InputStream
Comments • You really have one object, many interfaces • Thus you avoid an explosion of the inheritance hierarchy • FilterInputStream is really just an InputStream with added functionality • If you wanted a DataInputStream, just wrap the InputStream in a DataInputStream object • Construct a new DataInputStream object, using the InputStream as input to the constructor
Important Note • The primitive data-type operations of DataInputStream cannot be included in InputStream, because that object does not know about integer, float, etc • But you can make it understand these types by wrapping. • The neat thing: You can use a DataInputStream anywhere an InputStream is expected--thi is the advantage of inheritance
Example • If dataSource is of type InputStream: InputStream dataSource; • Then the DataInputStream: DataInputStream typedDataSource = new DataInputStream(dataSource); • Gets the data from exactly the same place as data retrieved from dataSource • We have merely provided a better interface to the same input stream
Another Example: Buffered Readers • Use Readers and Writers for character data • Start with primitive readers that directly manipulate the input data: • CharArrayReader, StringReader, FileReader • Now add functionality to data generated by the above Readers: • BufferedReader, FilterReader, LineNumberReader • Reader has the subclasses • BufferedReader • FileReader • PipedReader
One Way to Buffer the Char Input • Do it from FileReader: BufferedReader in =new BufferedReader( new FileReader(“stuff.in”); • Buffers the char file “stuff.in”
Another Way • Start with Reader (which is also primitive for 16 Bit Unicodes) • Buffer on top of it • Allow two types of buffer--a standard one with 8192 bytes and another of user-specified size • Use Reader’s capabilities and then add the buffering in the constructor
public class BufferedReader extends Reader{ private Reader in; //use composition private char cb[]; //the buffer private static int defaultCharBufferSize=8192; private static int defaultExpectedLineLength=80; public BufferedReader(Reader in, int sz){ super(in); //get Reader functionality if (sz <= 0) throw new IllegalArgumentException( “Illegal Buffer size”); this.in = in; cb = new char[sz]; nextChar = nChars = 0;} public BufferedReader(Reader in){ this(in, defaultCharBufferSize);}
Remarks • BufferedReader has methods read() and readLine() to read a single character, resp. a line of text. • Both of these throw an IOException • Remember to import from java.io