Expose Snapshots

Do not expose the internal of an object through getters but implement a method for objects to create a representation by themselves.

Alas, this is the terrible fate of some of our objects : to be serialized and sent through the wire.

Sometimes, they need to be exposed as part of an API, and some other times to be stored in a database. If you're using an ORM, you might be bound to add getters and setters to these objects. To this regard, the data mapper pattern can be a saner approach.

There's many ways to open these objects properly while maintaining as much encapsulation as desired, but most of them are cumbersome, verbose and not very practical.

The pragmatic approach to this problem is to use Expose Snapshots. The pattern is detailed at Snapshot but we'll take a rapid take at the problem.

Suppose we a have a Domain Model representing a Student in our driving school project. We can define this model in two parts :

  • Its internal state, reserved for its own use.

  • A snapshot, a model it agrees to expose and maintain

In TypeScript, it may look like this.

type State = {
  id: string;
  creditPoints: number;
  schedule: Schedule;
};

type Snapshot = {
  id: string;
  creditPoints: number;
  schedule: GetSnapshot<Schedule>;
};

export class Student extends Entity<State, Snapshot> { 
  // creates a student from a snapshot
  static fromSnapshot(snapshot: Snapshot) {
    return new Student({
      id: snapshot.id,
      creditPoints: snapshot.creditPoints,
      schedule: Schedule.fromSnapshot(snapshot.schedule)
    });
  }
  
  // take a snapshot
  snapshot(): Snapshot {
    return {
      id: this._state.id,
      creditPoints: this._state.creditPoints,
      schedule: this._state.schedule.snapshot(),
    };
  }
  
  // Many behavioral functions
}

Does it work ? Yes, it works properly and meet our needs to expose the object and its internal.

Does it suck ? Hell yes.

We just broke encapsulation. We force our object to expose its internals voluntarily in order to leave its safe object world. But at least, the object is aware of it and has control over it.

We now ought to maintain this contract and break it almost every time we want to change our internal structure. It will happen much less often than if we'd have the getters exposed, since the getters expose the raw properties while the snapshot exposes a flat representation composed of mostly primitives.

But at least it's easy to implement, to understand and to use. We basically transform our Domain Model into a DTO à la carte. All the knowledge is condensed into the object, instead of awfully exposing getters for each and every property. We have a semantically meaningful method name stating when and how this object will be exposed.

The light shines on CQRS

Quite often, almost every time, you will need to expose these objects from your public HTTP API via some GET calls.

At this point, the data of your objects are stored in a database.

That's great, your normalized data is available in storage, ready to be used. Since you will probably need to pick on specific data, make joins and make aggregations, odds are your output will be far different from your domain-model.

We might, for example, expose a Student like so.

[
    {
        "id": "...",
        "firstName": "...",
        "creditsHistory": [
            {
                "id": "...",
                "payment": "2024-01-01T10:00:00.000Z",
                "amount": 98.00
            }
        ],
        "lessonsTaken": 17,
        "nextLesson": {
            ...
        }
    }
]

See, our output data is nothing like our Domain Model, it contains other computed fields made available for our app's consumers.

Thus, you can expose your data using DTOs, or ViewModel (they're essentially the same in this context), which are poor data structures whose purpose is to be sent over the wire.

Differentiating the read model and the write model like so is called CQRS.

But the database ought to fall in darkness

We can't be saved from the database.

Sooner or later, we will need to save our objects up there, but the databases can't really store objects now, can they ?

So we have to store them in normalized form and carry IDs here and there.

Or we have to develop a domain-model that is bound to the ORM, using ORM constructions, limiting our modeling and testing capacity, but making our life somewhat easier.

The ORM will relentlessly enslave our objects and rid them of their property, but at least it happens in the world of the ORM, right ?

Maybe it's a small price to pay for simpler code.

That's really sad.

Last updated