Class Invariant

Updated Apr 9th, 2019 / 1 min read

About this...

Invariants are a form of ensuring data integrity of an object.




By data integrity, we mean "what shape is this data allowed to take?”, “what methods can be called and at which point?", “what are the required parameters and pre-conditions in order to create this object”?

Methods need to preserve the invariants of an object. They need to constrain the state stored in the object such that it doesn't end up being invalid.

Here's an example using a Latitude class.

import { ValueObject } from '../../../core/valueObject';
import { Result, TypedResult } from '../../../core/result';

export interface ILatitude {
  value: number;
}

export class Latitude extends ValueObject<ILatitude> {
  value: number;
  
  private constructor (props: ILatitude) {
    super(props);
    this.value = props.value
  }

  // Factory method
  // The latitude must be a number between -90 and 90
  public static create (latitude: ILatitude) : TypedResult<Latitude> {
    if (latitude.value < -90 || latitude.value > 90) {
      return Result.typedFail<Latitude>("Latitude must be within -90 and 90")
    }
    return Result.typedOk<Latitude>(new Latitude(latitude))
  }
}

In this example, in order to create a Latitude object, we need to satisfy the invariant of passing in a value between -90 and 90.

Anything else is not a valid latitude.


5 Comments

Submit
Julien Heller
a year ago

Do you have opinions on whether failed invariant assertions should throw errors or not? I'm interested in your decision to return a 'typedFail | typedOk' type instead. In the current app I'm building, failed invariant assertions throw an 'AggregateIntegreityError' which are caught specifically.

Khalil Stemmler
a year ago

Hey Julien,


That works. Though, I have a slight opinion on that. Check out this article on Functional Error Handling.


Julien Heller
a year ago

Brilliant, thank you

Amirhossein
2 months ago

Hello. Why do we need a create method? Can't we enforce our constraints in the constructor as well??

Metlic
a month ago

@Amirhossein One of the reasons I see is you could use async code within body of factory method, wich you couldn't with constructor. For example, validate data in DB. The other reason you can have factory in a base class, but this probably too specific case.

Metlic
a month ago

And of course in this particular case if wee use constructor to validate a data then the only way of knowing that the data is invalid is throw an exception. Instead we can model logic of our application using pipes (one for valid work, other for failure) that is demonstrates in the example from the article.