Your career needs a vision

Link: https://swizec.com/blog/your-career-needs-a-vision

It is well known the drunken sailor who staggers to the left or right with n independent random steps will, on the average, end up about √n, steps from the origin. But if there is a pretty girl in one direction, then his steps will tend to go in that direction and he will go a distance proportional to n. In a lifetime of many, many independent choices, small and large, a career with a vision will get you a distance proportional to n, while no vision will get you only the distance √n, . In a sense, the main difference between those who go far and those who do not is some people have a vision and the others do not and therefore can only react to the current events as they happen. 

The Art of Science and Engineering, Richard Hamming

Published
Categorized as Default

Bad names make you open the box

From the blog post “Bad names make you open the box” by Adam Zerner.

Imagine that you open the fridge. You see something labeled “brownie”. You eat it.

Then you hop in the car and start heading over to your friends house. But right as you merge on to the highway, you start feeling funny.

Turns out that the “brownie” label was a little misleading. It wasn’t a regular brownie. It was a pot brownie. There was something dangerous inside the brownie, but the label didn’t reflect that.

This is similar to poorly named functions with dangerous side effects. In both cases, if the thing in question can have dangerous side effects, you really want to make sure that it is reflected in the label. You can’t trust that people will read beyond the label. And even if you could, you wouldn’t want people to have to do that. You’d rather them be able to get the information they need from the label.

Adam Zerner
Published
Categorized as Default

Eventually, You Will Need To Upgrade the Crypto

Every hashing and encryption algorithm will eventually become vulnerable and obsolete. Before that happens, you need to stay ahead and use something known to protect your sensitive data.

When the algorithm you are using becomes vulnerable, you must act quickly and replace it as soon as possible to minimize the exposure window. Replacing your current algorithm might be a daunting task if you did not prepare for change.

We must design systems for change.

Recently I had to upgrade an encryption library while maintaining the old library for backward compatibility. The new library gave us some flexibility; the old one did not have. However, the underlying algorithms stayed the same.

The idea was that whenever we needed to decrypt an old piece of data, we would re-encrypt using the new library. That way, we could gradually migrate to the new library without interruption. We needed a way to encode what library we were using with each piece of data to decide what library we needed to use to decrypt and if we needed to migrate that data to the new library format.

When presented with this challenge, I remembered how Django encoded passwords for database storage.

<algorithm>$<iterations>$<salt>$<hash>

They encode the algorithm and other needed information to recreate the password hash into a string. Then, when a user enters a password, you can quickly parse this string and get all the required information to validate the hash and decide if the hash needs an upgrade.

It’s a straightforward idea that works great.

Quick aside to clarify: If you have a known vulnerable hash or encryption algorithm you will want to do something about it immediately and not wait for user action.

Specifically, in case of a vulnerable password hash the recommended technique is to rehash your current weak hashes with a secure hashing algorithm. Then, when the user logs in you can migrate their password hash to the new setup.

Finally, I wrote this post to remind you and me that we should add a version prefix whenever we need to store a password hash or encrypted data. That way, when we need to migrate, we are ready to do it.

Photo: Unsplash

Published
Categorized as Default

Logging is important

Getting application logging right is crucial for every software project. When done correctly it makes the maintainers work a lot simpler. There’s no worst feeling than getting an error report and searching for logs to try to understand what happened and not finding a single entry. This is especially bad with difficult to reproduce production errors.

Like everything else, having good logs requires a little bit of thought and consistency. But is not that hard.

I’m not an expert on this subject. This post contains my notes on what has worked for me in the past in the context of backend web applications.

Use a real logger

Every language has a very simple way to send strings to stdout. That’s not a “real” logger.

In Node you have console.log() and in Python print(). These work great for simple scripts and print debugging. On a real production application, you want to use a “real” logger.

For Node, I prefer using pino. It’s fast, has great support, and is very easy to use.

What to log

  • Request/Response – You must log every request and response. For requests, I like to include all headers, except the Authorization header. For responses, I always include the HTTP status code and the response time. Both request and response include a unique request ID to correlate them.
  • App Errors – every catch in your code must include an error log describing what went wrong. Also whenever your app gets into a state that will result in an HTTP status code greater than 299 you should have some sort of application-level error log, describing why your request ended in an error.
  • Security events – Always log when a login fails, or when a user tries to access a page or an object is not supposed to. Always log password changes, email changes or any other security credentials change. Make sure you don’t log the actual password or sensitive information.
  • Business events – This is optional but it’s a good idea. If your app is a shopping cart, create a log entry every time you close a sale. The idea here is that you might want to create alerts based on these log events. If you usually see 100 sales per hour and suddenly see zero, it might be a good idea to take a look and understand what it’s going on.

A good log line

Having an unsearchable pile of logs is not useful. All my logs are in JSON format. This is very useful if you use a service that allows you to query JSON logs. I use AWS CloudWatch logs which is not perfect, but it does a decent job of helping me find what I’m looking for if I created the logs correctly.

Every log line must have the following.

  • Log level – Usually a number to indicate what kind of log it is. Info, error, and debug are the most common.
  • Message – This is the most important part of the log entry. Try to make this explicit and easy to understand for your future self or your fellow team members. Try to make messages unique so they are easy to search on your codebase. Having multiple error logs with the same message makes things harder than they have to be.
  • Group – All of my logs have a group field that use to indicate what part of the application is producing the log entry. Most of the time this group is used to indicate which specific file is creating the log entry. This facilitates searching.
  • Context – Application error logs usually require some context that will help you determine what is the problem without logging all of your application state. It is very important to include the minimum amount of data possible on every log entry but enough to determine what is going on. For example if your input validation failed it might be a good idea to log what exactly went wrong. For example if you only allow names of a certain length, it might be a good idea to log the length of the submitted name that produced the error.
  • Timestamp – Almost every logger adds a timestamp. Just make sure that the format used works for you and make sure you include the time zone or that your are very certain of what zone it’s being used.

Security concerns

If your system deals with PII or CHD, make sure you are not logging sensitive information. Pino and every decent logger have a feature to redact logs.

Also, make sure that you periodically check your logs for sensitive data. This is something not obvious and I’ve seen it go wrong multiple times.

Don’t log sensitive or secret information.

Final thoughts

I know logs are boring but they are an essential part of every app. A lot of thought is put into many aspects of software development but I rarely find people writing or discussing this subject. You should put some thought on your logging practices. You future self will thank you.

In summary:

  • Having decent logs will greatly improve the maintenance of your software.
  • Error logs are the most important kind of log.
  • Always include context on error logs.
  • Log every request and response and include status codes.
  • Make sure you don’t log sensitive information.
Published
Categorized as Default

Result y Maybe

En el post anterior mencioné brevemente que mis value objects implementan un método estático create que utilizo para crear instancias sin temor a excepciones. Este método devuelve un objeto llamado Result que sirve para envolver el resultado de llamar al método create.

En este post vamos a ver cómo funciona Result y al final vamos a hablar un poco de Maybe que es muy similar.

Create

Vamos directo al código, así implemento create en mis VOs.

// amount.ts
static create(value: number, currency: string): Result<Amount, Error> {
    try {
        return ok(new Amount(value, currency));
    } catch(error) {
        return err(error);
    }
}

Esté método estático se puede llamar escribiendo:

Amount.create(value, currency)

La única diferencia es que en vez de devolver una instancia de Amount como haría:

new Amount(value, currency)

Este método devuelve un Result que podría ser un Ok o un Err.

Veamos el contenido de create en detalle. En el try intento crear una instancia de Amount. Si no hay problema utilizo la función ok para crear una instancia de Ok que envuelva el Amount que acabo de crear.

En el catch atrapo el error si es que el constructor de Amount lanza una excepción. Sabemos que las excepciones que lanza el constructor son de tipo Error. Si este es el caso uso la función err para crear una instancia de Err que envuelva nuestro error.

Tanto Ok como Err son Result. Por lo que el tipo que devuelve este método estático siempre es Result.

Ahora vamos a mirar Result en detalle para entender mejor. No te preocupes si en este punto no entiendes todo, he omitido algunos detalles que iré revelando según vamos avanzando.

Result

Un Result es un objeto que puede contener solo uno de dos tipos. Ok o Err. El primero lo usamos para envolver resultados positivos o sin problemas y el segundo lo usamos para envolver errores.

La idea aquí es que en vez de lanzar una excepción cuando instanciamos un VO, devolvemos un valor que podemos inspeccionar e incluso pasar a otras partes del código sin preocupación de excepciones.

Vamos a ver como se implementa Result en TypeScript.

// result.ts
export type Result<T, E> = Ok<T, E> | Err<T, E>;

export class Ok<T, E> {
  constructor(readonly value: T) {
    this.value = value;
  }

  isOk(): this is Ok<T, E> {
    return true;
  }

  isErr(): this is Err<T, E> {
    return !this.isOk();
  }

  unwrap(): T {
    return this.value;
  }
}

export class Err<T, E> {
  constructor(readonly error: E) {
        this.error =  error;
    }

  isOk(): this is Ok<T, E> {
    return false;
  }

  isErr(): this is Err<T, E> {
    return !this.isOk();
  }

  unwrap(): T {
    throw new Error("Called `unwrap` on an Err");
  }
}

// Utility functions
export function ok<T, E>(value: T): Ok<T, E> {
  return new Ok(value);
}

export function err<T, E>(err: E): Err<T, E> {
  return new Err(err);
}

Vamos a romper este archivo en pedazos para entender todo lo que está pasando. Empecemos con Ok.

class Ok<T, E> {
  constructor(readonly value: T) {
    this.value = value;
  }

  isOk(): this is Ok<T, E> {
    return true;
  }

  isErr(): this is Err<T, E> {
    return !this.isOk();
  }

  unwrap(): T {
    return this.value;
  }
}

Como ves el constructor recibe un valor y lo guarda en el campo value de la clase. Además tiene algunos métodos simples como isOk que siempre responde true porque todas las intancias de la clase Ok están ok. También tiene lo opuesto isErr que obviamente hace lo contrario que isOk. Estos métodos los usamos para verificar que contiene un Result.

Finalmente tenemos el método unwrap que se encarga de sacar el valor de dentro de nuestro Ok. Este método lo usamos para sacar el valor que nos interesa de dentro de un Result que sabemos está Ok.

Ahora veamos la clase Err.

class Err<T, E> {
  constructor(readonly error: E) {
        this.error = error;
    }

  isOk(): this is Ok<T, E> {
    return false;
  }

  isErr(): this is Err<T, E> {
    return !this.isOk();
  }

  unwrap(): T {
    throw new Error("Called `unwrap` on an Err");
  }
}

Err es muy similar a Ok. En este caso el constructor recibe un error como único valor. Tenemos los mismos métodos que en Ok porque queremos que estas dos clases tengan la misma interface.

Algo interesante es que el método unwrap de Err hace throw de una excepción si se llama. Esto es porque cuando tenemos un error no tenemos un valor que podamos sacar así que no hay otra cosa que podamos hacer.

Para evitar caer en esta situación siempre tenemos que usar los métodos isOk o isErr para verificar que no vamos a tener problema cuándo llamemos unwrap.

Generics

Lamentablemente este objeto Result tienen que hacer uso de un feature avanzado de TS llamado generics pero te puedo jurar que vale la pena entender que está pasando.

Tanto Ok como Err tienen unos <T, E> al lado del nombre de la clase. Esto lo que hace es que le dice a TS que cuando instanciemos estas clases queremos poder decidir que type van a tener los valores que vamos a envolver más adelante. En resumen es una forma de reutilizar estas clases sin tener que saber los types de antemano.

Aquí un ejemplo.

new Ok<number, Error>(1);

Acabo de crear una instacia de Ok que puede aceptar un number o un Error aunque la clase Ok no usa el tipo Error. Esto va hacer sentido más tarde.

Otro ejemplo.

new Err<number, Error>(new Error('Something wrong'));

Aquí cree un Err y nuevamente pasé number y Error como type variables a la clase para que TS sepa los types que puede aceptar.

Result type alias

Si te fijas el objeto Result no es una clase, en realidad es un type alias. Aquí está la definición.

type Result<T, E> = Ok<T, E> | Err<T, E>;

Voy a tratar de traducir esto. Result de T y E es igual a Ok de T y E o Err de T y E. Por esto es que ambos Ok y Err aceptan dos type variables aunque cada uno usa solo uno. Result puede ser cualquiera de estos dos tipos. O sea, un Ok que envuelve un valor de tipo T o un Err que envuelve un error de tipo E.

Utility functions

Las últimas piezas de este rompecabezas son las funciones ok y err que están al final del archivo. Nota que son en minúscula a diferencia de las clases Ok y Err que usan pascal case.

Estas funciones son con lo que interactuamos la mayoría de las veces. En el ejemplo del método create al principio de este post se puede ver como las usamos.

Usando Result

Regresemos al método create del principio.

static create(value: number, currency: string): Result<Amount, Error> {
    try {
        return ok(new Amount(value, currency));
    } catch(error) {
        return err(error);
    }
}

Cómo se puede ver aquí este método devulve un objeto Result de Amount y Error. En otras palabras este método puede devolver una instancia de Ok que contendrá un valor de tipo Amount o una instancia de Err que contendrá un error de tipo Error.

Vuelve a leer esa dos oraciones anteriores. Creo que es importante llamar la atención sobre toda la información que estamos “codificando” dentro del type system. Estamos poniendo los types a ayudarnos a programar y detectar problemas en nuestro programa.

Sobre las funciones ok y err no creo que tenga que decir mucho. Simplemente se encargan de crear una instancia de un objeto Ok o Err. Las usamos cuando estamos implementando algún método que queramos devuelva un Result. Siempre usamos ok cuando la operación tiene éxito y usamos err cuando algo falla.

Veamos cómo se ve cuando usamos el método create ahora que entendemos mejor como se comporta Result.

let priceOrErr = Amount.create(1, "USD");

if (priceOrErr.isErr()) {
    console.error(priceOrError.error);
    return res.send(422);
}

let price = priceOrErr.unwrap();

En la primera linea priceOrErr es una instancia de Result<Amount, Error>, así que puede ser un Ok o un Err y no tenemos forma de saber. En la próxima linea hacemos el check con isErr, si es cierto sabemos que el contenido es un error. Pero si logramos pasar ese check, en las lineas subsiguientes sabemos que priceOrErr tiene que ser un Ok y podemos hacer unwrap sin tenemor a ninguna exepción.

Lo bueno de esto es que no solo nosotros sabemos que en este punto priceOrErr es un Ok, también TS lo sabe y nos ayuda con los completions. Además como le dimos mucha información a TS, el type system sabe que es un Ok de Amount. Por lo que cuando hacemos unrap TS sabe que lo que devuelve es un Amount.

De la misma forma dentro del if TS sabe que priceOrErr es una instancia de Err y que contiene un valor de tipo Error. Gracias TypeScript.

Result en todas partes

Espero que ya veas el valor que puede añadir usar Result en tu programa.

El uso de Result no tiene que limitarse a la creación de VOs. Puedes usar un Result en cualquier parte de tu programa donde realizas una operación que puede fallar. O sea en muchas partes.

Por ejemplo cuando hacemos queries a la base de datos, en vez de devolver la data directamente, envolvemos el resultado en un Result. De esta forma podemos verificar si está Ok antes de continuar.

Result nos permite usar el type system para “codificar” información importante sobre el programa.

Combine

Además de las funciones ok y err uso una función adicional que me ayuda a verificar varios Result en un solo paso. A esta funcion le llamo combine porque combina todos los Result en un solo Result que me dice si todos los Result están Ok.

combine recibe un Array de Result, itera por ellos y si alguno es un Err devuelve un Err, si todos son Ok, devuelve un Ok.

function combine(
  results: Result<unknown, Error>[]
): Result<unknown, Error> {
  for (let result of results) {
    if (result.isErr()) {
      return result;
    }
  }
  return ok(results);
}

Así usamos combine.

let priceOrErr = Amount.create(100, "USD");
let discountOrErr = Amount.create(1, "USD");
let taxOrErr = Amount.create(1, "USD");

let validationOrErr = combine([
    priceOrErr,
    discountOrErr,
    taxOrErr
]);

if (validationOrErr.isErr()) {
    console.error(validationOrErr.error);
    return res.send(422);
}

let price = priceOrErr.unwrap();
let discount = discountOrErr.unwrap();
let tax = taxOrErr.unwrap();

Una vez pasamos el if sabemos que todos los valores son Ok y podemos unwrap sin problema.

Extendiendo Result

No hay ninguna regla que diga que Result no puede tener más métodos. Incluso si miramos la documentación de Rust para su tipo Result no damos cuenta que implementa algunos método interesantes como unwrap_or_else.

Ese método lo podemos implementar en nuestro Result para poder sacar nuestro valor o un valor default si resulta que nuestro Result es un Err.

let defaultAmount = new Amount(0, "USD");

let priceOrErr = Amount.create(req.body.price, "USD");
let price = priceOrErr.unwrapOr(defaultAmount);

De esta forma si el Result de priceOrErr es Err, price será el valor de defaultAmount.

Hay muchas cosas interesantes que se pueden hacer con Result, por ejemplo podemos añadir un método map que aplique una funcion al valor que contenien el Result y devuelva un nuevo Result con el valor modificado. Este map debe ser lo suficientemente inteligente como para no devolver un error si resulta que el Result lo que contiene es un Err.

Con este método de map se pueden hacer muchas cosas interesantes, especialmente hacer cadenas de método que todos devueven un Result y todos esperan como input un Result. Pero de esto podemos hablar otro día.

Maybe

Finalmente hablaré brevemente del objeto Maybe. Este objeto es muy similar a Result. Usamos Maybe como valor para operaciones que pueden devolver o resultado o no.

Por ejemplo imagina que tienes una funcion notJuan que recibe una Array de nombres de personas y devuelva un Array de nombres que no son Juan. Esta función podría devolver un Array de resultado pero también podría devovler un Array vació, o sea ningún resultado.

No tengo nada en contra de los Juanes, simplemente este fue el mejor ejemplo que se me ocurrió.

Esta función la podemos implementar usando Maybe como el return type.

Un Maybe puede devolver un Some o un None. Devuelve Some cuando tenemos un valor que devolver y None cuando no tenemos un valor que devoler. Similar a Result tenemos funciones some y none para crear Maybe fácilmente.

Así implementaríamos la función notJuan.

function notJuan(names: string[]): Maybe<string> {
    let validNames = names.filter((n) =>  n !== "juan");

    if (validNames.length === 0) {
        return none();
    }

    return some(validNames);
}

let friends = ["María", "Luis", "Juan"];

let filteredFriends = notJuan(friends);

if (filteredFriends.isNone()) {
    return res.send(400);
}

let friendsExceptJuan = filteredFriends.unwrap();

En la última linea sabemos que friendsExceptJuan contiene algunos nombres y que ninguno es “Juan”.

Result y Maybe

En mis programas mas recientes he empezado a utilizar tanto Result como Maybe. Siento que me ayudan a expresar mejor la intención de los métodos en mi programa.

Utilizo Result como return value para cualquier operación que puede devolver un error y utilizo Maybe para cualquier operación que puede devolver datos o no.

En algunos casos me he visto usando ambos en cojunto por ejemplo puedes imaginar un método findById que busca en la base de datos un record usando el id.

Esta al ser una operación de la base de datos puede fallar por muchas razones, podemos tener un error en la conexión o quizás la petición tarda mucho y falla. Para capturar esa información uso un Result.

Si todo sale bien lo próximo que puede pasar es que no exista un objeto con ese id en la base de datos y para comunicar esto uso un Maybe. O sea que cuando hago una operación como findById eso devuelve algo como esto.

Result<Maybe<User>, Error>

Admito que puede ser complicado pero pienso que de esta forma estoy codificando lo más posible dentro del type system. No quiero decir que esta es “the one true way” pero por ahora nos funciona.

Seguiremos experimentando con esto y seguirmos evolucionando pero por ahora estamos bastante satisfechos.

Referencias

Es increíble que hemos llegado hasta aquí sin usar la palabra monad. Aquí te dejo algunos enlaces que me han ayudado mucho.

Próximo

En un próximo post lo que me interesa discutir es el rol de los VOs en la validación de request en un web app y cómo podemos complementar usando otros tipos de validación con librerías como JSON Schema.

Foto: Unsplash

Published
Categorized as Default

Debe ser imposible representar estados ilegales

Este es el tercer post en esta serie sobre arquitectura de software. En los posts anteriores he hablado de mi experiencia aprendiendo sobre este tema y sobre algunos de los problemas que he enfrentado en situaciones donde no hay una arquitectura clara.

En este post espero empezar a introducir algunas ideas y técnicas concretas que he usado para comenzar a resolver mis problemas.

Estados ilegales

Cuando digo “que sea imposible representar estados ilegales”, lo que quiero decir es que nuestra meta debería ser que nuestro programa nunca esté en un estado inválido. O sea que sea imposible hacer algo que llevará a nuestro programa a estar en un estado inesperado o no definido.

Eso significa que si nuestro programa es un carrito de compras y tenemos una regla de negocio que permite un máximo de tres artículos en carrito, debería ser imposible añadir un cuarto artículo al carrito.

En el pasado he atentido este tipo de reglas de negocio, poniendo validaciones en el perímetro de mi programa. Usualmentemeste trabajo en aplicaciones web, así que esto signfica, validación en un controller o algo directamente relacionado al endpoint que recibe el request del usuario para añadir un artículo al carrito. No hay nada malo con esto, solo que si la petición de añadir un cuarto artículo al carrito llega de otra parte, nada evitará que suceda, poniendo nuestro programa en un estado inválido o ilegal. Este podría ser un efecto deseado en algunos casos pero en este ejemplo estamos supiniendo que no queremos que esto pase nunca.

Este ejemplo me ayuda a ilustrar lo que queremos alcanzar, pero este post no cubrirá todo lo necesario para lograrlo. Me voy a enfocar en la pieza mas pequeña de un sistema que no permite representar estados ilegales.

Value Objects

No voy a ponerme a definir términos así que puedes mirar aquí y aquí para buenas definiciones. De lo que si voy a hablar es sobre como uso VOs dentro de mis programas para aplicar reglas de negocio.

Lo primero es decidir cuando usar un VO. Mi respuesta es que todo valor dentro de tu programa por defecto, debería ser un VO. La ventaja de este acercamiento es que te da la oportunidad de establecer reglas para cada valor individualmente. El ejemplo obvio de un VO es una cantidad de dinero (Amount).

Si tenemos un endpoint en un web app que recibe una cantidad de dinero probablemente tendremos un payload JSON como este.

{
 "amount": 100  
}

Este objeto muestra el amount con un valor de 100 centavos o $1.00. Cómo nuestro programa puede manejar varios tipos de moneda, rápidamente nos damos cuenta que necesitamos saber la moneda de esa cantidad así que añadimos ese campo.

{
 "amount": 100,
 "currency": "USD"
}

Ok, ahora si tenemos todo lo que necesitamos. Lo próximo que hacemos es validar el input en nuestra aplicación así que podemos escribir el siguiente código.

app.post(async (req, res) => {
    let {amount, currency} = req.body;

    if (typeof amount !== "number") {
        return res.send(422);
    }

    if (amount < 0 || amount > 1000000) {
        return res.send(422);
    }

    if (typeof currency !== "string") {
        return res.send(422);
    }

    if (!["USD", "EUR"].includes(currency)) {
        return res.send(422);
    }

    try {
        let response = await controller(amount, currency);
        return res.json(response);
    } catch (error) {
        // do some error handling
        return res.send(400);
    }

});
async function controller(amount, currency) {
    // do stuff with amount and currency
}

Obviamente esto se puede hacer de forma mucho mas declarativa usando algo como JSON Schema o cualquier otra librería de validación de objetos.

Pero hacer ese tipo de validación no nos ayuda a recordar que ya hicimos esa validación. Las funciones que reciban esto valores no tienen forma de saber que ya fueron validados y por lo menos en mi caso esto crea un poco de duda. ¿Que tal si no valido el input en esta funcion y alguien pasa valores sin verificar? Esto puede producir bugs e incluso problemas de seguridad. Este problema se puede resolver con VOs.

De este punto en adelante voy a usar TypeScript para los ejemplos. El sistema de types de TS nos va a ayudar a garantizar el contenido de nuestro VOs. Primero vamos a definir este VO Amount.

class Amount {
    readonly value: number;
    readonly currency: string;

    constructor(value: number, currency: string) {
        this.value = value;
        this.currency = currency;
    }
}

Este es el primer paso. Si te das cuenta definí una clase con ambos valores. Esto es una decisión de diseño importante. Como sabemos estos dos valores están completamente conectados. No se puede cambiar uno, sin que el otro cambie. $1 USD y 1€ EUR no son lo mismo. Así que estos dos valores en realidad son uno solo. Esto es lo primero que añade este VO a nuestro programa. Nos está dejando representar dos datos como un solo objeto conectándolos para siempre y ayudándonos a recordar que no podemos cambiar uno sin pensar como se afecta el otro. Sigamos.

Ahora el problema que veo es que si creo una instancia de esta clase no hay nada impidiendo que ponga valores que no puedo aceptar en mi programa. Por ejemplo si estamos haciendo un carrito de compras, ningún producto puede tener como precio un número negativo pero este VO nos permite pasar un número negativo. Como esta, hay otras reglas de negocio, que podemos implementar en este objeto para asegurarnos de que siempre que lo usemos, estas reglas sean aplicadas y si alguien o algo trata de romper estas reglas nuestro programa no lo permita.

A mi me gusta poner estas reglas en el constructor y si alguna de estas reglas no se cumple, lo que hago es throw una excepción para impedir la creación de la instancia. Esto hace imposible que exista una instancia que no cumpla con las reglas que establecimos.

class Amount {
    readonly value: number;
    readonly currency: string;

    constructor(value: number, currency: string) {
        // Check value
        if (typeof value !== "number") {
            throw new Error("Amount value must be a number");
        }

        if (value < 1 || value > 1000000) {
            throw new Error("Amount value must be between 1 and 1,000,000");
        }

        // Check currency
        if (typeof currency !== "string") {
            throw new Error("Amount currency must be a string");
        }

        if (currency.length !== 3) {
            throw new Error("Amount currency must be a valid ISO-4217 alpha code")
        }

        if (!["USD", "EUR"].includes(currency)) {
            throw new Error("Amount currency can only be USD or EUR");
        }

        this.value = value;
        this.currency = currency;
    }

}

Ok, ahora este VO es bastante más estricto, verifica los tipos de los argumentos que recibe y también se asegura que este objeto sigue nuestras reglas de negocio. Como hace throw de una excepción si algo está mal, no es posible crear una instancia en un estado ilegal.

Por otro lado, si tenemos una instancia de este objeto, sabemos que tiene que ser válida porque logró pasar por todas las pruebas (guards) que establecimos. Así que una vez tenemos una instancia de este objeto podemos estar seguros que es válida y podemos confiar en lo que contiene. No tenemos que volver a verificar nada porque sabemos todo.

Ahora donde quiera que necesitemos este valor en vez de pasar un number y un string podemos pasar un Amount y estar confiados.

Se que esto puede parece increíblemente obvio para algunos, especialmente los que están acostumbrados a trabajar en lenguajes strongly typed pero les puedo asegurar que para los que llevamos toda la vida en lenguajes dynamically typed esto es una revelación.

Que más podemos hacer con un VO. Pues habrás notado declare los fields de Amount como readonly. Una de las caracteristicas más importantes de un VO es que sea inmutable y que una vez creado, no se pueda cambiar. Esto nos da muchas garantías sobre el estado interno de nuestro objeto.

Una acción que seguro vamos a necesitar hacer con nuestro Amount es sumarle otro Amount pero como dije antes no podemos modificar un VO una vez creado. Así que lo que podemos hacer es crear un método que recibe el Amount que queremos sumar y crea un VO nuevo que contiene el resultado.

add(amount: Amount): Amount {
    if (amount.currency !== this.currency) {
        throw new Error("Amounts of different currency cannot be added");
    }

    return new Amount(amount.value + this.value, this.currency);
}

Nuevamente aquí podemos ver que estamos validando que ambos VOs son del mismo currency antes de sumarlos. Una vez sabemos que son del mismo currency creamos una nueva instancia de Amount con el resultado de la suma.

Otra cosa importante es poder comparar dos VOs por su valor así que vamos a escribir un método para esto.

equals(amount: Amount): boolean {
    return amount.value === this.value && amount.currency === this.currency;
}

Fácil. Ahora podemos comparar dos VOs y saber si contienen el mismo valor. Es importante notar que estamos comparando tanto el value como el currency porque sabemos bien que un Euro y un Dólar no son lo mismo.

VOs en mi web app

Creo que esos son suficientes ejemplos simples. Ahora un último ejemplo de como se puede usar este VO en el contexto de nuestra aplicación web.

app.post(async (req, res) => {
    let {amount, currency} = req.body;

    try {
        let amount = new Amount(amount, currency);
        let response = await controller(amount);
        return res.json(response);
    } catch(error) {
        // do some error handling
        return res.send(400);
    }
});
async function controller(amount: Amount) {
    // do stuff with Amount
}

Ahí está. Ahora Amount nos ayuda a validar el input que estamos recibiendo y luego de que tenemos una instancia sabemos que es un valor que no necesitamos validar nuevamente. El resto de nuestro programa, siempre y cuando reciba una instancia de Amount, puede trabajar con el valor de forma segura y sin preocupación.

Además este VO nos ayuda a organizar el código y mantener las reglas de negocio cerca del los datos que las necesitan. La clase Amount tiene todo el código necesario para crear una instancia válida y las acciones (método) que podemos tomar con ese objeto.

Result

Como notaste, cada vez que queremos crear un VO, necesitamos hacer new y pasar los argumentos que hagan falta para crear la instancia. Como nuestro constructor puede throw siempre tenemos que asegurarnos de hacer try/catch para manejar cualquier error al momento de la instanciación.

Aunque esto no es el fin del mundo, bien rápido se puede hacer algo tedioso. Dicho eso, hay formas de mejorar la experiencia al usar VOs.

Todos mis VOs tienen un método estático llamado create. Este método me permite instanciar VOs sin temor a una excepción pero en vez de devolver una instancia de Amount, devulve una instancia de un objeto llamado Result que sirve para envolver el Amount que se creo o un Error si hubo problemas al crear el VO.

Esta idea de usar un objecto para envolver el resultado es algo muy común en otros lengajes como Rust y nos permite hacer cosas interesantes. Los detalles sobre Result los voy a dejar para un post futuro donde también hablare sobre como uso Maybe en mis programas.

Resumen

  • Los VOs nos ayudan a implementar reglas de negocio para un valor dentro de nuestro programa.
  • Los VOs deben ser inmutables.
  • Debe ser imposible instanciar un VO en un estado ilegal.
  • Una vez tenemos una instancia de VO podemos estar seguros que contiene un estado válido dentro de nuestro programa.

Foto: Unsplash

Published
Categorized as Default

Htmx y Lit

En nuestro equipo, tener un SPA para cada app es mucho trabajo adicional con muy poco beneficio. Muchas veces pasamos trabajo adicional para lograr que nuestros SPAs se comporten como web apps tradicionales con templates en el servidor. Muchos de los beneficios están relacionados a equipos grandes y ese no es nuestro caso.

Llevo un tiempo buscando cómo consolidar la lógica de negocio y compartirla entre los servidores y los SPAs para que siempre estén en perfecto sync, pero como todo el que ha tratado sabe es muy difícil. De vez en cuando haces un cambio en el cliente o en el servidor y se olvida actualizar su contra parte produciendo bugs.

Estos dos proyectos me parecen interesantes y les quiero dedicar más tiempo. Pienso que podrían ser una solución bastante completa para muchos de los web apps que manejo.

Htmx, escuché por primera vez sobre este proyecto hace unas semanas en un podcast. Esto no es una idea nueva pero creo que esta solución se ve aplicable a muchos casos comunes. En resumen la idea es crear interactividad del tipo que nos han acostumbrado los SPAs pero sin tener que crear un cliente completo. Básicamente es volver a como hacíamos las cosas antes cuando las siglas AJAX eran cool. Haces click en el UI, se envía un request al servidor y se responde con HTML nuevo que se injecta directamente en el DOM. Para sorpresa de los más jóvenes se puede lograr mucho con esta simple técnica.

Lit, aunque aparenta ser un rebranding de Polymer, se ve bastante bien y prometedor. Creo que la idea de usar Web Components, con el mínimo runtime posible en los lugares donde algo como Htmx no sea suficiente puede ser una buena alternativa. Siempre se puede usar el API nativo del browser pero parece que Lit ayuda a que el código sea mas declarativo y eso es bueno.

Cuando haga mas pruebas les contaré.

Published
Categorized as Default