JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 31
  • Score
    100M100P100Q63403F
  • License MIT

Provides a structured approach to a bussiness model creation and validation.

Package Exports

  • self-assert

Readme

self-assert

Design objects that are fully responsible for their validity.

npm version License Lint and Test


A stable version of this project is meant to be released soon.

Table of Contents

Credits and Acknowledgements

This project is based on the ideas presented by Hernán Wilkinson (@hernanwilkinson) in his Diseño a la Gorra webinar.

Diseño a la Gorra explores the principles of object-oriented software design, with a focus on practical examples and heuristics for creating high-quality software. The videos are mostly in Spanish, but the code and ideas are universally understandable.

A central theme of Diseño a la Gorra is understanding software as a model of a real-world problem. From this perspective, developing software is fundamentally the act of designing an effective model.

According to this approach:

  • A good software model abstracts the relevant aspects of the domain, allowing for clear understanding and effective solutions.
  • Software design is a continuous process of learning and refining the model.
  • A good model not only works but also teaches how to interact with it through its structure and behavior.
  • Objects should represent domain entities, and be created complete and valid from the start, reflecting a coherent state of the real world.

The concepts behind self-assert were introduced in Episode 2 ("Valid Objects") and further developed in Episode 3 ("Modeling Sets of Objects").

Diseño a la Gorra also encourages a shift in mindset:

  • Code is not written for the computer; it's written to model our understanding of the domain.
  • Objects are not just data containers; they are collaborators that encapsulate behavior and ensure consistency.

This mindset is what self-assert aims to support: designing objects that are responsible of protecting their own validity from the very beginning.

Installation

Install self-assert with npm:

npm install self-assert

Usage

This section is meant as a guide to help you get started with self-assert. It does not define rules, but rather showcases what the contributors consider to be best practices.

For more information, refer to the original webinar example.

Defining Assertions for Object Validation

To ensure that domain objects are created in a valid and complete state, self-assert introduces the Assertion abstraction.

There are two main ways to define and use assertions:

  • Self-contained assertions: the assertion conditions don't need any parameters to evaluate.
  • Reusable assertions: the assertion is defined once and later evaluated with different values.

A common workflow is:

  1. Define a main static factory method (e.g., create) that:
    • Receives all required parameters to build a complete object.
    • Validates those parameters using one or more Assertions.
    • Returns a valid instance or raises an error.
  2. Use Assertion.for for self-contained checks, or create reusable assertions and apply them later using AssertionEvaluation.
  3. Use AssertionsRunner.assertAll to execute the assertions together in the previously defined factory method.
  4. (Optional) If you are using TypeScript, consider marking the class constructor as protected.
  5. Ensure that all other factory methods use the main one.

Here's a simplified example:

import { Assertion, AssertionEvaluation, AssertionsRunner } from "self-assert";

class Person {
  static readonly nameNotBlankAID = "name.notBlank";
  static readonly nameNotBlankDescription = "Name must not be blank";
  static readonly agePositiveAID = "age.positive";
  static readonly agePositiveDescription = "Age must be positive";

  // Reusable assertion (evaluated later with a value)
  static readonly nameAssetion = Assertion.for<string>(
    this.nameNotBlankAID,
    this.nameNotBlankDescription,
    (name) => name.trim().length > 0
  );

  static named(name: string, age: number) {
    AssertionsRunner.assertAll([
      // evaluated with `name`
      AssertionEvaluation.for(this.nameAssertion, name),
      // self-contained assertion for age
      Assertion.for(this.agePositiveAID, this.agePositiveDescription, () => age > 0),
    ]);

    return new this(name, age);
  }

  protected constructor(protected name: string, protected age: number) {}

  getName() {
    return this.name;
  }

  getAge() {
    return this.age;
  }
}

try {
  const invalidPerson = Person.named("  ", -5);
} catch (error) {
  if (error instanceof AssertionsFailed) {
    console.log(error.hasAnAssertionFailedWith(Person.nameNotBlankAID, Person.nameNotBlankDescription)); // true
    console.log(error.hasAnAssertionFailedWith(Person.agePositiveAID, Person.agePositiveDescription)); // true
  } else {
    console.error("An unexpected error occurred:", error);
  }
}

If any of the assertions fail, an AssertionsFailed error will be thrown, containing all failed assertions.

This promotes the idea that objects should be created valid from the beginning, enforcing consistency.

Using Draft Assistants

The FieldDraftAssistant helps validate and suggest completion options for a single field or property. It should be used when you need to encapsulate the logic that determines whether a field is complete and what values could make it valid.

The SectionDraftAssistant validates and suggests how to complete a group of related fields. It aggregates multiple FieldDraftAssistant or other SectionDraftAssistant instances.

function createPersonAssistant() {
  const nameAssistant = FieldDraftAssistant.handlingAll<Person>([Person.nameNotBlankAID], (person) => person.getName());
  const ageAssistant = IntegerDraftAssistant.for<Person>(Person.agePositiveAID, (person) => person.getAge());

  const personAssistant = SectionDraftAssistant.topLevelContainerWith<Person, [string, number]>(
    [nameAssistant, ageAssistant],
    (name, age) => Person.named(name, age),
    [] // Any other assertion IDs if apply
  );

  return Object.assign(personAssistant, { nameAssistant, ageAssistant });
}

const personAssistant = createPersonAssistant();
// Use your assistant in your system's external interfaces (UI, REST, etc.), then:

personAssistant.withCreatedModelDo(
  (person) => {
    console.log(person instanceof Person); // true
    doSomething(person);
  },
  () => {
    // The creation of a Person failed.
    console.log(personAssistant.hasFailedAssertions()); // true
  }
);

[!NOTE] Using Object.assign can help you keep track of the internal assistants of a higher-level assistant. In the above example, TypeScript should correctly infer the return type of createPersonAssistant:

// No compilation error
createPersonAssistant().nameAssistant.setModel("John");

Resources

License

MIT