Focusing on objects can help us deal with complexity. Why might this be? Contrast object-orientation with process-orientation.
OBJECTS | PROCESSES |
---|---|
Things are fundamental | Processes are fundamental |
Democritus | Heraclitus |
Mostly bottom-up; focus on building blocks and components | Top-down; focus on inputs and outputs |
Object-oriented | Algorithmic, or process-oriented |
Recognizes fundamental objects and their behavior — objects know how to act | Recognizes fundamental processes that “operate on” objects |
Modern | Traditional (Classical) |
Directly addresses complexity through abstraction, classification and hierarchy | Has limits to the amount of complexity it can handle: “structured programming appears to fall apart when applications exceed 100,000 lines or so of code.” Also, does not directly address data abstraction and information hiding. |
Natural view of system as a set of highly reusable cooperating objects with crisply defined behaviors; communication via messages | Views a system in terms of algorithms; subroutines process data (which is often global data); Does not address concurrency well, if at all |
OOD leads to systems which are smaller; based on stable intermediate forms; more resilient to change; and can evolve incrementally | Does not scale up well. No real components to swap out; changes generally have wide scope |
What is more fundamental, objects or processes? For example: is there a fundamental process in the universe called push, or are stacks a fundamental kind of object in the universe which happen to behave in such a way that they only grow by adding to the top?
Our brains seem to be wired for this, no?
It is unclear how to design a complex system like an OS, DBMS, robot, or traffic control system in a top down fashion using stepwise refinement because such a system is not a single input-single output program. In such systems it is better to focus on the components, such as buffer managers, query optimizers, transaction managers, etc. Even if the component is essentially a functional component, you still name it with a noun. Examples: ImageMapper, SiteInitializer, StringTokenizer, QueryOptimizer, Enumerator, StreamReader, StreamWriter, Factory, and so on (but don’t overdo this).
When you need to implement the various behaviors of objects you apply top-down algorithm design techniques. You just don’t do the system top-down.
Object oriented principles came out of work in many areas:
An object oriented system is one that is designed as a cooperating set of objects, where “code” is simply just another property of an object, namely its behavior.
A process oriented system is one that is designed as a set of subroutines, each passing around pieces of data to come up with a result.
Basically, that’s it, but the next level of refinement of the terminology requires that the objects be classified — grouped into classes — and that the classes be arranged hierarchically in an inheritance lattice.
Some people are picky and require that to be called “object-oriented” a system must go even further and directly support something called late binding or dynamic polymorphism (otherwise, they say, you can only call the system “object based.”)This distinction between object-based and object-oriented was first popularized in the famous paper by Luca Cardelli and Peter Wegner, “On Understanding Types, Data Abstraction, and Polymorphism” ACM Computing Surveys, December 1985.
What did the term originally mean?
There is great value in understanding the original big idea, but also be pragmatic and understand that language evolves, and for better or for worse, the term means many things to many people. It certainly did not orginially mean the Java or C++ style we have today.
Where do the objects come from? There are two main styles of OOP:
A Java example of a mutable circle class:
public class Circle { private double radius; private double x, y; // center of the circle public Circle(double xc, double yc, double r) {x = xc; y = yc; radius = r;} public double area() {return Math.PI * radius * radius;} public double perimeter() {return 2.0 * Math.PI * radius;} public void expand(double factor) {radius = radius * factor;} public void move(double dx, double dy) {x += dx; y += dy;} }
A C# example of a mutable circle class:
class Circle { private double radius; private double x, y; // center of the circle public Circle(double xc, double yc, double r) {x = xc; y = yc; radius = r;} public double Area() {return Math.PI * radius * radius;} public double Perimeter() {return 2.0 * Math.PI * radius;} public void Expand(double factor) {radius = radius * factor;} public void Move(double dx, double dy) {x += dx; y += dy;} };
A C++ example of a mutable circle class:
class Circle { private: double radius; double x, y; // center of the circle public: Circle(double xc, double yc, double r): x(xc), y(yc), radius(r) {} double area() {return PI * radius * radius;} double perimeter() {return 2.0 * PI * radius;} void expand(double factor) {radius = radius * factor;} void move(double dx, double dy) {x += dx; y += dy;} };
A Groovy example of a mutable circle class:
class Circle { private def x, y, r Circle(x = 0, y = 0, r = 1) {this.x = x; this.y = y; this.r = r} def area() {Math.PI * r * r} def perimeter() {Math.PI * 2.0 * r} def expand(factor) {r *= factor} def move(dx, dy) {x += dx; y += dy} }
A Ruby example of a mutable circle class:
class Circle def initialize(x = 0, y = 0, r = 1) @x = x @y = y @r = r end def area Math::PI * @r * @r end def perimeter Math::PI * 2.0 * @r end def expand!(factor) @r *= factor self end def move!(dx, dy) @x += dx @y += dy self end end
A Python example of a mutable circle class:
import math class Circle: def __init__(self, x, y, radius): self.x = x self.y = y self.radius = radius def area(self): return math.pi * (self.radius**2) def perimeter(self): return math.pi * self.radius * 2 def expand(self, factor): self.radius *= factor return self def move(self, dx, dy): self.x += dx self.y += dy return self
A JavaScript example of a constructor and prototype for mutable circles:
function Circle(x, y, r) { this.x = x || 0; this.y = y || 0; this.r = r || 1; } Circle.prototype.area = function () { return Math.PI * this.r * this.r; } Circle.prototype.perimeter = function () { return Math.PI * 2.0 * this.r; } Circle.prototype.move = function (dx, dy) { this.x += dx; this.y += dy; } Circle.prototype.expand = function (factor) { this.r *= factor; }
Circle c = new Circle(10, 5, 3.2);
in Java, means, let c be a reference
to a newly constructed circle centered at (10, 5) with radius 3.2.
Circle c = Circle(10, 5, 3.2);
in C++, means, let c be
a newly constructed circle centered at (10, 5) with radius 3.2.
c.area();
means ask the circle what its area is.
c.move(3, 2);
means tell the circle to move itself 3 units in the x direction and 2 units
in the y direction.
Note the difference between the functional approach, (move(c, 3, 2)
) where you, the boss, move the (inanimate) circle and your code operates on the circle; and the object approach (c.move(3, 2)
) where the circle is a living breathing agent that moves itself. Here you can identify with the object; the circle has feelings; it is alive; it moves.
One of the major advantages of OOP is that it often becomes possible to extend a system by adding new types in a hierarchy without changing existing source code. (Of course, it’s so easy to add new operations!)
Consider the task of building a library of Shapes that can be used in draw programs, math tutorial programs, CAD programs, etc.
Let’s look two ways to write the library
Comparing the two libraries makes clear the essential distinction between what is object-oriented and what is not.
/***************************************************************************** * * shapelibrary.c * * Illustration of a hierarchy of shapes, implemented in the non object * oriented language C. This program shows that C is insecure when it * comes to implementing hierarchies. * *****************************************************************************/ #include <stdio.h> #include <ctype.h> #include <math.h> #include <stdlib.h> /* Some C compilers do not define PI for you. */ #define PI (acos(-1.0)) /* * The usual implementation of a "class hierarchy" in C is to define a * struct for the root type which includes a "tag" member of an * enumerated type. */ typedef enum {circle, square, polygon} shape; /* * The root class of the hierarchy is called Figure. Every figure * has a color, but then there are different kinds of figures so we * keep a tag to distinguish them. Then we have a set of attributes * which supposedly depend on that tag, grouped into a union. In C, * you cannot "force" dependency on the tag: e.g. for a Figure f you * are completely free to set f.kind = circle and then immediately * assign f.a.side = 5.2, and no compile-time or run-time error will * ever be generated! */ typedef struct { int color; shape kind; /* what kind of figure is it? */ union { /* figure-specific attributes */ float radius; /* radius for circles */ float side; /* side length for squares */ struct {float *x; float *y; int n;} p; /* point array for polygons */ } a; } Figure; /* * Now all operations on figures have an embedded switch, controlled by * the tag of the figure. Note that one drawback of this is that if * we ever extend the system by adding new shapes, the bodies of all * of the operations need to be modified. */ float perimeter(Figure f) { switch (f.kind) { case circle: return 2.0 * PI * f.a.radius; case square: return 4.0 * f.a.side; case polygon: { double result = 0.0; int i; for (i = 0; i < f.a.p.n; i++) { float deltaX = f.a.p.x[i + 1] - f.a.p.x[i]; float deltaY = f.a.p.y[i + 1] - f.a.p.y[i]; result += sqrt(deltaX * deltaX + deltaY * deltaY); } return result; } } } float area(Figure f) { switch (f.kind) { case circle: return PI * f.a.radius * f.a.radius; case square: return f.a.side * f.a.side; case polygon: { double result = 0.0; int i; for (i = 0; i < f.a.p.n; i++) { result += f.a.p.x[i] * f.a.p.y[i + 1] - f.a.p.x[i + 1] * f.a.p.y[i]; } return result / 2.0; } } } /* * Here are the operations which do not depend of the kind of shape * we have. */ int getColor(Figure f) { return f.color; } void setColor(Figure f, int color) { f.color = color; } /* * To fake a constructor in C, you write a function that returns a * particular kind of Figure. */ Figure makeCircle(int color, float radius) { Figure f; f.color = color; f.kind = circle; f.a.radius = radius; return f; } Figure makeSquare(int color, float side) { Figure f; f.color = color; f.kind = square; f.a.side = side; return f; } Figure makePolygon(int color, float packedCoordinates[], int numPoints) { Figure f; int i; f.color = color; f.kind = polygon; /* * Copies the packed array of coordinates into separate x and y arrays * and adds a new last point which is the same as the first point * to allow cleaner implementations of perimeter and area. */ f.a.p.n = numPoints + 1; f.a.p.x = malloc(sizeof(float) * (f.a.p.n + 1)); f.a.p.y = malloc(sizeof(float) * (f.a.p.n + 1)); for (i = 0; i < numPoints; i++) { f.a.p.x[i] = packedCoordinates[i * 2]; f.a.p.y[i] = packedCoordinates[i * 2 + 1]; } /* Make the last point identical to the first */ f.a.p.x[f.a.p.n] = f.a.p.x[0]; f.a.p.y[f.a.p.n] = f.a.p.y[0]; return f; } /* * The main program makes some figures then calls some operations * on them. */ void show(Figure f) { setColor(f, 0x00FF33CC); printf("Perimeter = %f, area = %f\n", perimeter(f), area(f)); } float aTriangle[] = {0, 0, 5, 0, 0, 12}; float aRectangle[] = {0, 0, 5, 0, 5, 20, 0, 20}; int main() { int i; Figure fig[5]; fig[0] = makePolygon(0x0000FF00, aTriangle, 3); fig[1] = makeCircle(0x00FF8888, 1.0); fig[2] = makeCircle(0x00CCCCCC, 10.0); fig[3] = makeSquare(0x00ABCD12, 3.0); fig[4] = makePolygon(0x0000FF00, aRectangle, 4); for (i = 0; i < sizeof fig / sizeof(Figure); i++) { show(fig[i]); } return 0; }
This C program
Unions basically suck. It is interesting to note that some languages do allow the construction of a type-safe version of a union. Ada calls this a variant record.
The Java version is broken up into several files. First an abstract base class:
package edu.lmu.cs.oop.figures; import java.awt.Color; /** * A little library of figures. It is the classic example of object * oriented programming. In our library of figures all figures are * derived from an abstract class called Figure. It is abstract * because there are no plain figures; all real figures are instances * of classes derived from Figure. A figure has a color which can be * read and written with getColor() and setColor(); the default color * is blue. Also, you can get the area and perimiter of any figure; * these methods are abstract because their implementation depends * on the particular subclass. */ public abstract class Figure { protected Color color = Color.blue; public void setColor(Color c) {color = c;} public Color getColor() {return color;} public abstract double perimeter(); public abstract double area(); }
Now here are three subclasses.
package edu.lmu.cs.oop.figures; import java.awt.Point; /** * A simple circle class which is part of our figures package. */ public class Circle extends Figure { protected Point center; protected double radius; public Circle(Point c, double r) {center = c; radius = r;} public double perimeter() {return 2.0 * Math.PI * radius;} public double area() {return Math.PI * radius * radius;} public void expand(double scaleFactor) {radius *= scaleFactor;} }
package edu.lmu.cs.oop.figures; import java.awt.Point; /** * A simple square class which is part of our figures package. */ public class Square extends Figure { protected Point upperLeft; protected double sideLength; public Square(Point upperLeft, double sideLength) { this.upperLeft = upperLeft; this.sideLength = sideLength; } public double perimeter() { return 4 * sideLength; } public double area() { return sideLength * sideLength; } }
package edu.lmu.cs.oop.figures; import java.awt.Point; /** * A simple polygon class which is part of our figures package. */ public class Polygon extends Figure { protected Point[] vertices; /** * Accept any array of points, but make a new array with the * first point repeated at the end to store internally. */ public Polygon(Point[] vertices) { this.vertices = new Point[vertices.length + 1]; System.arraycopy(vertices, 0, this.vertices, 0, vertices.length); this.vertices[this.vertices.length - 1] = this.vertices[0]; } public double perimeter() { double result = 0.0; for (int i = 0; i < vertices.length; i++) { double deltaX = vertices[i + 1].x - vertices[i].x; double deltaY = vertices[i + 1].y - vertices[i].y; result += Math.sqrt(deltaX * deltaX + deltaY * deltaY); } return result; } public double area() { double result = 0.0; for (int i = 0; i < vertices.length; i++) { result += vertices[i].x * vertices[i + 1].y - vertices[i + 1].x * vertices[i].y; } return result / 2.0; } }
Note:
The essential difference between process and object orientation is illustrated here:
If you’re going to be adding more objects, the OO arrangement is good. If you’re going to be adding more operations, the procedural arrangement is good.
In Clojure, you can use both datatypes and protocols to extend systems simultaneously in both directions. You can do this in other languages, too. How to do it well is known as the Expression Problem.
You need to pay careful attention to knowing which methods
should be abstract and which should not. If the implementation
is the same across classes (e.g. getColor
and setColor
)
the method should not be abstract; if the implementation is different,
that is, it depends on the subclass (e.g. perimeter
and
area
), then it should be made abstract.
BUT WAIT! Why do we even have getColor
and setColor
. Shouldn’t we avoid getters and setters?