Rice University - Comp 212 - Intermediate Programming

Spring 2007

Lecture #4 - OOP Fundamentals; From Functional Programming (FP) to OOP 

0. Recall Pizza Design

The above Pizza design exhibits two fundamental relationships between classes and/or interfaces in OOP: "is-a" and "has-a"

The "is-a" relationship is called inheritance.  In the above design, the inheritance hierarchy formed by IShape, Circle and Rectangle is called a taxonomy tree.

The "has-a" relationship is called composition.

OO design decomposes the data objects in the problem domain into appropriate combinations of inheritance hierarchies and compositions.  This process is called encapsulation

I. Fundamentals of Object-Oriented Programming

Encapsulation

``Encapsulation is the process of compartmentalizing the elements of an abstraction that constitute its structure and behavior; encapsulation serves to separate the contractual interface of an abstraction and its implementation.'' [Booch]
Behavioral Encapsulation vs. Data Encapsulation

Often, encapsulation in OOP will appear as a combination of both behavioral and data encapsulation.  In Java, we encapsulate the structural and behavioral elements of an abstraction in a class. These are the fields and the methods of the class, respectively. The attributes public and private are used to separate the interface from its implementation.

In Java, the basic behaviors of all data are encapsulated and implemented in a class called Object.  For example, any instance of Object has a method called toString(), which returns the string consisting of the name of the class followed by the the at-sign character `@', and the unsigned hexadecimal representation of the hash code of the object!

    new Object().toString() may return the string "java.lang.Object@15273a"

When we program in Java, we rarely use the Object class directly.  Instead, we identify the various data and their behaviors in the problem domain and encapsulate them in appropriate user-defined classes.  Any user-defined class automatically has all the data (fields) and behaviors (methods) that are encapsulated in Object.  This concept is called "inheritance".  For example, the class Rectangle inherits all the methods and fields of Object.

    new Rectangle(4, 5).toString() may return the string "Rectangle@a43123"

Inheritance

In OOP, inheritance is defined as the capability to define new classes from existing classes.  Class B inherits class A means B contains all the code of A and can automatically access all the non-private fields and methods of A. Class A is said to be the super class of class B.  Class B is said top be a subclass of class A.  In Java, B may override any non-private method of A that is not final, and provide new behavior by redefining it with new code. B may also have additional fields and methods that A has absolutely no knowledge of. As such, B is said to be a specialization or variant of A. Inheritance is also called the "is-a" relationship between two classes. 

The inheritance relationship between classes is transitive.  That is, if class A inherits from class B, and class B inherits from class C, then class A inherits from class C.  In Java, any user-defined class inherits from the Object class.  Whenever a class is defined without specifying any super class (also called parent class), it is automatically a (direct) subclass of Object.  For example, the classes Pizza, Circle, Rectangle, are all subclasses of Object.  The Object class is said to be at the root (or top) of the inheritance hierarchy.

Inheritance is useful when a group of classes present a similar interface to the outside world, but have different internals.

Inheritance also facilitates a form of code re-use.  In effect, a subclass re-uses the super class's code.  The subclass can extend the functionality of the super class by adding new fields and methods.  The subclass can also replace functionality of the super class by overriding fields and methods defined by the super class.  (We will rarely, if ever, use inheritance in this way.  We will use class composition, the `has-a', rather than the `is-a' relation, in such cases.)

We can think of inheritance as viewing the taxonomy tree from the bottom up.  A dual and powerful point of view is to look at the taxonomy tree from the top down.  In the above inheritance hierarchy for IShape, an IShape object can be any of the concrete variant shape.  IShape is said to be polymorphic.

Polymorphism

Webster's dictionary offers the following definitions of polymorphism: ``2. (Biol.) (a) The capability of assuming different forms; the capability of widely varying in form. (b) Existence in many forms; the coexistence, in the same locality, of two or more distinct forms independent of sex, not connected by intermediate gradations, but produced from common parents.'' [Webster's Revised Unabridged Dictionary, (C) 1996, 1998 MICRA, Inc.]

In OOP, the definition of polymorphism more closely parallels that of Webster's dictionary than you might expect.  The term is applied to variables which may refer at run time to objects of different but related classes.  The classes are related in that they are subclasses of the same super class.  Thus, they all inherit the interface of the super class, obligating them to support any method (behavior) of the super class.

Furthermore, in Java, a variable of a super class type can be assigned any object created from a subclass, but not the other way around.  For example,

 

IShape s = new Circle(2.7); // OK.

IShape t = new Rectangle(3, 4); // OK.

s.getArea(); // get the area of the circle of radius 2.7

t.getArea(); // get the area of a rectangle 3 x 4

t = s; // OK, the old Rectangle is gone.

t.getArea(); // What area is this?

s = new Rectangle(5, 6); // Is it OK?

// Has t changed at all at this point?

t.getArea(); // What area is this?

Circle u = new Rectangle(5, 6); // NO!

Object x = new Circle(4.3); // Is it OK?

x.getArea(); // Is it OK?

((Circle)x).getArea(); // This is called type-casting.  We will see more type-casting in future notes.

 

Polymorphism and inheritance are dual concepts that always go hand in hand. They are the foundation of object-oriented programming. Good object-oriented designs effectively exploit polymorphism/inheritance to reduce code complexity and facilitate code maintenance.

 

II. OOP vs. FP formulation of the Pizza Design

Recall the design for the solution of the Pizza problem.

 

The following table contrasts the OOP formulation in Java against the functional programming formulation in Scheme.

OO Design in Java Data-directed Design in Scheme
(Advanced Level)
/**
 *
A pizza has a price and a shape.
 */

public class Pizza {
  private double _price;
  private
IShape _shape;
  public Pizza(double p, IShape
s) {
   
_price = p;
   _shape = s;
  }
  public double getPrice() {
    return _price;
  }
  public IShape
getShape() {
    return _shape;
  }
}
;; Pizza represents a pizza 
;; getPrice: --> num 
;; returns the price of the pizza
;; getShape: --> IShape
;; returns the shape of the pizza

(define-struct Pizza (getPrice getShape))


;; pizzaFactory: num IShape --> Pizza
;; returns a Pizza, given a price, p, and
;; and IShape, s.

(define (pizzaFactory p s)
  (make-Pizza 
    (lambda () p)
    (lambda () s)))
/**
 *
A shape is an abstract entity that intrinsically knows how to compute its area.

 */

public interface IShape
{
  public double getArea();
}
;;IShape represents an abstract shape
;; getArea: --> num 
;; returns the area of the IShape

(define-struct IShape (getArea))
/**
 *
A rectangle is a shape.
 * It has a width and a height and a concrete
 * way of computing its area using its width and height.

 */

public class Rectangle
implements IShape {
  private double _width;
  private double _height;
  public Rectangle(double w, double h) {
      _width = w;
      _height =
h;
 }
  public double getArea() {
   
return _width * _height;
  }
}
;;rectFactory: num num --> IShape
;; returns an IShape that is a rectangle
;; where w is the width and h is the height 

(define (rectFactory w h)
  (make-IShape
    (lambda () (* w h))))
/**
 *
A circle is a shape.
 * It has a radius and a concrete way of computing its area using its radius.

 */

public class Circle implements IShape {
  private double _radius;
  public Circle(double r) {
    _radius = r;
  }
  public double getArea() {
    return Math.PI * _radius * _radius;
  }
}
;;circFactory: num --> IShape
;; returns an IShape that is a circle
;; where r is the radius 

(define (circFactory r)
  (make-IShape
    (lambda () (* pi (sqr r)))))
/**

 * Computes the price/area ratio of a Pizza to model the notion of 
 * a "deal". Compares the deals between two Pizzas.
 */

public class PizzaDeal {

    /**
     * Computes the price/area ratio of a given Pizza.
     * @param p != null
     * @returns the price/area ratio of p.
     */

    public double deal(Pizza p) {
        return p.getPrice() / p.getShape().getArea();
    }



    /**
     * Compares the price/area ratios of the two Pizza parameters.
     * @param p1 != null
     * @param p2 != null
     * @returns true if p1 is a better deal than p2, false otherwise.
     */

    public boolean betterDeal(Pizza p1, Pizza p2) {
        return deal(p1) < deal(p2);
    }

}



;;PizzaDeal represents an object to 
;; calculate the deal of and between Pizzas
;;deal: Pizza --> num
;; returns the price to area ratio
;; of the pizza.
;; Smaller number is a better deal.
;; betterDeal: Pizza Pizza --> boolean
;; Returns true if Pizza p1 is a better deal 
;; than Pizza p2. Returns false otherwise.

(define-struct PizzaDeal(deal betterDeal))

;;pizzaDealFactory: --> PizzaDeal
;; returns a PizzaDeal

(define (pizzaDealFactory)
  (local ((define this
    (make-PizzaDeal
      (lambda (pizza)
        (/((Pizza-getPrice pizza))
          ((IShape-getArea ((Pizza-getShape pizza))))))
      (lambda (p1 p2)
        (< ((PizzaDeal-deal this) p1) 
                   ((PizzaDeal-deal this) p2))))))
  this))


public class PizzaClient {

    /**
     * Prints the answer to the problem stated in the above. 
     * Question to students: explain the output.
     */

    public void run() {

        Pizza round = new Pizza (3.99, new Circle (2.5));
        Pizza rect =  new Pizza (4.99, new Rectangle (6, 4));
        PizzaDeal pd = new PizzaDeal();
        System.out.println(round + " is a better deal than " + rect 
                           + ": " + pd.betterDeal(round, rect));

    }



    /**
    * Main entry to the program to find the better deal.
    * Instantiates an instance of PizzaClient and tells it to run.
    * This is what all main() should do: instantiates a bunch of
    * objects and "turn them loose"!
    * There should no complicated logic and/or control in main().
    * @param nu not used
    */

    public static void main (String[] nu) {
        new PizzaClient().run();
    }

}
"PizzaDeal test cases:"
(define pd (pizzaDealFactory))
(= (/ 4.99 24) ((PizzaDeal-deal pd) p1))
(= (/ 3.99 (* pi 6.25)) ((PizzaDeal-deal pd) p2))
(not ((PizzaDeal-betterDeal pd) p1 p2)) 
((PizzaDeal-betterDeal pd) p2 p1)

 

 

III. Extending the Pizza problem

What if we want to find the best deal among one or more pizzas?  Can we re-use what we have written without making any changes to the existing code?

To solve this problem, we will need a data structure to hold zero or more pizzas.  One of the most most fundamental data structures in programming is the list structure.  Remember Comp 210?  Instead of designing a list structure to hold only Pizza objects, we will formulate and implement a list structure to store any Object instances.

 
Dung NguyenAlan Cox, Stephen Wong last revised 01/10/2007