Lecture: 10 Date: Jan 31, 2008 How many recognize this problem? interface ITaxiVehicle { } class Cab implements ITaxiVehicle { int idNum; int passengers; int pricePerMile; ... } class Limo implements ITaxiVehicle { int idNum; int passengers; int pricePerMile; int minRental; ... } class Van implements ITaxiVehicle { int idNum; int passengers; int pricePerMile; boolean access; ... } Add fare method. Every class computes fare in a different way. Add costPerRider method. Every class computes costPerRider in the same way. We're forced to write the same thing again and again (A Really Bad Idea). Today we introduce abstraction - a way of pulling out common parts and engendering code re-use. abstract class ATaxiVehicle implements ITaxiVehicle { ... } The abstract keyword means that we can't make instances of this class, in other words, you shouldn't see: new ATaxiVehicle(...); Q: What is the design recipe for abstraction? We can lift the common fields to the abstract class. There are three fields that occur in each of our "subclasses" of ATaxiVehicle, ie. Limo, Van, Cab. So we have: abstract class ATaxiVehicle implements ITaxiVehicle { int idNum; int passengers; int pricePerMile; ... } Even though we can't construct instances of an abstract class, we can write a constructor for the class that the subclasses can use. This saves us from rewriting the initialization code for the three common fields: abstract class ATaxiVehicle implements ITaxiVehicle { int idNum; int passengers; int pricePerMile; ATaxiVehicle(int idNum, int passengers, int pricePerMile) { this.idNum = idNum; this.passengers = passengers; this.pricePerMile = pricePerMile; } } Subclasses must declare that they are subclasses of ATaxiVehicle using the extends keyword: class Limo extends ATaxiVehicle { int minRental; ... } We removed the three fields defined in ATaxiVehicle because we "inherit" them. Likewise, we inherit all the implements obligations of the class we're extending. Now we have to write the Limo constructor: class Limo extends ATaxiVehicle { int minRental; Limo(int idNum, int passengers, int pricePerMile, int minRental) { ... } } The constructor takes just as many arguments as it did before abstraction, but we can now rely on the super class constructor to initialize the fields: class Limo extends ATaxiVehicle { int minRental; Limo(int idNum, int passengers, int pricePerMile, int minRental) { super(idNum, passengers, pricePerMile); ... } ... } What's left is initializing the one field specific to Limo in the usual way: class Limo extends ATaxiVehicle { int minRental; Limo(int idNum, int passengers, int pricePerMile, int minRental) { super(idNum, passengers, pricePerMile); this.minRental = minRental; } ... } Now, in each class Limo, Cab, and Van we have the following code: int costPerRider(int passengers, int miles) { return this.fare(passengers, miles) / passengers; } We can lift these definitions into the abstract class and remove them from the subclasses: abstract class ATaxiVehicle implements ITaxiVehicle { int idNum; int passengers; int pricePerMile; ATaxiVehicle(int idNum, int passengers, int pricePerMile) { this.idNum = idNum; this.passengers = passengers; this.pricePerMile = pricePerMile; } int costPerRider(int passengers, int miles) { return this.fare(passengers, miles) / passengers; } } Now the code is written only once and the sub classes will reuse this implementation. Finally since every taxi vehicle should have a fare method, but its definition is different for each class, we put the header in the abstract class and declare it abstract, signally that every subclass must define this method: In ATaxiVehicle --------------- abstract int fare(int passengers, int miles); Another example: consider our drawing package that we use, it has a drawDisk method in Canvas: In Canvas --------- boolean drawDisk(Posn, int, IColor); When we wanted to extend Posns to do things like compute the distance to the origin, we developed new classes (CartPt) that had these methods we wanted. Since every shape has a location, we might abstract as follows: +------------------+ | AShape | +------------------+ | CartPt loc | +------------------+ | double distTo0() | +------------------+ Since every shape will compute distTo0 in the same way, we can lift this method: In AShape --------- double distTo0() { return this.loc.distTo0(); } We also want every shape to be able to have a draw method for drawing itself on a given canvas, but every shape class is drawn differently. We put only the header for the method in the class and declare it abstract: In AShape --------- abstract boolean draw(Canvas c); In Circle, we have to implement draw. We might write: In Circle --------- boolean draw(Canvas c) { return c.drawDisk(this.loc, this.radius, new Red()); } But drawDisk expects a Posn and we've given it a CartPt -- this blows up. We have two options at this point: 1) add a toPosn method to CartPt that returns to corresponding Posn, change the draw method to the following: In Circle --------- boolean draw(Canvas c) { return c.drawDisk(this.loc.toPosn(), this.radius, new Red()); } 2) Extend Posn. It is already a concrete class (that is, not abstract), but it turns out we can extend concrete classes just like we can abstract classes. class CartPt extends Posn { // no need for x, y; we inherit them. CartPt(int x, int y) { super(x,y); } double distTo0() { ... } } Note that: - Every CartPt is a Posn. - Not every Posn is a CartPt. Now we can code draw as we did initially: In Circle --------- boolean draw(Canvas c) { return c.drawDisk(this.loc, this.radius, new Red()); } Even though loc is a CartPt, since every CartPt is a Posn, this will work.