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. Subroutines process data (sometimes global). Subroutines pass data to each other. Subroutines 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. |
Let’s see OO and non-OO code together. We’ll implement four operations:
Circle | Rectangle | |
---|---|---|
Area | Area of Circle | Area of Rectangle |
Perimeter | Perimeter of Circle | Perimeter of Rectangle |
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 |
---|---|
|
Here the functions are emphasized. Functions operate on objects. Some people might say in this case the things operated on are just data structures and not objects:
// In the non-OOP style, functions operate on plain objects. function area(shape) { if (shape.type === 'Circle') { return Math.PI * shape.radius * shape.radius } else if (shape.type === 'Rectangle') { return shape.width * shape.height } throw new Error('Unknown shape type') } function perimeter(shape) { if (shape.type === 'Circle') { return 2.0 * Math.PI * shape.radius } else if (shape.type === 'Rectangle') { return 2.0 * (shape.width + shape.height) } throw new Error('Unknown shape type') } // Tests import assert from 'assert' const c1 = { type: 'Circle', radius: 1.0 } const c2 = { type: 'Circle', radius: 5.0 } const r1 = { type: 'Rectangle', height: 1.0, width: 2.5 } const r2 = { type: 'Rectangle', height: 10.0, width: 12.0 } assert.equal(area(c1), Math.PI) assert.equal(area(c2), 25.0 * Math.PI) assert.equal(perimeter(c1), 2.0 * Math.PI) assert.equal(perimeter(c2), 10.0 * Math.PI) assert.equal(area(r1), 2.5) assert.equal(area(r2), 120) assert.equal(perimeter(r1), 7) assert.equal(perimeter(r2), 44)
In the OO style, objects (unlike dumb “data structures”) have behavior. We often see OO code written with the behavior specification for an object (or object family) rolled into a class or interface or prototype or protocol. Here’s a JavaScript specification using the class
syntax:
// In the OOP style, behavior is bundled with the data. class Circle { constructor(r) { this.radius = r } area() { return Math.PI * this.radius * this.radius } perimeter() { return 2.0 * Math.PI * this.radius } } class Rectangle { constructor(w, h) { this.width = w; this.height = h } area() { return this.width * this.height } perimeter() { return 2.0 * (this.width + this.height) } } // Tests import assert from 'assert' const c1 = new Circle(1.0) const c2 = new Circle(5.0) const r1 = new Rectangle(1.0, 2.5) const r2 = new Rectangle(10.0, 12.0) assert.equal(c1.area(), Math.PI) assert.equal(c2.area(), 25.0 * Math.PI) assert.equal(c1.perimeter(), 2.0 * Math.PI) assert.equal(c2.perimeter(), 10.0 * Math.PI) assert.equal(r1.area(), 2.5) assert.equal(r2.area(), 120) assert.equal(r1.perimeter(), 7) assert.equal(r2.perimeter(), 44)
In the expression c.area()
, c
is called the receiver and area
is called a method.
Previously we saw that JavaScript’s class
syntax actually was nothing more than a nice way to define a constructor function and a protype object to be used for all objects created with that constructor. Fine, so it’s not like the “real” classes that you might see in a language like Smalltalk or Ruby or even Java. But so what. It’s fine. Close enough.
We haven’t covered very much so far, but this comparison sure stands out:
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.
You can read about the Expression Problem, and various approaches to solving it, at Wikipedia. For now, though, there is much more to learn about OO.
Sigh. I hate definitions. Usually they can be pretty contentious. But most people today would say, to be object-oriented, a system must:
It’s fine to quibble over how strongly a system has to meet each of these criteria. There’s a view out there you should know about, that says systems meeting most of characteristics 1, 2, and 4, but not 3, are called object-based and not object-oriented. This view appears to have come from this paper.
There is great value in understanding the original big idea, but to 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 what the word means today.
Oh well. Understand both 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.
So let’s explore the ideas of inheritance, encapsulation, and polymorphism. We will make some animals—three different kinds of animals. All animals will have a name, and they will all speak by saying their name and making a species-specific sound:
class Animal { constructor(name) { this.name = name } speak() { return `${this.name} says ${this.sound()}` } } class Cow extends Animal { sound() { return 'moooo' } } class Horse extends Animal { sound() { return 'neigh' } gallop() { return 'galloping woohoo' } } class Sheep extends Animal { sound() { return 'baaaa' } } const h = new Horse('CJ') for (const a of [h, new Cow('Bessie'), new Sheep('Little Lamb')]) { console.log(a.speak()) }
What does this show us?
extends
to set up IS-A relationships. A horse IS-A animal; a cow IS-A animal; a sheep IS-A animal. This is inheritance.
speak
is defined in the superclass Animal
.
sound
is defined in each subclass.
sound
. But which of the three sound methods is called? We don’t know until the program runs. Since the one line could refer to multiple implementations, we call this behavior polymorphism. It is also the “late binding” we mentioned above. In “early binding” we would have to commit to calling a particular sound method before the code ever runs. JavaScript does late binding.
But where is the encapsulation?
rename
for animals, or expand
for circles: these methods would cause the animal’s name or the circle’s radius to change as a side effect. But the outside world would never know what exactly was going on inside the object.
c.radius = 10.0
. Everyone knows circles have a radius
property, and everyone can whack it.
In addition to object-oriented programming, you can do object-oriented modeling. This is where to think hard about what are the right classes (and relevant methods, etc.) that describe your world. With a good model, the programming should come easy. (This is NOT to say that experimentation is not worthwhile. Sometimes you have to code a bit to truly understand what you need to build.)
So let’s say we need a game where our player is being chased by multiple enemies. We direct the player by moving our (mouse) cursor; the player moves toward the cursor and the enemies move toward the player. Try it:
Thinking in terms of objects with properties and behavior, we note:
The big deal in modeling.
When coming up with a family of classes, we look for those properties and behaviors that are the same among classes in the family and those that are different.
Here’s a first pass at hacking these ideas together:
// This is a hacked together foundation of a chasing game, used to start // a discussion of object oriented programming. The game contains several // design flaws, which will discussed in a classroom setting. function setup() { game.initialize() } function draw() { game.update() } function mouseMoved() { game.mouseMoved() } class Field { constructor(width, height, color) { Object.assign(this, { width, height, color }) } clear() { background(this.color) } clamp(x, y) { return { x: constrain(x, 0, this.width), y: constrain(y, 0, this.height) } } } class Agent { constructor(x, y, speed, target) { Object.assign(this, { x, y, speed, target }) } move(field) { const [dx, dy] = [this.target.x - this.x, this.target.y - this.y] const distance = Math.hypot(dx, dy) if (distance > 1) { const step = this.speed / distance Object.assign(this, field.clamp(this.x + (step * dx), this.y + (step * dy))) } } } class Player extends Agent { draw() { fill('blue') circle(this.x, this.y, 10) } } class Enemy extends Agent { draw() { fill('rgba(255, 50, 50, 0.5)') circle(this.x, this.y, 20, ) } } const game = { initialize() { createCanvas(400, 400) this.field = new Field(width, height, 'lightgreen') this.mouse = { x: 0, y: 0 } this.player = new Player(20, 20, 2.5, this.mouse) this.enemies = [ new Enemy(4, 5, 2, this.player), new Enemy(94, 95, 1.5, this.player), new Enemy(400, 503, 1.8, this.player), ] }, mouseMoved() { Object.assign(this.mouse, { x: mouseX, y: mouseY }) }, update() { this.field.clear() for (let agent of [this.player, ...this.enemies]) { agent.move(this.field) agent.draw() } } }
We saw examples that used classes, with subclassing for inheritance and polymorphism through dispatching on a method receiver. But there are other ways to structure systems. Some people call them OOP; others say not so fast. But they solve the same problems of designing software with things and behvior. Here are three concepts you should know.
Using classes for inheritance is called classical inheritance. Using prototypes gives you prototypal inheritance. Let’s try the animals example with prototypes:
function Animal(name) { this.name = name; } Animal.prototype.speak = function () { return `${this.name} says ${this.sound()}`; }; function Cow(name) { Animal.call(this, name); } Object.setPrototypeOf(Cow.prototype, Animal.prototype); Cow.prototype.sound = function () { return 'moooo'; }; function Horse(name) { Animal.call(this, name); } Object.setPrototypeOf(Horse.prototype, Animal.prototype); Horse.prototype.sound = function () { return 'neigh'; }; function Sheep(name) { Animal.call(this, name); } Object.setPrototypeOf(Sheep.prototype, Animal.prototype); Sheep.prototype.sound = function () { return 'baaaa'; }; const animals = [new Horse('CJ'), new Cow('Bessie'), new Sheep('Little Lamb')]; animals.forEach(a => console.log(a.speak())); console.log(animals[Math.floor(Math.random() * animals.length)].sound());
Here’s how things look:
If you like to get deep into JavaScript, note how the following relationships follow from the diagram:
h.name === 'CJ' h.constructor === Horse h instanceof Horse h instanceof Animal h.sound() === 'neigh') h.speak() === 'CJ says neigh' Object.getPrototypeOf(h) === Horse.prototype Horse.prototype.isPrototypeOf(h) h.hasOwnProperty('name') !h.hasOwnProperty('sound')
This is a rather ugly example, because we have IS-A relationships which look a lot better with the class syntax. Then again, resist the tempatation to overuse hierarchies with IS-A relationships; they don’t really show up too often. (But when they do, they are useful.)
Rather than performing polymorphic dispatch on the method receiver, some languages dispatch on the types of all of the methods’ arguments. These methods are called multimethods. JavaScript doesn’t support multimethods, but you can find them in Julia and Clojure. If you are interested, see:
You can get all the benifits of OOP by using protocols instead of classes, prototypes, or multimethods. Basically, protocols are bundles of properties and behaviors that can be “adopted” by objects. The language Swift bills itself as a protocol-oriented language, rather than an object-oriented language. To lern about protocols, do a web search for “protocol oriented programming” and have fun exploring. Also here are some code examples: