An Introduction to Object-Oriented Programming and Abstraction

We declared types when working with cellular automata

We have already started defining our own types that allow us to abstract what the computer is doing on a lower level. Recall from our work in Chapter 3 that when we worked with cellular automata like the Game of Life or Langton’s loops (replicated below), we thought of a cellular automaton in two ways.

  1. As a “game board”, an abstraction that is helpful for us humans.
  2. As a two-dimensional array stored within a computer of either boolean values (for two-state automata like the Game of Life) or integers (for multi-state automata like Langton’s loops).

Abstracting shapes

To help us transition toward object-oriented programming, we will imagine that we are producing an early version of an application for making slide decks like PowerPoint, and we want to find a simple abstract representation of different shapes such as rectangles or circles (shown below).

Exercise: What is the smallest amount of information that your computer needs to represent a rectangle in 2-D space? What is the smallest amount of information required to represent a circle? (Don’t think just yet about the color of the circle or the thickness of its boundary, but rather only its position.)

Our old friend Euclid, who gave us the world’s first nontrivial algorithm for computing a GCD in Chapter 0, proved much of the foundation of geometry in his Elements using only a compass and a straightedge. And you may or may not recall from the horrors of your geometry class that we can construct any circle that we like using a compass, which fixes the center of the circle, and draws the arc of all points that are a fixed distance from this center. In other words, the circle is defined as the set of all points that are a fixed distance from a given center.

Because of this definition, we could imagine defining our own Circle type constituting just three pieces of information, all of which can be represented as float (i.e., floating-point number) variables: the x-coordinate of the circle’s center, the y-coordinate of the circle’s center, and the circle’s radius.

We could therefore represent a Circle object as an array of floating-point numbers of length 3, but this approach presents a couple of issues. First, the order that the circle’s three defining attributes should take within the array representing that circle is unclear. Furthermore, once we establish that order, we will need to be consistent with it throughout our code, and if we ever access c[0] for a given Circle variable c, then the user must remember which attribute c[0] represents.

Second, storing the attributes of a Circle within an array is a brittle design choice. When we later update our software application and need to add an additional attribute to a Circle, say its fill color, or its edge thickness, then we will need to greatly change our implementation by expanding the length of the array. And if the eventual attributes of a Circle are not all floating-point numbers, then we will have an annoying array whose values have differing types.

Instead, we will think of a Circle as an object, where each instance of the object has three variables associated with it that we call fields:

  1. x1: the x-coordinate of the circle’s center;
  2. y1: the y-coordinate of the circle’s center;
  3. radius: the circle’s radius.

We therefore propose the following language-neutral declaration of a Circle type.

type Circle
    x1 float
    y1 float
    radius float

As for rectangles, we might use the following four fields.

  1. x1: the x-coordinate of one point (say, the center or upper left corner);
  2. y1: the y-coordinate of that same point;
  3. width: the rectangle’s width;
  4. height: the rectangle’s height.

These fields imply the following declaration of a Rectangle type.

type Rectangle
    x1 float
    y1 float
    width float
    height float

Objects are nothing new

Before continuing, we point out that we have already started working with objects when we needed to represent a virtual “canvas”, and the above representation is already how we conceptualize rectangles on such a canvas.

For example, in Go, we introduced graphics via a “canvas” object contained within a canvas.go file that contained the following type declaration. Explanations of each field are shown via a comment at the end of the line.

type Canvas struct {
    gc     *draw2d.ImageGraphicContext   // a "pen" object
    img    image.Image                   // an "image" object
    width  int                           // the canvas width, in pixels
    height int                           // the canvas height, in pixels
}

Furthermore, in Python, our canvas was a pygame.Surface object, and we encountered the pygame.draw.rect() when introducing graphics. This function takes the following parameters as input:

  • a pygame.Surface object representing the canvas;
  • a tuple (i.e., array) of length 3 (r, g, b) representing the color of the rectangle in RGB format, e.g. (255, 0, 0) for red.
  • a tuple of length 4 (x, y, width, height) specifying the x- and y-coordinates of the rectangle’s top left corner, along with the rectangle’s width and height, in pixels.

Allowing for rectangles to rotate

The astute reader may have noticed that our rectangle suffers from a critical flaw: our representation of a rectangle can only represent upright rectangles!

STOP: How would you represent an arbitrary rectangle in 2-D space?

We present two approaches for how we could improve our Rectangle type. First, we could add a rotation field that would store the angle of rotation of the figure (e.g., between 0 and 2π radians). The adjusted Rectangle type declaration is below.

type Rectangle
	x1 float
	y1 float
	width float
	height float
	rotation float

Second, we could instead specify the rectangle as containing four points: (x1, y1), (x2, y2), (x3, y3), and (x4, y4). The following representation contains these eight fields.

type Rectangle
	x1 float
	y1 float
	x2 float
	y2 float
	x3 float
	y3 float
	x4 float
	y4 float

This representation is a little unwieldy because we are abstracting the rectangle not as eight floating-point numbers, but as four points. Instead, a Rectangle object will now have four OrderedPair fields, and an OrderedPair object in turn contains two floating-point numbers as its fields. Top-down programming applies to object design too!

type Rectangle
    p1 OrderedPair
    p2 OrderedPair
    p3 OrderedPair
    p4 OrderedPair

type OrderedPair
    x float
    y float
STOP: We now have two representations of an arbitrary rectangle. Which do you prefer?

One might prefer the first abstraction of a rectangle, in which we introduced a rotation field, since it requires only five total pieces of information in order to store the shape, compared to eight for the second abstraction.

This focus on economy is reasonable, but it misses a larger reason to reject the second representation, which is that many collections of four points do not form the corners of a rectangle! The figure below shows one such collection.

A collection of four points that do not form the corners of a rectangle.

This example may be simple, but it exemplifies the need for care when abstracting even seemingly simple objects so that we don’t create problems later for the user.

More generally, the central premise of abstraction is to reduce an object to the critical attributes that define it, and which distinguish it from other types of objects. After all, whereas our brains envision an abstract concept of a rectangle, a computer does not (yet) possess that intelligence and sees only a type that is associated with a collection of fields.

Accessing the fields of an object

As we can declare variables that have type integer or string, once we have created an object type, we can declare objects having that type. We will implement this idea in this chapter’s code alongs.

We also can pass an object as input to a function or return it as output. For example, once we know the width and height of a rectangle, or the radius of a circle, we can immediately infer the shape’s area. More generally, because the fields of an object abstract that object, we can infer properties of the object beyond just its fields. The following two Area() functions take as input, respectively, a Rectangle object r and a Circle object c; they show the generally accepted practice of accessing the field of a given object using a period, followed by the name of the field. The second function uses pi to denote the mathematical constant π (3.14159…), which can typically be accessed from a language’s math package.

Area(r)
    return r.width * r.height

Area(c)
    return c.radius * c.radius * pi
Exercise: Write two pseudocode functions Perimeter() that take a Rectangle object and a Circle object as input, respectively, and return each of their perimeters.

Just as we can pass an object as input to a function, we can return the object as the output of a function. For example, say that we want to move, or translate, a shape from one position to another, as shown in the figure below.

Translating shapes on a canvas.

We can think of translating a shape as moving it a units in the x-direction and b units in the y-direction, where a and b can be positive or negative depending on whether we are moving right or left, or up or down. As a result, when writing our Translate() functions, we simply need to take a and b as inputs in addition to the shape; after updating the shape, we will return it. (We will see an even better way to translate shapes in this chapter’s code alongs.)

Translate(r, a, b)
    r.x1 = r.x1 + a
    r.y1 = r.y1 + b
    return r

Translate(c, a, b)
    c.x1 = c.x1 + a
    c.y1 = c.y1 + b
    return c
Note: You may have noticed that these functions, apart from taking a Circle vs. a Rectangle as input, are identical. This observation hints at a larger principle of object-oriented programming called polymorphism, in which we treat both the Circle and Rectangle objects as instances of a more general Shape object, which can be translated in the same way regardless of the type of shape it represents.
Exercise: Write two functions called Scale() that scale the size of a Rectangle or a Circle by an input factor f. (As a result, the area of the shape will increase by the square of f.)

Looking ahead

In the next lesson, we will extend what we have learned about abstraction to plan objects for our gravity simulator. Before moving on, we give you the chance to think about this question on your own.

Exercise: What object types do we need to build a gravity simulator?  What fields should each one have? What functions might be useful to have for these objects? 
Page Contents
Scroll to Top