Designing and Writing Classes

What is a class? Why are classes important? How do you go about designing and writing classes?

Purpose

Software systems manipulate different kinds of things like accounts, calendars, cards, contacts, rational numbers, dates, windows, animals, buildings, countries, players, toolbars, menus, songs, artists, and play lists. The behavior of these things—in terms of what they can and cannot do, and what we can and cannot say about them—gives rise to the notion of types.

In many programming languages, there’s a related notion of class. Every object, in these languages, has a unique class, although it many have several types. The class defines a structure, or implementation, for the instances of the class. A type, on the other hand, refers only to behavior.

A class defines a set of properties and operations for its instances, and may include a bunch of metadata, too. Here is an example of a polygon class, written in the UML:

polygonuml.png

What is the UML, you may ask?

A good resource is Scott Ambler's, check it out.
Please Note: Classes are unrelated to Object-Oriented Programming

You can do object-oriented programming very well without classes. You have even have classes without object-orientation. People often get them confused, since most explanations of OOP feature classes prominiently, but again, this not need be the case.

A First Example

Let’s implement our point and polygon classes in a few languages.

Java

Here are the point and polygon classes in Java:

Point.java
/**
 * An immutable point class.
 */
public class Point {
    public static final Point ORIGIN = new Point(0, 0);
    private double x;
    private double y;
    public Point(double x, double y) {this.x = x; this.y = y;}
    public double getX() {return x;}
    public double getY() {return y;}
    public static Point mid(Point p, Point q) {
        return new Point((p.x + q.x)/2.0, (p.y + q.y)/2.0);
    }
    public double distanceFromOrigin() {return Math.sqrt(x*x + y*y);}
    public Point reflectionAboutOrigin() {return new Point(-x, -y);}
}
Polygon.java
import java.util.Arrays;
import java.util.ArrayList;

/**
 * A mutable polygon containing at least three vertices, where the vertices are
 * assumed to be listed in counter-clockwise order.
 *
 * Why are you mutating a polygon, anyway? Oh that's right, this class is just a
 * teaching example.
 */
public class Polygon {

    private ArrayList<Point> vertices;

    public Polygon(Point... vertices) {
        if (vertices.length < 3) {
            throw new IllegalArgumentException("Need at least 3 vertices");
        }
        this.vertices = new ArrayList<>(Arrays.asList(vertices));
    }

    public double getPerimeter() {
        double result = 0.0;
        for (int i = 0; i < vertices.size(); i++) {
            Point p = vertices.get(i), q = vertices.get((i + 1) % vertices.size());
            result += Math.hypot(q.getX() - p.getX(), q.getY() - p.getY());
        }
        return result;
    }

    public double getArea() {
        double result = 0.0;
        for (int i = 0; i < vertices.size(); i++) {
            Point p = vertices.get(i), q = vertices.get((i + 1) % vertices.size());
            result += p.getX() * q.getY() - q.getX() * p.getY();
        }
        return result / 2.0;
    }

    public Point[] getVertices() {
        return vertices.toArray(new Point[0]);
    }

    public void addVertex(int index, double x, double y) {
        if (index < 0 || index > vertices.size()) {
            throw new IllegalArgumentException("Cannot add at index: " + index);
        }
        vertices.add(index, new Point(x, y));
    }

    public void updateVertex(int index, double x, double y) {
        if (index < 0 || index >= vertices.size()) {
            throw new IllegalArgumentException("Cannot update at index: " + index);
        }
        vertices.set(index, new Point(x, y));
    }

    public void removeVertex(int index) {
        if (index < 0 || index >= vertices.size()) {
            throw new IllegalArgumentException("Cannot remove at index: " + index);
        }
        if (vertices.size() == 3) {
            throw new IllegalStateException("Removal would make the polygon degenerate");
        }
        vertices.remove(index);
    }
}

Kotlin

Here are the same classes in Kotlin:

TODO

Ruby

To do these classes the Ruby way, we’re going to do some slight touching up of the names. Ruby uses snake_case and kind of frowns on using get and set in naming.

polygons.rb
class Point
  attr_reader :x, :y
  def initialize(x, y); @x = x; @y = y; end
  @@origin = Point.new(0, 0)
  def self.ORIGIN; @@origin; end
  def self.mid(p, q); Point.new((p.x + q.x) / 2, (p.y + q.y) / 2.0) end
  def distance_from_origin; Math.sqrt(@x ** 2 + @y ** 2); end
  def reflection_about_origin; Point.new(-@x, -@y); end
end

class Polygon
  def initialize(*points)
    raise 'Need at least three points' if points.length < 3
    @points = Array.new(points)
  end

  def perimeter
    result = 0
    @points.each_with_index do |p, i|
      q = @points[(i + 1) % @points.length]
      result += Math.hypot(p.x - q.x, p.y - q.y)
    end
    result
  end

  def area
    result = 0
    @points.each_with_index do |p, i|
      q = @points[(i + 1) % @points.length]
      result += p.x * q.y - q.x * p.y
    end
    result / 2
  end

  def vertices
    @points.copy
  end

  def add_vertex(index, x, y)
    raise "Cannot add at #{index}" if index < 0 or index > @points.length
    @points.insert(index, Point.new(x, y))
  end

  def update_vertex(index, x, y)
    raise "Cannot update at #{index}" if index < 0 or index >= @points.length
    @points[index] = Point.new(x, y)
  end

  def remove_vertex(index)
    raise "Cannot remove at #{index}" if index < 0 or index >= @points.length
    @points.delete_at(index)
  end
end

JavaScript

Interesting fact: in Java, Kotlin, and Ruby, classes are actually classes. In JavaScript we use the word class but what we are creating is a function. But it looks like a class, and sort of acts like a class, so....

polygons.js
class Point {
  constructor(x, y) { Object.assign(this, { x, y }); Object.freeze(this); }
  get distanceFromOrigin() { return Math.hypot(this.x, this.y); }
  get reflectionAboutOrigin() { return new Point(-this.x, -this.y); }
}

Point.ORIGIN = new Point(0, 0);
Point.mid = (p, q) => new Point((p.x + q.x) / 2, (p.y + q.y) / 2.0);

class Polygon {
  constructor(...points) {
    if (points.length < 3) {
      throw new Error('Need at least three points');
    }
    this.points = points.slice();
    Object.freeze(this);
  }

  get perimeter() {
    let result = 0;
    for (let i = 0; i < this.points.length; i += 1) {
      const [p, q] = [this.points[i], this.points[(i + 1) % this.points.length]];
      result += Math.hypot(p.x - q.x, p.y - q.y);
    }
    return result;
  }

  get area() {
    let result = 0;
    for (let i = 0; i < this.points.length; i += 1) {
      const [p, q] = [this.points[i], this.points[(i + 1) % this.points.length]];
      result += (p.x * q.y) - (q.x * p.y);
    }
    return result / 2;
  }

  get vertices() {
    return this.points.slice();
  }

  addVertex(index, x, y) {
    if (index < 0 || index > this.points.length) {
      throw new Error(`Cannot add at index: ${index}`);
    }
    this.points.splice(index, 0, new Point(x, y));
  }

  updateVertex(index, x, y) {
    if (index < 0 || index >= this.points.length) {
      throw new Error(`Cannot update at index: ${index}`);
    }
    this.points[index] = new Point(x, y);
  }

  removeVertex(index) {
    if (index < 0 || index >= this.points.length) {
      throw new Error(`Cannot remove at index: ${index}`);
    }
    if (this.points.length === 3) {
      throw new Error('Removal would make this polygon degenerate');
    }
    this.points.splice(index, 1);
  }
}

Aspects of Classes

When we design classes we pay attention to three aspects:

SPECIFICATION
The protocol, interface, "contract", or behavior. Given primarily by constructor and method signatures.
REPRESENTATION
(should be hidden) The low-level structural details. Given by the field declarations.
IMPLEMENTATION
(should be hiden) The bodies of the constructors and methods.
Exercise: Identify the specification, representation, and implementation in the Point class above.

Structure of Classes

The most visible structural components of classes (in most languages) are:

Exercise: Identify and describe the properties and operations of the Point class above.

Some Design Considerations

A few tips for class designers follow.

The most important thing about operations

Think: every operation should succeed or fail. If it succeeds, great; if it fails, throw an exception or return an optional. Do not send back failure codes only to clients if you can help it. Doing so puts the burden of checking on the client and many times the client programmer forgets the check.

Exercise: Describe various ways implement an operation to get the integer value of a string, knowing that an arbitrary string might not look like a integer at all. Talk about the consequences of each of the design alternatives.

Hide the Representation

Hiding the representation is a really good idea, for these four primary reasons:

In practice this means:

Keep Interfaces Small

Don't stuff the class full of too many fields or too many methods. For example if your Customer class has fields called street, city, state and zip as well as name and account number, you should introduce a new class called Address. If you have way too many methods, you may need to think about factoring the responsibilities of the class into two or more classes.

Consider Immutability

Immutable objects (objects whose values never change after they have been created) can be awesome. They are:

Exercise: Read the section on immutability in Bloch's text.
Exercise: The point class above is immutable. Write an immutable polygon class using this point class.

Consider Factory Methods

Somtimes you'll want to hide constructors and instead expose methods that return new objects (called static factory methods). Advantages:

Exercise: Read the section on static factory methods in Bloch's text.
Exercise: (Somewhat contrived) Rewrite the point class above to use a private constructor and add a factory method. Add a caching mechanism so that separate point objects with the same value are never created.

Read a Classic Book

If you are interested in classes in popular entrprise languages, read Effective Java by Joshua Bloch, and/or Effective C++ by Scott Meyers.