Look at the world—or the universe—around you. What do you see? Do you see:
If you see or feel the latter, you may like object-orientation (OO); if you like the former, you may like process-orientation. The latter view sees strings as things that can be uppercased, trimmed, and searched. The former sees uppercasing as a fundamental process in the universe, that uppercases all the things. How it uppercases a paper plate I do not know....
What does this have to do with programming?
OBJECTS | PROCESSES |
---|---|
Things are fundamental. | Processes are fundamental. |
Democritus | Heraclitus |
Mostly bottom-up, focused on building blocks and components. | Top-down, focused 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. |
Been around since the late 1960s. | Been around since the dawn of computing. |
Directly addresses complexity through abstraction, classification and hierarchy. | Approaches complexity by getting data abstraction and information hiding through clever tricks with functions, like making closures, which start to look like objects. |
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. Functions process data (sometimes global). Functions pass data to each other. Functions can be composed. |
Object-based designs leads to systems which are smaller, based on stable intermediate forms, more resilient to change, and can evolve incrementally (observation by Grady Booch). | Appears to not scale up well. No real components to swap out; changes generally have wide scope. People end up packaging functions into modules or packages, which feel like...objects. |
These two world views translate into different ways of organizing code. Suppose we needed to compute the areas and perimeters of circles and rectangles. We need four operations:
Circle | Rectangle | |
---|---|---|
Area | Area of Circle | Area of Rectangle |
Perimeter | Perimeter of Circle | Perimeter of Rectangle |
The two world views show different emphases. In the non-OO style, the functions are emphasized. In the OO style, the objects (actually the objects’ types) are emphasized:
Non OO-Style | OO-Style |
---|---|
|
If you started learning about “object oriented” in the 1980s, you’d hear about how 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, they would tell you, 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, only then do you apply top-down algorithm design techniques. You just don’t do the system top-down. So today, people say:
Basically, that’s it, but the next level of refinement of the terminology requires that the objects be class-ified — that is, 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. This has led to all discussions of object-orientation being filled with terms such as: Object, Class, Abstraction, Encapsulation, Modularity, Hierarchy, Inheritance, Generalization, Specialization, Superclass, Subclass, Aggregation, Type, Field, Method, Constructor, Message, Polymorphism, Identity, State, Behavior, Use Case, Scenario, Framework, and Pattern.
But wait! What did the term originally mean?
So the term “OOP” in the words of Alan Kay, was coined to describe “messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”
There is great value in understanding the original big idea, but you should also be pragmatic and understand that language evolves, and for better or for worse, the term now means many things to many people. It certainly, emphatically, did not originally mean the Java or C++ style of inheritance and method overriding we have today.
You should understand all the meanings. Kay’s original meaning was influenced by a big idea (attributed to his teacher Bob Barton): that the key to successful decomposition of a system is to break it down into parts that are just like the whole. Kay saw that computers need not be decomposed into separate and barely overlapping concepts of algorithms and data structures, but rather neatly into little computers that communicate through messaging each other.
Where do the objects come from? There are two main styles of OOP:
Most languages support classes. Prototypes are found in JavaScript, Io, Self, and Lua.
Plato and Object OrientationClasses are like Plato’s ideal forms. They are not things themselves, but they are the kinds of things that things are. The class of all dogs is not a dog, but it is the kind of thing that dogs are. A prototype is an actual dog, and you can make more dogs from it. A prototype-based system has no need for classes or any kind of ideal forms. See the difference?
Here are some examples that show how classes bundle construction, structure definition, and behavior.
Each example shows the same mutable circle class implemented in several languages.
class Circle { private double x, y, radius; 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;} }
class Circle { private double x, y, radius; 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;} };
class Circle { private: double x, y, radius; 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;} };
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} }
class Circle( private var x: Double, private var y: Double, private var radius: Double) { fun area(): Double = Math.PI * radius * radius fun perimeter(): Double = 2.0 * Math.PI * radius fun expand(factor: Double) { radius *= factor } fun move(dx: Double, dy: Double) { x += dx; y += dy } }
class Circle def initialize(x = 0, y = 0, r = 1) @x = x @y = y @r = r end def area = Math::PI * @r * @r def perimeter = Math::PI * 2.0 * @r def expand!(factor) @r *= factor self end def move!(dx, dy) @x += dx @y += dy self end end
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
Note how classes actually define types. They are called “classes” because they classify things.
A JavaScript example of a constructor and prototype for mutable circles:
function Circle(x = 0, y = 0, r = 0) { this.x = x this.y = y this.r = r } Object.assign(Circle.prototype, { area() { return Math.PI * this.r * this.r }, perimeter() { return Math.PI * 2.0 * this.r }, move(dx, dy) { this.x += dx; this.y += dy }, expand(factor) { this.r *= factor } })
Here it is in Io:
Circle := Object clone do( x := 0 y := 0 radius := 0 set := method(xc, yc, r, x = xc y = yc radius = r ) area := method( Number constants pi * radius * radius ) perimeter := method( 2 * Number constants pi * radius ) expand := method(factor, radius = radius * factor ) move := method(dx, dy, x = x + dx y = y + dy ) ) circle := Circle clone circle set(0, 0, 5) circle area println circle perimeter println circle expand(2) circle area println circle move(3, 4)
Lua embeds its “prototype” inside of a metatable:
There’s thing that sometimes pops up in discussions about OO called “Tell don’t ask”. Basically it means that objects should do things, even if one of the things they do is answer queries about themselves. This leads to a way of reading code:
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 tell the circle to respond with 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.
Remember the two ways of organizing code from above?
Non OO-Style | OO-Style |
---|---|
|
This tells us that:
Let’s see this in action. Here are circles and rectangles with a procedural orientation:
enum Shape {
case rectangle(Float64, Float64)
case circle(Float64)
}
func area(_ shape: Shape) -> Float64 {
switch shape {
case .rectangle(let width, let height):
return width * height
case .circle(let radius):
return Float64.pi * radius * radius
}
}
func perimeter(_ shape: Shape) -> Float64 {
switch shape {
case .rectangle(let width, let height):
return 2 * (width + height)
case .circle(let radius):
return 2 * Float64.pi * radius
}
}
let r = Shape.rectangle(10, 20)
let c = Shape.circle(5)
print(area(r), perimeter(r), area(c), perimeter(c))
We can add an isRound
operation, say, without disturbing any existing code. But to add a triangle type, we have to modify the Shape
type source code.
Now here are circles and rectangles with an object orientation:
protocol Shape {
func area() -> Float64
func perimeter() -> Float64
}
struct Rectangle: Shape {
let width: Float64
let height: Float64
func area() -> Float64 {
return width * height
}
func perimeter() -> Float64 {
return 2 * (width + height)
}
}
struct Circle: Shape {
let radius: Float64
func area() -> Float64 {
return Float64.pi * radius * radius
}
func perimeter() -> Float64 {
return 2 * Float64.pi * radius
}
}
let r = Rectangle(width: 10, height: 20)
let c = Circle(radius: 5)
print(r.area(), r.perimeter(), c.area(), c.perimeter())
Now adding a triangle type is trivial, no existing course code needs to be changed. But adding a isRound
operation requires us to edit all of the source code of all of the existing types.
Is there a way to get the advantages of both?
Let’s review this one more time:
Non-OO | OO | |
---|---|---|
Expression | Functions-first: area(c) | Objects-first: c.area()
|
Ease of adding new types | Hard because we have to change existing code, adding new branches to the if -statements in all the functions | Easy because no existing code needs to be touched |
Ease of adding new operations | Easy because no existing code needs to be touched | Hard because we have to change existing code, adding new methods to existing classes |
It seems like no matter which approach above we take, we will either have to edit existing code when adding operations, or edit existing code when adding types. Designing a programming language or paradigm in which you can add both new types and new operations without changing existing code is called the Expression Problem.
Here’s a way we can do this in Swift:
protocol Shape {
func area() -> Float64
func perimeter() -> Float64
}
struct Rectangle: Shape {
let width: Float64
let height: Float64
func area() -> Float64 {
return width * height
}
func perimeter() -> Float64 {
return 2 * (width + height)
}
}
struct Circle: Shape {
let radius: Float64
func area() -> Float64 {
return Float64.pi * radius * radius
}
func perimeter() -> Float64 {
return 2 * Float64.pi * radius
}
}
extension Shape {
func isRound() -> Bool {
return self is Circle
}
}
struct Triangle: Shape {
let a: Float64
let b: Float64
let c: Float64
func area() -> Float64 {
let s = (a + b + c) / 2
return (s * (s - a) * (s - b) * (s - c)).squareRoot()
}
func perimeter() -> Float64 {
return a + b + c
}
func isRound() -> Bool {
return false
}
}
let r: Shape = Rectangle(width: 10, height: 20)
let c: Shape = Circle(radius: 5)
let t: Shape = Triangle(a: 3, b: 4, c: 5)
print(r.area(), r.perimeter(), r.isRound())
print(c.area(), c.perimeter(), c.isRound())
print(t.area(), t.perimeter(), t.isRound())
And here is a nice example in Clojure (using records and protocols).
You can read about the Expression Problem, and various approaches to solving it, at Wikipedia.
In the previous examples, we had a Shape
type that either had two variants (when enums were used) or two subtypes (when using enums, classes, or structs). But in principle, we could have many more variants or subtypes of shapes: triangles, ellipses, squares, etc.
But sometimes, we will know exactly the entire set of variants or subtypes. With enums, this is automatically done for you:
enum Tree { case empty indirect case node(String, Tree, Tree) var size: Int { switch self { case .empty: return 0 case let .node(_, left, right): return 1 + left.size + right.size } } } // More operations can be added here
But in other languages, enums are not the style! Instead, they use classes or interfaces or protocols and extend them. How then do you say “allow only these subclasses but no others”? The answer is to seal the interface or base class. Here’s a Kotlin example:
sealed interface Tree { fun size(): Int // More operations can be added here object Empty : Tree { override fun size() = 0 // Implement additional operations here } data class Node( private val data: String, private val left: Tree, private val right: Tree ) : Tree { override fun size() = left.size() + right.size() + 1 // Implement additional operations here } }
There’s one last thing to cover in this painfully short overview of OO.
In some treatises on “Object Oriented Programming” you will see people say “always make your fields private and access them and update them with methods.” This is not a bad idea, but when the only thing these methods do is access and update, you end up with silly sounding method names like getColor
and setColor
. We’ve created methods, it seems, just to avoid direct access to a property. So what the heck did with bother for?
Shouldn’t we avoid getters and setters?
What do you think now?
Here are some questions useful for your spaced repetition learning. Many of the answers are not found on this page. Some will have popped up in lecture. Others will require you to do your own research.
We’ve covered: