Computational Literacies Lab

0002.l Object-oriented concepts

On this page
  1. Modules: dividing a program
  2. Classes and objects
  3. Attributes, methods, and constructors
  4. Encapsulation
  5. Inheritance
  6. Polymorphism

Object-oriented programming is easiest to understand as a way of organizing a program at several levels of granularity, each nested inside the one above. At the largest level, a program is divided into modules with separate responsibilities. Within a module, a class defines a kind of thing. From each class, a program creates many objects, and each object is made of attributes, which hold its data, and methods, which define what it can do. Three principles then govern the boundaries between these pieces. Encapsulation concerns the boundary around a single class, inheritance concerns the relationship between a class and a more general one, and polymorphism concerns how code can treat objects of different classes alike. This section moves down through the levels and then takes up the three principles in turn. Classes and objects were introduced as data structures in 0002.h, and this section looks at them as tools of design.

Modules: dividing a program#

The design techniques of 0002.k included decomposition, and modularization is decomposition applied to a whole program. It divides a program into separate units, called modules, each with a clear responsibility and an interface, the set of public functions through which other code uses it. The benefit is independence: each module can be developed and tested on its own.1 Suppose a school scheduling application has a Roster module that manages students, a Rooms module that manages rooms, and a Scheduler module that assigns students to rooms by calling the public functions of the other two. A programmer who needs to change how room capacity is stored can make and test the change entirely within Rooms, as long as its public functions keep behaving the same way. This is the interface and implementation distinction of 0002.e at the scale of program parts.

That independence holds only if modules respect one another's boundaries, and two rules follow. First, modules should interact through public functions, not through one another's internal helpers. If three other modules call a helper function that Rooms uses internally, then a change to that helper can break all three, because they have come to depend on a detail that was never meant to be shared. Second, one module should own each piece of data. If two modules each store a copy of a user's name, the copies have to be kept in sync and will eventually disagree, so it is better to make one module responsible for names and have the others ask it. A single-file game in which input handling, game rules, and drawing are tangled together, so that any change to drawing tends to break the rules, is the kind of program that benefits from being separated into modules with clear interfaces.

Classes and objects#

Within a module, a class is a template that defines attributes and methods for one kind of thing, such as a student, a timer, or a bank account. Each instance, or object, created from the class holds its own values for those attributes. As a result, a method call affects only the instance on which it is called.2

01 Counter a = Counter(5)
02 Counter b = Counter(10)
03 a.add(2)
04 output(a.total)
05 output(b.total)
>>> 7
>>> 10

Calling add() on a changes a.total and leaves b.total alone. A program that needs three independent timers, each with its own remaining time, should therefore create three Timer objects from one class. Some languages also allow a class attribute, a single value shared by all instances, such as Student.total_students. A shared value like that suits information about the class as a whole, such as a count of how many students exist, but not information about any one student.

Attributes, methods, and constructors#

Each object has three kinds of member, and each has a distinct job. Attributes store an object's data, such as a book's title or whether it has been checked out. Methods define its behavior, such as check_out(). A method can read the attributes of its own object without being passed them as arguments: after Circle c = Circle(2), the call c.area() uses the radius stored in c.2 A constructor sets an object's initial state when it is created, and it runs as part of the creation itself, as in Point p = Point(3, 4), which creates a point whose coordinates are 3 and 4. In Python, the constructor is the method named __init__. Without a constructor that sets every attribute, an object can start in an incomplete state. A Timer whose tick() method subtracts from seconds will fail the first time it runs if nothing ever gave seconds a value, and the direct fix is a constructor that sets seconds when each timer is created.

Encapsulation#

With the levels in place, the first principle concerns the boundary around a class. Encapsulation bundles data with the methods that operate on it and restricts direct access to the data, so that changes go through code that can validate them. Consider an Account class whose balance any code can set directly. If the bank forbids negative balances, nothing enforces the rule, and a bug anywhere in the program can break it. If balance can change only through methods such as deposit() and withdraw(), those methods can refuse invalid changes. In the same way, a Thermostat that should allow only temperatures from 50 to 90 can enforce that range in a set_temp() method that rejects other values, whereas a comment saying "keep between 50 and 90" can't enforce anything.

Encapsulation has a second benefit, which follows from the first. Because other code depends only on the methods, the internals can change without breaking it. A class that stores its values in an array behind add() and get() methods can switch to a map, and code that uses the class won't notice. Encapsulation is a design tool, which limits how parts of a program can depend on one another's data. It isn't a form of encryption or a defense against attackers.

Inheritance#

The second principle concerns relationships between classes. Inheritance lets a subclass reuse and extend the attributes and methods of a parent class, which places shared behavior in one location. If Car and Truck have identical start() and stop() methods and identical fuel attributes, and only load_cargo() is specific to trucks, a parent class Vehicle can hold the shared members, with both classes inheriting from it. A subclass instance has the parent's members as well as its own, so if Dog inherits from Animal, which has a name attribute and a describe() method, then Dog d = Dog("Rex") can call both d.describe() and Dog's own fetch(). Because shared behavior lives in one place, correcting a bug in a parent's method also corrects it for every subclass that inherits the method without overriding it.3

Inheritance should follow behavior and not only everyday categories. A subclass should preserve its parent's expected behavior, so that it can be used wherever the parent is expected. A square is a kind of rectangle in geometry, yet a Square class that inherits from Rectangle can break: a program that sets a rectangle's width expects the height to stay the same, and a square can't allow that and remain square. In a case like this, "is a" in ordinary language isn't enough reason to inherit.

Polymorphism#

The third principle concerns how code treats objects of different classes. Polymorphism lets objects of different classes respond to the same method call in their own ways. Often this happens through overriding, in which a subclass provides its own version of a method that its parent defines. If Employee has a pay() method that Manager and Intern override in different ways, then calling staff[i].pay() on an array of employees runs the version that belongs to each object's actual class.

01 Shape[] shapes = [Circle(2), Square(3)]
02 for (int i = 0; i < shapes.size[1]; i = i + 1)
03   shapes[i].draw()

Each object draws itself in its own way, and the loop needs no if statements that check what kind of shape it holds. The alternative is a long chain of if statements that tests each object's type before calling type-specific code, and every new type then requires editing every chain. Polymorphism moves the type-specific behavior into the classes themselves, so adding a new kind of shape means writing one new class. A simple classroom example is Dog and Cat classes that each define speak(), with a loop that calls speak() on each pet.

All of these ideas aim at programs that can be changed safely, by limiting how much each part has to know about the others. That makes them a natural bridge to the next section, which looks at programs as things that are built and changed in repeated cycles.

  • modularization: dividing a program into separate units with clear responsibilities and interfaces
  • interface: the public functions or methods through which other code uses a module or class
  • class: a template defining attributes and methods
  • instance (object): one object created from a class, holding its own values
  • class attribute: a value shared by all instances of a class
  • attribute: data stored in an object
  • method: behavior defined for an object
  • constructor: code that sets an object's initial state when it is created
  • encapsulation: bundling data with its methods and restricting direct access, so changes go through code that can validate them
  • inheritance: a subclass reusing and extending a parent class's attributes and methods
  • subclass: a class that inherits from a parent class
  • polymorphism: objects of different classes responding to the same method call in their own ways
  • overriding: a subclass providing its own version of a parent's method

Practice: practice questions for 0002.l

  1. MWC1, Retro: a game library, its modules, and working with agents. ↩

  2. MWC1, Dice: classes, instances, randomness, and repeated runs. ↩ ↩2

  3. MWC2, Riddle Server: views, models, fields, subclasses, and an API specification. ↩