Las aplicaciones modernas se interconectan cada vez más con muchos sistemas externos: API de terceros, bases de datos, colas de mensajes, etc.En algunos casos, podemos controlar la forma de los datos que se transmiten, pero en otros casos podemos estar a merced de otra cosa.si no tenemos cuidado, los detalles de implementación como la forma de los datos y la denominación de los campos pueden impregnar todo nuestro código.el diseño basado en el dominio (DDD) habla de construir una capa anticorrupción para aislar nuestro dominio de estos sistemas externos.
Excavemos en un ejemplo práctico de cómo esta tensión podría desarrollarse para una aplicación web de fondo. Los ejemplos están en TypeScript, pero los conceptos se aplican también a otros ecosistemas.
Qué no hacer
Empecemos por buscar una forma «mala» de consultar una base de datos y devolver algunos datos en un punto final de la API:
app.get($0027/usuarios$0027,async(req,res)={{{constusers=awaitquery($0027SELECT * FROM users$0027);res.send({users})})
Podrías pensar que esto es un poco poco poco ingenioso, y tendrías razón. Hablemos de algunas limitaciones concretas del enfoque.
¿Es sólo que todo el código está en el controlador de la ruta? Eso es ciertamente un síntoma, pero sólo extraer, digamos, una capa de acceso a los datos para nuestra consulta no ayuda mucho. ¿Por qué? Fíjese que en realidad no hemos declarado ningún tipo de TypeScript. Además, hicimos un SELECT * en lugar de sacar columnas específicas. Estamos tomando lo que sea que se devuelva de nuestra consulta y lo metemos en una carga útil JSON.
¿Qué pasa cuando se añade una columna a nuestra tabla? ¿Vamos a exponer accidentalmente algunos datos que no pretendíamos, o quebrar a un consumidor? ¿Los recién llegados a la base de código podrán ver de un vistazo qué datos se devuelven? ¿Qué pasa si queremos eliminar una columna que creemos que no se utiliza? ¿Seremos capaces de saberlo?
Escribiendo la respuesta de la base de datos
Intentémoslo de nuevo:
typeUserRow={nombre: cadena,fecha_de_nacimiento: número}app.get($0027/usuarios$0027,async(req,res)=[;{constantes: UserRow[]=awaitquery($0027SELECT * FROM users$0027);res.send({users})})
Esto es un poco mejor ya que nos da una idea del tipo de datos que regresan de la base de datos. También nos da algún tipo de seguridad si necesitamos hacer algún tipo de procesamiento de los datos. Sin embargo, todavía tiene algunos problemas.
Una representación para muchos usos
Hemos estado usando un solo tipo para representar los datos del usuario, desde el momento en que salen de la base de datos, hasta que se serializan a JSON.Un problema con esto es que todo está acoplado a cómo se representan los datos en la base de datos.Otro problema, algo específico de TypeScript, es que nuestro tipo de UserRow se borra en tiempo de ejecución.Si alguna columna además del nombre y la fecha_de_nacimiento se devuelve de la consulta, se filtrará a la respuesta JSON – aunque no aparezca en nuestro tipo de usuario.
DTOs y objetos de dominio
Los DDD distinguen entre objetos de dominio y objetos de transferencia de datos (DTO).los DTO son los objetos en los bordes de nuestra aplicación para la interfaz con otros sistemas, que a menudo utilizan estructuras de datos primitivas y sencillas.la forma suele estar dictada por algo más, como una API de terceros o un esquema de base de datos.los objetos de dominio son los objetos que creamos en el núcleo de nuestro programa, estructurados como queremos que sean, con nombres que son significativos para nosotros.
Vamos a crear tipos separados para los datos que salen de la base de datos, una representación de dominio y una representación serializada (JSON):
// DTO para la base de datos tipo tablaUserRow={nombre: cadena,fecha_de_nacimiento: número}// Tipo de objeto de dominioName=stringtypeUser={nombre: Nombre,fecha_de_nacimiento: Fecha}// DTO para el tipo de responsabilidad de la APIUserResponse={name: string,dateOfBirth: string}
Tenemos tres tipos distintos que son similares en estructura, pero con sus propias consideraciones.
Por ejemplo, UserRow tiene claves de caso de serpiente y utiliza un número (unix timestamp) para leer la fecha_de_nacimiento de la base de datos.
En nuestro objeto de dominio, dateOfBirth es una caja de camello y convertida en Date.name tiene un tipo de nombre que es sólo un alias para encadenar en este momento, pero que conlleva algún significado semántico adicional y podría evolucionar para ser un tipo distinto con una validación especial en el futuro.
En la respuesta del API, la fecha de nacimiento se convierte en una cadena para que podamos decidir explícitamente cómo dar formato a la fecha.
Aunque hubiéramos terminado con tipos idénticos, sigue siendo útil crearlos como tipos separados y trazar un mapa entre ellos, lo que permitirá a los tipos divergir según sea necesario en el futuro. Esto puede parecer que va en contra de «No te repitas» (DRY), pero el espíritu de DRY es consolidar las cosas que tienen el mismo significado semántico , no combinar las cosas que resultan tener la misma estructura .
Aquí están las funciones que definiremos para mapear entre nuestros tipos:
<pre>constfromUserRow=(row: UserRow):User=>({nombre: row.name,dateOfBirth: newDate(row. fecha_de_nacimiento)})consttoUserResponse=(usuario: Usuario):UserResponse=>({nombre: nombre.usuario,fechaOfBirth: usuario.fechaOfBirth.toUTCString()})</pre>
Aquí está nuestro nuevo controlador de ruta:
<pre>app.get('/usuarios',async(req,res)=>{constantes: UserRow[]=awaitquery('SELECCIONAR nombre, fecha_de_nacimiento_de_usuarios');constantes: User[]=rows.map(fromUserRow)//domain logic here, using our `User` Domain objectconstuserResponse: UserResponse[]=users.map(toUserResponse)res.send({data: userResponse})})</pre>
(Nótese que ahora estamos seleccionando explícitamente las columnas que necesitamos en nuestra consulta también, lo que apoya nuestros esfuerzos para ser explícitos sobre los datos a medida que fluyen a través de las diversas etapas).
Dividiendo en capas
Puede parecer un poco tonto ver los tres tipos utilizados en una sola función. En la vida real, podríamos dividir esto en diferentes archivos basados en las preocupaciones – tal vez una capa para el manejador de la ruta, una capa para el dominio y la lógica de negocio, y una capa para el acceso a los datos:
<pre>//users.route.tsimport{getUsers}de'./users.db'importar{User,transformUsers}de'./users. domain'typeUserResponse={name: string,dateOfBirth: string}consttoUserResponse=(user: User):UserResponse=>({name: user. nombre,fechaDeNacimiento: usuario.fechaDeNacimiento.aUTCString()})app.get('/usuarios',async(req,res)=>{constantesDeDb: User[]=awaitgetUsers()constusers: User[]=transformUsers(usersFromDb)constuserResponde: UserResponse[]=users.map(toUserResponse)res.send({data: userResponse})})</pre>
<pre>//users.domain.tsexporttypeName=stringexporttypeUser={nombre: Nombre, fecha de nacimiento: Date}exportconsttransformUsers=(users: User[]):User[]=>{//domain logic herereturnusers}</pre>
<pre>//users.db.tsimport{Usuario}de'./usuarios.dominio'typeUserRow={nombre: cadena,fecha_de_nacimiento: número}constfromUserRow=(fila: UserRow):Usuario=>({nombre: fila. name,dateOfBirth: newDate(row.date_of_birth)})exportconstgetUsers=async():Promise<User[]>=>{constuserRows: UserRow[]=awaitquery('SELECT name, date_of_birth FROM users');returnuserRows.map(fromUserRow)}</pre>
Hemos aislado UserResponse y UserRow a su propio archivo en los bordes de nuestra aplicación. Ni siquiera exportamos esos tipos – los mapeamos rápidamente a nuestro tipo de dominio de usuario y lo devolvemos.
TransformUsers es un sustituto de cierta lógica de dominio. Una vez más, habla en términos de Usuario y no tiene ningún concepto de cómo se representan los datos en otro lugar.
¡Piensa en la flexibilidad que esto nos compra! Podemos cambiar la representación externa de los datos en un solo archivo , sin tocar el código del dominio.Dentro de nuestro dominio, podemos usar nombres que son significativos para nosotros, y no necesariamente los usados en otros sistemas.Podemos usar las estructuras de datos apropiadas, por ejemplo, usar un Set en nuestro objeto Dominio, incluso si la representación JSON es un array.
Tipos y pruebas
He estado dando ejemplos en TypeScript.Aunque los tipos estáticos son útiles para documentar y comprobar la separación que he esbozado, no son estrictamente necesarios. Tanto si usas tipos como si no, también querrás alguna cantidad de pruebas de unidad e integración. Por ejemplo, hemos intentado capturar nuestro esquema de base de datos en el tipo UserRow, pero hasta que no ejecutemos realmente el código, no podemos estar completamente seguros de haber mapeado correctamente los nombres de las columnas y los tipos de datos. (Ver mi post acompañante, Domando los Datos Dinámicos en TypeScript , para patrones para tratar los datos dinámicos como este en los bordes).
Conclusión
Al igual que la práctica del desarrollo basado en pruebas nos hace reflexionar sobre el diseño (además de producir un conjunto de pruebas), reflexionar sobre cómo quieres estructurar los datos dentro de tu dominio es un ejercicio útil en sí mismo.
Categorías: technicalTags: javascript, typescript, architecture, ddd