Introduction to Object Oriented Programming

Many of you are already familiar with the ideas of Object-Oriented Programming (OOP), but some are not. Now is a good time to learn.

The goal of OOP is to help in dealing with complexity and scale. That is, as programs become bigger, and are programmed by multiple teams of people instead of by a solitary wizard, it becomes important to organize the data and code. Back in the olden days, say the 1950s and 1960s, programs in FORTRAN would have a big common block of global variables, mostly arrays, that all the code could access and modify. That doesn't scale well. (If this reminds you of our discussion of modularity, it should. OOP is partly about modularity.)

Terminology Note

It is an unfortunate fact that the objects we are discussing in OOP are related to but different from the JavaScript object literals that we learned about earlier in the course. For example:

var harry = {name: 'Harry Potter',
             hair: 'black',
             house: 'Gryffindor'};

What kind of data is stored in the harry variable? In JavaScript, this is called an object. (In Python it would be called a dictionary; in Java, it might be a HashMap. If you don't know those other languages, don't worry about it. This paragraph is just to connect to other knowledge if you happen to have it.)

However, this object is not the kind of object we are discussing today. When I need to specify these simpler objects, I'll put the word "dictionaries" after it in parentheses.

General Idea

In OOP, an object is a kind of value (data structure) that can have two important characteristics:

State: This is information or data describing the object. For example, a ball object could have a state like: being color red, having a radius of 12 inches, and having polka dots. Or a list could have a set of things that are in the list. In fact, each of the list items could be an object itself.

Behavior: This is the information describing the actions of the object. For instance, a ball object can bounce. A list object might have ways to add and remove elements from the list.

In order to create specific objects, we need to describe a class of objects and then create some instances of the class. (Note that these are different from the HTML classes!) Imagine a class as a general kind of thing, and objects are the specific instances of that kind. Here are some examples of classes and objects.

  • Class: Ball
    • Objects:
    • b1: Red, has a radius of 12, has polka dots
    • b2: Blue, has a radius of 5, does not have polka dots
  • Class: Wellesley College Student
    • Objects:
    • stu1: Beatrice, Class of 2019, MAS major, says "hello!"
    • stu2: Jackie, Class of 2018, biology major, runs to lab
    • stu3: Kate, Class of 2020, English major, sings

Programming Classes and Objects

In discussing how to implement Object-Oriented Programming in JavaScript, we have a choice. In 2015, the JavaScript language was expanded to include specialized syntax to support classes and objects. (Technically, this was ECMAscript6.) However, you should know that OOP was a part of JS before 2015; it just had a different syntax. In fact, the semantics (the meaning of things) is the same as before 2015 ; it just had a new syntax.

We will focus on the new syntax because it is simpler and easier to understand. In fact, that's its purpose: to be a better syntax for an existing implementation. This is sometimes called syntactic sugar. So, for that reason, we'll introduce the new syntax first. We'll save the older syntax for an appendix.

Defining Classes

In presenting this syntax, I've followed the Mozilla Developer's Network web docs on Classes, but that document isn't intended for novices. Nevertheless, you're welcome to read it if you want more depth and rigor.

Let's describe our ball objects using the class syntax:

class Ball {
    // instance variables with default values
    // the values are not necessary
    // because we initialize all of the in the constructor
    ballColor = 'white';
    ballRadius = 1;
    hasDots = false;
    
    constructor(color, radius, hasPolkaDots) {
        this.ballColor = color;
        this.ballRadius = radius;
        this.hasDots = hasPolkaDots;
    }
    getColor() {
        return this.ballColor;
    }
    setColor(color) {
        this.ballColor = color;
    }
    // returns a string describing the ball
    describe() {
        let size = this.ballRadius + " inch ";
        let dots = this.hasDots ? " polka dot " : " ";
        return "a " + size + this.ballColor + dots + " ball";
    }
    bounce() {
        alert(this.describe() + " bounces");
    }
}

Some observations:

  • a class has a special function called a constructor which initializes the instance variables of each new object (instance of the class). The constructor is automatically invoked whenever you create a new instance.
  • we can optionally declare instance variables before the methods. Here we have three instance variables, which we give default values to, mostly as examples.
  • a class can have any number of methods. Here we have four: getColor, setColor, describe and bounce.
  • the bounce method uses the describe method to get a string describing the ball. It's okay for one method to call another.
  • the ? in the describe method is the JS ternary operator; it's a short-cut for an if statement. Not important.

Using Objects

Let's see how our code would use this class definition:

// examples of creating two instances of the class
var b1 = new Ball("Red", 12, true);
var b2 = new Ball("Blue", 5, false);

// examples of invoking a method
console.log(b1.getColor()); // "Red"

b1.setColor("Green");

console.log(b1.getColor()); // "Green"


// example of retrieving an instance variable
// it would be better to define a method to do this
console.log(b2.ballRadius); // 5

console.log(b2.describe()); // "a 5 inch Blue ball"

b1.bounce();  // Booiiiiinng. Ball is bouncing
b2.bounce();  // Booiiiiinng. Ball is bouncing

More observations:

  • a method is invoked by giving the object (or a variable containing the object) a dot and the name of the method, along with parentheses around any arguments to the method. As with function invocation, there are always parentheses, even if there are no arguments to the method. Indeed, that's because a method is just a special kind of function.
  • we use the new operator when creating an instance of a class. The new operator creates the new, empty, object, binds it to this and then invokes the constructor. The constructor can then initialize the object by using the magic keyword this.
  • similarly, methods can refer to the object using this in their code. For a method invocation like b1.color(), the this keyword refers to b1, while in b2.describe(), the this keyword refers to b2.
  • The name of the constructor function is the name of the class (here it's Ball)
  • By convention, class names and therefore constructor functions are named with an initial capital letter, so Ball not ball.

You can see that code in action using the ball modern demo. Note that the web page will be blank. View the page source to see the JS code, and open the JavaScript console to see what was printed to the console.

Type in the names of the two variables, b1 and b2 to see how they are displayed.

There are other features of the new syntax that we won't go into now, but we may look at later in the course.

The this Keyword

Constructors and methods always have access to the object via the keyword this. That keyword looks and acts a lot like a variable, but it's a bit more slippery than that. Its value changes whenever a function or method is invoked: Inside a constructor, this refers to the newly created object, and inside a method like getColor or setColor, this refers to the object that the method is invoked on. For example, in the case of b1.getColor(), the this keyword inside the getColor method refers to b1.

Furthermore, you can't have closures over the value of this. We'll talk more about this issue later, when it's necessary.

But don't let that complexity confuse you. If you've done OOP in Java (such as in CS 230), this is pretty much the same as in Java: it's a keyword that means the current object. If you've done OOP in Python, this is essentially the same as self. All OOP languages have a way to refer to the current object.

Exercises

Try out some of the exercises below.

  1. Below is the Ball code in JSfiddle to explore. Open up JSfiddle and the console, and try creating new objects, accessing instance variables, and calling methods. Afterwards, try creating new instance variables and methods to your liking.
  2. Create your own Person class. Add instance variables and methods of your choices.

OOP in CoffeeRun

Our version of CoffeeRun uses the new syntax for OOP. CoffeeRun will define several classes. Let's start by discussing the DataStore class, but I'll put off looking at the implementation until later in this reading. For now, it's sufficient to know that the data we will be putting in the data store will be coffee orders. We will look up each coffee order by the email address of the person who placed the order.

Key-Value Databases

There are many kinds of database in Computer Science. A very common and useful type is a key-value database. With a key-value database, the value can be any collection of data, and the key is some unique identifier. For example, the college keeps track of a lot of information about you (name, home address, phone number, high school info, etc.). That data can all be looked up by a unique identifier, like your B-number (though now that some of you have C-numbers, we'll have to come up with another word).

These kinds of databases are very common in Computer Science, but we won't say much more now.

In-Memory Key-Value Databases

If these key-value databases remind you of Python dictionaries, Java HashMaps and the JavaScript objects (dictionaries) we learned earlier in the course (see terminology note), that's exactly right. All of those data structures are in-memory key-value databases.

Therefore, in implementing our DataStore class, we can use JavaScript objects (dictionaries) to store the data. However, we will wrap them up inside OOP syntax to hide this implementation fact. Why hide the implementation? Hiding the implementation allows us to change it later, either by changing this code, or providing an alternative class. More on this concept later.

For CoffeeRun, the values will be collections of information about the coffee order (size, flavor, etc). The keys will be the email address of the person ordering the coffee.

Here's how to use JavaScript objects (dictionaries) as a key-value database:

var house = {};
house['cho'] = 'ravenclaw';
house['draco'] = 'slytherin';
house['cedric'] = 'hufflepuff';
console.log(house['cho']);

Or this:

var heads = {};
heads['gryffindor'] = 'McGonnagall';
heads['slytherin'] = 'Snape';

Here's an example of a coffee order:

var key1 = 'scott@wellesley.edu';
var order1 = {emailAddress: key1,
              coffee: "black with milk and an embarrassing amount of sugar",
              size: "short",
              flavor: "mocha",
              strength: 38
              };

Here's how we could store the coffee order in a database:

var coffee_database = {}
coffee_database[key1] = order1;

Notice that we use the square bracket notation for storing into the database, rather than the dot notation. Here, the square bracket is necessary because we won't know the keys (email addresses) when we're writing the code. Users will enter their email address when they fill out the CoffeeRun form, and so that information will be in a variable like key1.

Look for these implementation ideas in our implementation of the DataStore class. But, why not just use these ideas directly, instead of wrapping them up in OOP? The answer we mentioned above (implementation/information hiding) can also be described as an abstraction barrier. We provide an API to the database implementation.

APIs

The notion of an API or an Abstraction barrier is a common and important idea, but it's, well, very abstract and so it can be hard to understand at first. Let's start with some examples. Here are my favorite metaphors:

  • Stereo components: you can upgrade your speakers/headphones without getting all new equipment, because they support a standard interface.
  • You can connect to any of hundreds of external monitors because the HDMI interface (formerly VGA) is standardized.
  • You can charge your e-vehicle, regardless of make and model (unless it's a Tesla) at any charging point except Tesla's because they all use a standard interface (except Tesla).

The point is that one device can talk to another because they understand each other.

An API is the software equivalent: one chunk of code can talk to another because they understand each other: they use and supply the same variables, functions, and methods with the same meaning. For example, if we load some database software, we should be able to get and store data without having to know the internal details of how those operations are implemented. We just need to know how to use them: what are their names, arguments, return values, etc.

(Wouldn't it be nice not to have to worry about whether the connector is USB-A, USB-C, lighting, thunderbolt, ...)

Abstraction barriers give the implementation freedom and flexibility. Here's a picture of the idea (picture by Ross Anderson):

A client and two Database Implementations
The code on the left (the client) needs a database. On the right, there are two different databases to choose from, and the client could use either one, because they implement the same API.

In this version of CoffeeRun, we want a key-value database and we decided to use objects, but what if we wanted to change our minds? What if we wanted to switch to Oracle NoSQL or LMDB or ... The code in the previous section would be a nightmare to update. But if we hide the database behind an abstraction barrier, we can isolate that implementation decision and allow ourselves the freedom to change it, should we choose to.

Our API will consist of two methods:

  • put(key,val) stores a key, value pair
  • get(key) returns the stored value

Here's one way, using the new class syntax:

class KeyValueDB {
    constructor () {
        this.db = {};  // empty JS object (dictionary)
    }
    get(key) {
        return this.db[key];
    }
    put(key,val) {
        this.db[key] = val;
    }
}

Again, notice the square bracket notation to put things in the database and to get them out.

Finally, let's see an example of it in use:

// make a new database of heads-of-house
var heads = new KeyValueDB();

// store two values
heads.put('gryffindor', 'McGonnagall');
heads.put('slytherin','Snape');

CoffeeRun DataStore Class

Finally, we're ready to look at the code for the DataStore class

/* Rewritten by Scott, Summer 2020. This implements a class DataStore
 * that has an API for a key/value database. 
 *
 * This version of the API uses return values.
*/

class DataStore {
    // instance variables
    data = {};

    constructor() {
        console.log('running the DataStore function')
        this.data = {};
    }

    add (key, val) {
        this.data[key] = val;
    }

    get (key) {
        return this.data[key];
    }

    getAll () {
        return this.data;
    }

    remove (key) {
        delete this.data[key];
    }
}

var ex1 = new DataStore();
ex1.add('harry', {name: 'Harry Potter',
                  hair: 'black',
                  house: 'Gryffindor'})
ex1.add('ron', {name: 'Ron Weasley',
                hair: 'red',
                house: 'Gryffindor'})
ex1.add('hermione', {name: 'Hermione Granger',
                     hair: 'brown',
                     house: 'Gryffindor'})

console.log('lookup harry', ex1.get('harry'));

function showDictionary(dic) {
    let keys = Object.keys(dic);
    keys.map(k => console.log(k, '=>', dic[k]));
}

console.log('all values before removing ron');
showDictionary(ex1.getAll());

ex1.remove('ron');

console.log('all values after removing ron');
showDictionary(ex1.getAll());

export { DataStore };

Let's discuss what this means, line by line.

  • line 7 defines our instance variable where we store the data.
  • line 11 sets the instance variable to an empty JS object (dictionary) when we create a new DataStore
  • line 14 is the add method which stores a key-value pair
  • line 18 is the get method, which retrieves the value corresponding to a key
  • line 22 gets and returns the whole database
  • line 26 is the remove method, which deletes a key-value pair, removing it from the JavaScript object (dictionary).

The file also has some example code, from line 31 to 52. You can see the console.log statements when you load the app:

CoffeeRun Basic

Finally, the last line is the export statement that matches the import statement in the main-module.js file.

Note that the file only exports the DataStore. It doesn't export the ex1 example or anything else. The example code is nicely isolated from the rest of our app. In a professional web development setting, tools called "tree shakers" can identify and remove this example code, since it's not used by the App.

The Truck Class

There's another class that we'll look at in this reading, though not in as much detail as DataStore. That's the Truck class. Recall that the Truck class stores information about a single truck. It has the following methods:

  • createOrder which adds an order to the database
  • deliverOrder which removes an order from the database
  • printOrders which prints all the pending orders to the console.

The App doesn't use the printOrders method, but it can be useful for debugging. It also demonstrates an issue with OOP that is important to know, so it's valuable for that reason. But that issue is tricky, so we'll leave it for another day.

Here's an excerpt from the Truck class definition. You can see that these methods don't do much, but if we did want to do more, later, we can easily add that functionality while keeping the API.

class Truck {
    // instance variables
    truckId = null;
    db = null;

    constructor (truckId, db) {
        this.truckId = truckId;
        this.db = db;
    }

    createOrder (order) {
        console.log('Adding order for ' + order.emailAddress);
        this.db.add(order.emailAddress, order);
    }

    deliverOrder (customerId) {
        console.log('Delivering order for ' + customerId);
        this.db.remove(customerId);
    }

    printOrders() {
        this.printOrders_arrow();
    }

} // end of Truck class

Note that the db instance variable will hold a database that this truck uses to store its data. The caller for the constructor supplies the database, rather than the truck creating its own. That technique allows for more flexibility and modularity of implementation: the caller can change the type of database without having to revise the Truck code. The this.db.add() and this.db.remove method invocations, above, are adding and removing from the database, using the DataStore methods we saw earlier.

Summary

  • OOP is very important in modern coding.
  • OOP helps to manage complexity and provides an abstraction barrier between the implementation and the client code.
  • OOP provides an API: application programmer interface.
  • In JavaScript, the class syntax surrounds:
    • some instance variables
    • a constructor
    • some methods
  • An object is created like this: var myObj = new Class(args) where Class is the name of the class and the arguments are passed to the constructor to initialize instance variables. The class name is capitalized.
  • A method is invoked like this: myObj.meth(args)
  • The implementation of a method can refer to the current object using the special keyword this

Appendix

Unless you're interested, you can skip this section on the older OOP syntax