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
andbounce
. - the
bounce
method uses thedescribe
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 anif
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. Thenew
operator creates the new, empty, object, binds it tothis
and then invokes the constructor. The constructor can then initialize the object by using the magic keywordthis
. - similarly, methods can refer to the object using
this
in their code. For a method invocation likeb1.color()
, thethis
keyword refers tob1
, while inb2.describe()
, thethis
keyword refers tob2
. - 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
notball
.
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.
- 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.
- 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):
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 pairget(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:
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 databasedeliverOrder
which removes an order from the databaseprintOrders
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)
whereClass
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