State and Identity

An object has behavior (methods), state (fields), and identity (what it represents).

Value objects represent mathematical abstractions like numbers, strings, quantities, vectors, dates, money, and locations.

Reference objects represent entities, events, and processes in the application domain.

For value objects state and identity are the same. For example, the state of a Point2D object is its x- and y-coordinates. For example, the state of the Point2D object p representing the point (2.5, 3.0) is p.xc = 2.5 and p.yc = 3.0. Notice that if we change p.xc to 2.6, then we change the identity of p. It no longer represents the point (2.5, 3.0). For this reason we don't usually provide clients to change the state of a value object. We say that value objects are stateless.

On the other hand, an Account object represents a bank account in the banking domain. Its state is its balance, which changes each time the account holder withdraws or deposits money. We do want to allow clients to modify the balance of an account object, but want to prevent unwanted state changes (like a negative balance). We say that reference objects are stateful.

Example: Account in Java

A bank account is a classic example of a reference object. Its state is its balance. The withdraw and deposit methods allow users to modify the balance. Methods that modify the state of an object are called mutators.

public class Account {
  
   private Double balance;

   public Account(Double balance) {
      super();
      this.balance = balance;
   }

   public Double getBalance() {
      return balance;
   }

   public void setBalance(Double balance) {
      if (0 <= balance) this.balance = balance;
   }
}

Having multiple instances of the Account class represent the same bank account in the real world can lead to a disaster if the representatives disagree about the balance.

Example: Point in Java

A Point class such as one might find in a graphics library represents a point in some coordinate system:

class Point {
   private double xc, yc;
   public Point(double x, double y) {
      xc = x;
      yc = y;
   }
   public double getXC() { return xc; }
   public double getYC() { return yc; }
   public String toString() {
      return "(" + xc + ", " + yc + ")";
   }
   public boolean equals(Object other) {
      if (other == null) return false;
      Class otherClass = other.getClass();
      Class thisClass = this.getClass();
      if (!thisClass.equals(otherClass)) return false;
      Point otherPoint = (Point) other; // a safe cast
      return otherPoint.xc == this.xc && otherPoint.yc == this.yc;
   }
   public int hashCode() {
      return toString().hashCode();
   }
}

Notes

·       Points are value objects. The identity of a point is its state. Therefore, we declare these fields private and do not provide getter methods for them.

·       We provide a constructor that initializes both fields. This is the only way to assign values to these fields.

·       We provide getter methods for these fields.

·       We override the inherited toString method.

·       We also override the equals method to test for logical equality. The inherited method tests for literal equality. In other words, two instances of the class are equal only if they are literally the same object in memory. This implementation wouldn't work for a value object:

Point2D p1 = new Point2D(3, 4), p2 = new Point2D(3, 4);
p1.equals(p2); // = false!

·       We must also override hashCode. Otherwise, two points that are logically the same might have different hash codes. This can cause errors when searching maps:

cities.put(p1, "Chicago");
cities.get(p2); // = null

·       The hashCode method is used to search hash tables. It must obey the rule:

if a.equals(b), then a.hashCode() == b.hashCode()

·       It is desirable for objects to have evenly distributed hash codes. We can accomplish both by reducing the hash code of a point to the hash code of the point's string representation.