A raíz del artículo publicado hace algunas semanas sobre las ventajas de usar diccionarios en lugar de listas, me llegaba vía comentarios un escenario en el que se utilizaba una clase List<T>
para almacenar objetos a los que luego se accedía mediante clave. Lo diferencial del caso es que dichos objetos tenían varias claves únicas a través de las cuales podían ser localizados.
Por verlo por un ejemplo, el caso era más o menos como el que sigue:
public class FriendsCollection
{
private List<Friend> _friends = new();
...
public void Add(Friend friend)
{
_friends.Add(friend);
}
public Friend? GetById(int id)
=> _friends.FirstOrDefault(f => f.Id == id);
public Friend? GetByToken(string token)
=> _friends.FirstOrDefault(f => f.Token == token);
}
Obviamente en este escenario no podemos sustituir alegremente la lista por un diccionario, porque necesitamos acceder a los elementos usando dos claves distintas. Pero, por supuesto, podemos conseguir también la ansiada búsqueda O(1) si le echamos muy poquito más de tiempo.
A veces, los problemas de rendimiento de las aplicaciones, o determinadas funcionalidades de ellas, vienen derivados del uso de estructuras incorrectas para almacenar los datos, ya sea en memoria, base de datos o en cualquier tipo de almacén.
En este post vamos a centrarnos en un caso específico que me he encontrado demasiadas veces en código real: el uso indebido del tipo List<T>
cuando sólo nos interesa buscar en esta colección por una propiedad que actúa como identificador único del objeto T
.
A veces no es necesario usar lenguajes esotéricos para crear un código que nadie sea capaz de entender a simple vista... de hecho, lo hacemos muy frecuentemente en nuestro día a día 😉. Basta nombrar inapropiadamente unas cuantas variables, acoplar y desacoplar sin criterio o usar una mala indentación para que nuestro código ya venga "ofuscado" de serie, sin usar ninguna herramienta externa.
Sin embargo hay otro nivel de maldad, que consiste en el abuso de la flexibilidad de sintaxis en algunos lenguajes para construir expresiones diabólicamente enrevesadas. Hace poco me topé por casualidad con un buen ejemplo de ello en JavaScript, un código que, a simple vista, es imposible de entender:
// ¿Qué retorna esta expresión?
(_$=($,_=[]+[])=>$?_$($>>+!![],($&+!![])+_):_)(255)
Obviamente, podemos copiarla y pegarla en la consola de nuestro navegador, y rápidamente veremos de qué se trata. Sin embargo, me pareció interesante dedicar unos minutos a intentar comprender el código, así que vamos a ir troceando y refactorizando esta expresión ilegible hasta convertirla en algo que, al menos, podamos digerir.
Pero aunque indudablemente los tipos booleanos o flags son una fórmula muy compacta para almacenar información, el mundo suele ser mucho más complejo y estas simplificaciones son a menudo origen de problemas y trampas para nuestro yo del futuro.
En este post vamos a ver algunos escenarios en los que este tipo de dato puede llegar a complicarnos la vida.
Presto y obediente, el browser interpretará esta orden navegando hacia la URL indicada en el encabezado
location
del resultado, es decir, generando una nueva petición de tipo GET
y mostrando al usuario la página obtenida.Un ejemplo del workflow de peticiones y respuestas de este tipo podría ser la siguiente:
// Petición:
GET /home/articles/welcome-to-my-blog.html HTTP/1.1
Host: www.myserver.com
// Respuesta:
HTTP/1.1 301 Moved Permanently
Location: http://www.myserver.com/blog/welcome-to-my-blog.html
// Nueva petición:
GET /blog/welcome-to-my-blog.html HTTP/1.1
Host: www.myserver.com
...
La diferencia entre el código 301 y 302 es que el primero de ellos indica al agente de usuario (sea un browser o aplicación cliente) que la redirección es permanente, esto es, que puede almacenar localmente la nueva ubicación y utilizarla en el futuro con seguridad en lugar de la que se usó originalmente. El código 302, en cambio, indica que la nueva ubicación es temporal y sólo debe ser utilizada en esta ocasión para dirigir la petición al lugar correcto.Los códigos de estado HTTP 301 y 302 son válidos en la mayoría de escenarios, y permiten solucionar problemas como, entre otros, el cambio de ubicación nuestros recursos sin perder posicionamiento en buscadores, o implementar el patrón Post-Redirect-Get para mejorar un poco la experiencia de usuario y evitar dobles envíos de información en formularios.
Sin embargo, hay ocasiones en que la solución queda algo corta. Por ejemplo, si cambiamos de URL el endpoint de un servicio programado exclusivamente para ser invocado mediante peticiones de tipo
POST
o PUT
, lo que nos interesaría sería que las peticiones a la dirección original retornaran una redirección indicando la nueva ubicación pero también informando al browser de que utilice sobre ella el mismo verbo de la petición original.Por ello, y algunas otras razones que veremos después, el estándar HTTP amplió, hace ya bastante tiempo, el conjunto de códigos de redirección con tres nuevos miembros: HTTP 303, 307 y 308. Los dos primeros formaron parte de HTTP 1.1, mientras que el código 308 fue añadido en la RFC 7538 algo más adelante.
Veamos para qué sirve cada uno de ellos.
Probablemente sabréis (y si no, podéis echar un vistazo a este post) que existen diversas fórmulas para añadir servicios o componentes al contenedor de dependencias, y una de ellas es la denominada "Scoped". Estos componentes quedan registrados asociados a un ciclo de vida específico (
ServiceLifetime.Scoped
), que indica que cada vez que haya que se solicite un objeto de este tipo se retornará la misma instancia dentro el ámbito actual, que normalmente coincide con el proceso de una petición específica. O en otras palabras, dentro de la misma petición, la instancia del componente será compartida por todos los objetos que dependan de él. Además, al finalizar el proceso de la petición, el framework invocará su método
Dispose()
(si existe, claro), permitiendo liberar los recursos que haya utilizado. Por ejemplo, suele ser una buena práctica emplear esta técnica para reutilizar un contexto de datos entre los servicios que participan en el proceso de una petición que accede a una base de datos, y asegurar su liberación final para no dejar conexiones abiertas.Pero este post no va de ver cómo registrar estos componentes o de cómo utilizarlos en nuestras aplicaciones, sino de responder una simple curiosidad: ¿quién se encarga de crear este scope cuando comienza una petición y de liberarlo cuando finaliza? Ya, sé que no tiene mucha utilidad práctica en el día a día, pero es una buena excusa para echar un vistazo y entender cómo funcionan las cosas por dentro ;)
<Disclaimer>Todo lo que comentaremos a continuación son detalles de la implementación actual del framework y son válidos en la versión 1.1 de ASP.NET Core, pero podrían cambiar en futuras versiones.</Disclaimer>