Object Orientation

Object orientation, or “OO” is a powerful idea that is still, many years after becoming mainstream computer science, sometimes misunderstood.

Two Philosophical World Views

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:

 CircleRectangle
AreaArea of CircleArea of Rectangle
PerimeterPerimeter of CirclePerimeter 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-StyleOO-Style
  • area function
    • if circle then ...
    • else if rectangle then ...
  • perimeter function
    • if circle then ...
    • else if rectangle then ...
  • Circle class
    • area method
    • perimeter method
  • Rectangle class
    • area method
    • perimeter method

OO in Practice

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?

Exercise: Read each of those.

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.”

Exercise: Memorize this quote.

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.

inigo-oop.jpg

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.

Classes or Prototypes?

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 Orientation

Classes 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?

Exercise: Read about Plato at the Stanford Encyclopedia of Philosophy and about his theory of forms at Wikipedia.

Examples of Classes

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.

Examples of Prototypes

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:

Exercise: Implement circle types in Io and Lua.

How to read OO Code

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.

Exercise: Read about what Turkle and Papert have to say about this.

Case Study

Remember the two ways of organizing code from above?

Non OO-StyleOO-Style
  • area function
    • if circle then ...
    • else if rectangle then ...
  • perimeter function
    • if circle then ...
    • else if rectangle then ...
  • Circle class
    • area method
    • perimeter method
  • Rectangle class
    • area method
    • perimeter method

This tells us that:

Let’s see this in action. Here are circles and rectangles with a procedural orientation:

shapes_procedural_demo.swift
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:

shapes_object_demo.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
    }
}

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?

The Expression Problem

Let’s review this one more time:

 Non-OOOO
ExpressionFunctions-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 functionsEasy because no existing code needs to be touched
Ease of
adding new
operations
Easy because no existing code needs to be touchedHard 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:

shapes_extension_demo.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.

Limiting Variants or Subtypes

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
  }
}
Exercise: Read this article comparing sealed classes and enums. It compares Kotlin and Swift.

Getters and Setters

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?

Exercise: Prepare and give a 10 minute TED-style talk on the evils of getters and setters.

Recall Practice

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.

  1. What is the difference between the object-oriented and process-oriented philosophies?
    The former sees objects (with behaviors) as fundamental, the latter sees processes (operating upon objects) as fundamental.
  2. How are object-oriented systems organized at the top-level?
    As a cooperating set of objects, where “code” is simply just another property of an object, namely its behavior.
  3. How are non-object-oriented systems organized at the top-level?
    As a set of functions, each passing around pieces of data to come up with a result.
  4. What is the view of OOP popularized by Cardelli and Wegner?
    That OOP is about three things: types, data abstraction, and dynamic polymorphism.
  5. Who coined the term object orientation?
    Alan Kay
  6. Alan Kay said, in a letter to Stefan Ram, “OOP to me means only ________________, local retention and protection and hiding of ________________, and extreme ________________ of all things.”
    messaging, state-process, late-binding
  7. Where did the original idea of object-oriented programming come from?
    Alan Kay’s realization that his teacher’s remark “the key to successful decomposition of a system is to break it down into parts that are just like the whole” applied to computer systems. Objects are little computers bundling both state and behavior.
  8. The two major approaches to OOP are characterized by ________________ and ________________. Which of the two is more associated with Plato and why?
    classes and prototypes; classes are more associated with Plato because they are ideal forms that objects are instances of. Prototypes are actual objects of the same types as their derivatives. Classes are distinct kinds of things, if they can be said to be things at all. The class of all dogs is not a dog.
  9. Which languages use prototypes rather than classes?
    JavaScript, Io, Self, and Lua
  10. When writing code in the OOP style, you can generally add new ________________ without changing existing code, but adding new ________________ generally requires editing existing code.
    types, operations
  11. When writing code in the procedural style, you can generally add new ________________ without changing existing code, but adding new ________________ generally requires editing existing code.
    operations, types
  12. What is the expression problem?
    The problem of designing a programming language or paradigm in which you can add both new types and new operations without changing existing code.
  13. With enums (as in Swift), the number of variants is fixed. How do you get the same effect in languages that prefer subclassing to enums?
    By sealing the interface or base class.
  14. What might have caused the proliferation of getter and setter methods in languages like Java and C# that purport to be object oriented? What are the arguments as to why getters and setters are evil?
    That all fields should be private and accessed and updated through methods. But getters and setters violate the notion of objects having agency and behavior, and should normally not be considered as dumb bundles of data.

Summary

We’ve covered:

  • The Object and Process Philosophies
  • OO in Practice
  • Classes vs Prototypes
  • How to “read” OO code
  • Examples both with and without OO
  • The Expression Problem
  • Getters and setters—evil or not?