We will implement the observer pattern to create a means of State Validation in Javascript.
Introduction
State Validation with Javascript. So you can know your data and objects are valid in Javascript.
Background
I had an amazing conversation on the Frontend Masters discord server today. We were discussing many different topics, but the portion that was stuck on my mind was when we were discussing why I dislike server-side Javascript.
It is no secret that you can’t know the exact state of objects or data at all times in Javascript and that “fun” bugs crop up due to this.
That is when DTOs / POJOs were brought up. A DTO is a Data Transfer Object in C# and POJO is Plain Old Java Object. The equivalent of this in Javascript is just called Object Validation.
It never occurred to me that I could and should validate objects and data in Javascript at a core level. I do this all of the time in C#, so why didn’t I do this in Javascript?
This nagged at my very core as I was laying down for a nap (I’m old, get over it). As I was laying down, it just kept coming up in my mind, how would I do this in a clean and efficient manner without using zod or valibot?
I fell asleep with my mind aglow with whirling, transient nodes of thought careening through a cosmic vapor of invention. When I woke up, it hit me! If I want to know the exact state of an object at all times, I can use the Observer Pattern!
I can extend it to also check for validations when moving to a new state, it can fail or succeed. If it fails, no updates can take place, if it passes, then updates are allowed to go through.
It was genius, eloquent and simple!
Using the code
export class Observable {
constructor(config = {}) {
const { initialState = {}, validators = [], onSuccess = () => {}, onFailure = () => {} } = config;
this.state = { ...initialState };
this.subscribers = [];
this.validators = validators;
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
subscribe(callback) {
this.subscribers.push(callback);
return this;
}
addValidator(validator) {
this.validators.push(validator);
return this;
}
validate(newState) {
for (const validator of this.validators) {
const result = validator(this.state, newState);
if (result !== true) {
return result || "Invalid state update.";
}
}
return true;
}
setState(newState) {
const validationResult = this.validate(newState);
if (validationResult === true) {
this.state = { ...this.state, ...newState };
this.notify();
this.onSuccess(this.state);
} else {
console.log("State update failed:", validationResult);
this.onFailure(validationResult);
}
return this;
}
notify() {
for (const cb of this.subscribers) {
cb(this.state);
}
}
getState() {
return this.state;
}
}
Let’s break this class down piece by piece.
constructor
: This is the method that’s called when you create a new instance of the Observable class. It sets up the initial state of the Observable, including its initial state, any validation functions, and what to do when state changes are successful or fail. subscribe(callback)
: This method allows other parts of your code to subscribe to changes in this Observable’s state. When the state changes, all subscribed callbacks are called with the new state. addValidator(validator)
: This method allows you to add validation functions that can check whether a proposed state change is valid. validate(newState)
: This method runs all the validator functions on a proposed new state. If any validator returns a value other than true
, it returns that value as an error message. Otherwise, it returns true
to indicate that the new state is valid. setState(newState)
: This method attempts to update the state of the Observable. It first validates the new state. If the new state is valid, it updates the state, notifies all subscribers of the change, and calls the onSuccess
callback. If the new state is not valid, it logs an error message and calls the onFailure
callback. notify()
: This method calls all subscribed callbacks with the current state. It’s called whenever the state changes successfully. getState()
: This method returns the current state of the Observable.
This class is chainable, meaning you can link multiple method calls together like this:
observable.subscribe(callback).addValidator(validator).setState(newState);
This is because the subscribe
, addValidator
, and setState
methods return this
, which is the instance of the Observable.
For simplicity, I am only going to show a couple validators, but you can make them according to your needs.
export const nonEmptyNameValidator = (oldState, newState) => {
if ("name" in newState && newState.name.trim() === "") {
return "Name cannot be empty.";
}
return true;
};
export const ageRangeValidator = (oldState, newState) => {
if ("age" in newState && (newState.age < 0 || newState.age > 150)) {
return "Age must be between 0 and 150.";
}
return true;
};
These are pretty straightforward and self documenting Javascript functions. So I don’t think I need to go into any detail here.
Let’s move on to the index.js file.
import { nonEmptyNameValidator, ageRangeValidator } from './Validators.js';
import { Observable } from './Observable.js';
const config = {
initialState: { name: "John", age: 30 },
validators: [nonEmptyNameValidator, ageRangeValidator],
onSuccess: (newState) => {
console.log("State successfully updated:", newState);
},
onFailure: (error) => {
console.log("State update failed due to:", error);
}
};
const myObject = new Observable(config);
myObject.subscribe((newState) => {
console.log("Subscriber: State updated to:", newState);
});
myObject.setState({ name: "Jane" }).setState({ age: 13 }).setState({ name: "" }).setState({ age: 251 });
console.log(myObject);
We import our Validators.js with the two validations we have and we also import Observable
from our Observable
class.
We create our config, which is the object we want to keep close watch on. Define the initial state, the validators we want to use with it, what happens on success and failure.
We instantiate our Observable
class and pass our config object as a parameter for the constructor. We subscribe to the observer and when we want to update the object, we set the new state.
What happens when the state is updated and it fails? The changes aren’t stored.
Points of Interest
What was the point of this story? Well, that is for you to decide. I just wanted to share a source of inspiration I had based on a conversation.
One final tidbit, there is one final thing that bugs me with this implementation, It isn’t as clean with the index.js as I would like. I think the onSuccess
and onFailure
should probably be optional parameters and have it default to logging to the console. That way, a dev doesn’t have to arduously write out onSuccess
and onFailure
for every object. Additionally, You should be able to pass in an array of objects like you can with the validators for the Observable
constructor.
Again, the subscribe call should be inherent to the constructor, there is no reason why you would need to have to write that out unless it is for DB updates.