Domain Modelling

Essential steps

  1. Identify Key Concepts
  2. Create a Model
  3. Define Attributes and Behaviour
  4. Refine the Model

Relating to Types and Objects

  • Concepts => Types
  • Instances => Objects
  • Attributes => Properties (fields)
  • Behaviour => Methods

Relating to an Algebra

Algebraic Structures

  • Types => Sets
  • Relationships => Functions
  • Operations
  • Laws & Properties

Relating to an Algebra

Connecting the Pieces

  • Domain model informs the Algebra
  • Algebra provides Structure
  • Implementation

Relating to an Algebra

Benefits of this Approach

  • Rigour and Precision
  • Testability
  • Maintainability

Banking Example

Domain Concepts

Account, Transaction, Balance

Operations

deposit, withdraw, transfer

Algebraic laws

Consistency

withdraw operation requires sufficient funds

Algebraic laws

Associativity

Multiple deposits and withdrawals result in same final balance regardless of order

Algebraic Data Types

Why Algebraic?

  • Objects
  • Operations
  • Laws
The algebra consists of two primary operators

  • Product (represented by "×" or "AND")
  • Sum (represented by "+" or "OR")

Product Type

x

  • Think of it as an AND relationship.
  • A Point type is a product of Coordinate types
  • tuples, POJOs, structs, or records
  • Cartesian product

  public record TextStyle(Font font, Weight weight){}
  public enum Font { SERIF, SANS_SERIF, MONOSPACE }
  public enum Weight { NORMAL, LIGHT, HEAVY }
         
TextStyle = Font ⨯ Weight
9 = 3 x 3

Sum Type

+

  • Think of it as an OR relationship.
  • A Shape could be a Circle OR a Square OR a Triangle.
  • Defines variants.
 Type A = Integer | Boolean 
 Type B = String | Float 
 Type C = A + B 
 4 = 2 + 2

Combining Product and Sum Types


DnsRecord(AValue(ttl, name, ipv4)
          | AaaaValue(ttl, name, ipv6)
          | CnameValue(ttl, name, alias)
          | TxtValue(ttl, name, value))
        

DnsRecord(ttl, name, AValue(ipv4)
          | AaaaValue(ipv6)
          | CnameValue(alias)
          | TxtValue(value))
      

Distributive

\( \color{orange} (a \cdot b + a \cdot c) \Leftrightarrow a \cdot (b + c) \)

Commutative

\( \color{blue} (a \cdot b) \Leftrightarrow (b \cdot a) \)
\( \color{red} (a + b) \Leftrightarrow (b + a) \)

Associative

\( \color{green} (a + b) + c \Leftrightarrow a + (b + c) \)
\( \color{purple} (a \cdot b) \cdot c \Leftrightarrow a \cdot (b \cdot c) \)

Algebraic Data Types

Features

  • Composite
  • Readability
  • Constraint Enforcement
  • Reduce Boilerplate

A Historical Perspective

Early Days

  • Originated in the 1970s with functional languages like ML and Hope.
  • Popularized by Haskell, where ADTs are a fundamental building block.

Algebraic Data Types

Different Approaches

C language

  • No built-in ADT support.
  • Product types simulated with structs.
  • Sum types crudely approximated with unions and an enum tag for type tracking.
  • Unions are notoriously unsafe (no compile-time type checking).

C language


        union vals {
          char ch;
          int nt;
        };

        struct tagUnion {
          char tag; // Tag to track the active type
          union vals val;
        };
      

Haskell

  • Elegant ADT support using the data keyword.
  • Type system designed for ADT creation and manipulation.

        data Shape = Circle Float | Rectangle Float Float
      

Scala

  • case classes for product types.
  • sealed traits with case classes/objects for sum types.
  • Robust and type-safe ADT definition.

  sealed trait Shape
  case class Circle(radius: Double) extends Shape
  case class Rectangle(width: Double, height: Double) extends Shape

TypeScript

  • Product types can be represented using interfaces or classes.
  • Sum types are created by combining types with the union operator and adding a discriminant property.
  • Type-safe pattern matching.

TypeScript


interface Circle {
  kind: "circle"; // Discriminant
  radius: number;
}

interface Rectangle {
  kind: "rectangle"; // Discriminant
  width: number;
  height: number;
}

type Shape = Circle | Rectangle;
    
            
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}
    

Legacy Java (Pre-Java 17)

  • Simulated Sum types
  • Product types represented by classes with member variables.

Algebraic Data Types

Modern Java

  • Records
  • Sealed Interfaces
  • Pattern Matching

What about Enums?


enum Task {
  NotStarted,
  Started,
  Completed,
  Cancelled;
}

sealed interface TaskStatus{
  record NotStarted(...) implements TaskStatus
  record Started(...) implements TaskStatus
  record Completed(...) implements TaskStatus
  record Cancelled(...) implements TaskStatus
}
                
            
                
enum Planet {
  MERCURY (3.303e+23, 2.4397e6),
  VENUS (4.869e+24, 6.0518e6),
  EARTH (5.976e+24, 6.37814e6);

  private final double mass;   // in kilogram
  private final double radius; // in metres

  private Planet(double mass, double radius) {
    this.mass = mass;
    this.radius = radius;
  }
  public double mass() { return mass; }
  public double radius() { return radius; }
}
              
            
                
sealed interface Celestial {
  record Planet(String name, double mass, double radius)
                  implements Celestial {}
  record Star(String name, double mass, double temperature)
                  implements Celestial {}
  record Comet(String name, double period)
                  implements Celestial {}
}
              
            

The Visitor Pattern

Purpose

Handle operations on different data types within a hierarchy of objects without modifying the structure of those objects themselves.


sealed interface Shape
    permits Circle, Rectangle, Triangle, Pentagon {
  <T> T accept(ShapeVisitor<T> visitor);
}
            
interface ShapeVisitor<T> {
  T visit(Circle circle);
  T visit(Rectangle rectangle);
  T visit(Triangle triangle);
  T visit(Pentagon pentagon);
}
          

record Circle(double r) implements Shape {

  public <T> T accept(ShapeVisitor<T> visitor) {
    return visitor.visit(this);
  }
}
            
record Rectangle(double w, double h) implements Shape {

  public <T> T accept(ShapeVisitor<T> visitor) {
    return visitor.visit(this);
  }
}
            
record Triangle(double s1, double s2, double s3)
        implements Shape {

  public <T> T accept(ShapeVisitor<T> visitor) {
    return visitor.visit(this);
  }
}
            
record Pentagon(double s) implements Shape {

  public <T> T accept(ShapeVisitor<T> visitor) {
    return visitor.visit(this);
  }
}
        

class AreaCalculator implements ShapeVisitor<Double> {

  public Double visit(Circle circle) {
    return PI * circle.r() * circle.r();
  }

  public Double visit(Rectangle rectangle) {
    return rectangle.w() * rectangle.h();
  }

  public Double visit(Triangle tri) {
    double s = (tri.s1() + tri.s2() + tri.s3()) / 2;
    return sqrt(s * (s - tri.s1()) * (s - tri.s2())* (s - tri.s3()));
  }

  public Double visit(Pentagon p) {
    return (0.25) * sqrt(5 * (5 + 2 * sqrt(5))) * p.s() * p.s();
  }
}
         
class PerimeterCalculator implements ShapeVisitor<Double> {

  public Double visit(Circle circle) {
    return 2 * PI * circle.r();
  }

  public Double visit(Rectangle rectangle) {
    return 2 * (rectangle.w() + rectangle.h());
  }

  public Double visit(Triangle triangle) {
    return triangle.s1() + triangle.s2() + triangle.s3();
  }

  public Double visit(Pentagon pentagon) {
    return 5 * pentagon.side();
  }
}
         
class InfoVisitor implements ShapeVisitor<String> {
  public String visit(Circle circle) {
    return "Circle with radius: %.2f, area: %.2f, perimeter: %.2f"
     .formatted(circle.r(), new AreaCalculator().visit(circle),
        new PerimeterCalculator().visit(circle));
  }

  public String visit(Rectangle rect) {
    return "Rectangle with width: %.2f , height: %.2f, area: %.2f, perimeter: %.2f"
     .formatted(rect.w(), rect.h(),
        new AreaCalculator().visit(rect),
        new PerimeterCalculator().visit(rect));
  }

  public String visit(Triangle tri) {
    return "Triangle with sides: %.2f, %.2f, %.2f, area: %.2f, perimeter: %.2f"
     .formatted(tri.s1(), tri.s2(), tri.s3(),
        new AreaCalculator().visit(tri),
        new PerimeterCalculator().visit(tri));
  }

  public String visit(Pentagon pentagon) {
    return "Pentagon with side: %.2f, area: %.2f, perimeter: %.2f"
        .formatted(pent.side(),
          new AreaCalculator().visit(pent),
          new PerimeterCalculator().visit(pent));
  }
}
        

class Shapes {
  public static void main(String[] args) {
    List<Shape> shapes = List.of(new Circle(5), new Triangle(3, 3, 3), new Rectangle(3, 5), new Pentagon(5.6));
    InfoVisitor infoVisitor = new InfoVisitor();

    System.out.println("\nShapes:");
    shapes.stream().map(s -> s.accept(infoVisitor)).forEach(System.out::println);
  }
}
        
          
Shapes: [Circle[radius=5.0], Triangle[side1=3.0, side2=3.0, side3=3.0],
          Rectangle[width=3.0, height=5.0], Pentagon[side=5.6]]
Circle with radius: 5.00, area: 78.54, perimeter: 31.42
Triangle with sides: 3.00, 3.00, 3.00, area: 3.90, perimeter: 9.00
Rectangle with width: 3.00 , height: 5.00, area: 15.00, perimeter: 16.00
Pentagon with side: 5.60, area: 53.95, perimeter: 28.00
        

Pattern Matching


sealed interface Shape
                permits Circle, Rectangle, Triangle, Pentagon {}

record Circle(double r) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double s1, double s2, double s3) implements Shape {}
record Pentagon(double s) implements Shape {}
            

static double area(Shape shape) {
  return switch (shape) {
    case Circle(var r) -> PI * r * r;
    case Rectangle(var w, var h) -> w * h;
    case Triangle(var s1, var s2, var s3) -> {
            double s = (s1 + s2 + s3) / 2;
            yield sqrt(s * (s - s1) * (s - s2) * (s - s3));}
    case Pentagon(var s) -> (0.25) * sqrt(5 * (5 + 2 * sqrt(5))) * s * s;
   };
}
                   

 static double perimeter(Shape shape) {
    return switch (shape) {
      case Circle(var r) -> 2 * PI * r;
      case Rectangle(var w, var h) -> 2 * (w + h);
      case Triangle(var s1, var s2, var s3) -> s1 + s2 + s3;
      case Pentagon(var s) -> 5 * s;
    };
  }
               

   static String info(Shape shape) {
    return switch (shape) {
      case Circle c ->
          "Circle with radius: %.2f, area: %.2f, perimeter: %.2f"
             .formatted(c.r(), area(c), perimeter(c));
      case Rectangle r ->
          "Rectangle with width: %.2f , height: %.2f, area: %.2f, perimeter: %.2f"
             .formatted(r.w(), r.h(), area(r), perimeter(r));
      case Triangle t ->
          "Triangle with sides: %.2f, %.2f, %.2f, area: %.2f, perimeter: %.2f"
             .formatted(t.s1(), t.s2(), t.s3(), area(t), perimeter(t));
      case Pentagon p ->
          "Pentagon with side: %.2f, area: %.2f, perimeter: %.2f"
             .formatted(p.s(), area(p), perimeter(p));
    };
               

The Expression Problem

The challenge of extending data structures and operations independently.

  • Add new data types Without modifying existing code that operates on those data types.
  • Add new operations Without modifying existing data types.

Visitor Pattern

  • Adding new operations (Easy)
  • Adding new data type (Hard)
  • Verbose
  • No Exhaustiveness Checking

Pattern Matching

  • Adding new operations (Easy)
  • Adding new data type (Easier)
  • More concise
  • Exhaustiveness Checking

🍦

Questions?

Fini

🍰

🐟

🐳