Para empezar, las primitivas son los tipos de datos básicos disponibles en la mayoría de los idiomas. Estos incluyen tipos de datos como cadenas, números (int, flotantes) y booleanos.
La obsesión primitiva es un olor a código en el que los tipos de datos primitivos se usan en exceso para representar sus modelos de datos. El problema con las primitivas es que son muy generales. Por ejemplo, una cadena podría representar un nombre, una dirección o incluso una identificación. ¿Por qué es esto un problema?
A continuación se muestra un ejemplo de una clase de persona que sufre de obsesión primitiva:
public class Person { public Person ( string id, string firstName, string lastName, string address, string postcode, string city, string country ) { // initialisation logic } public string Id { get ; set ; } public string FirstName { get ; set ; } public string LastName { get ; set ; } public string Address { get ; set ; } public string PostCode { get ; set ; } public string City { get ; set ; } public string Country { get ; set ; } public void ChangeAddress ( string address, string postcode, string city, string country ) { // change address logic } }
¿Que esta mal aquí? Bueno, tenemos una clase que consta completamente de propiedades de cadena. El constructor consta de una larga lista de parámetros de cadena: ¡puedo garantizar que en algún momento se asignará el valor incorrecto a la ranura de parámetro incorrecta! También tenemos un método para cambiar la dirección, pero realmente esta lógica no debería ser responsabilidad de la clase Persona. Finalmente, la ID también es una cadena, por lo que podría usarse accidentalmente como ID para otros tipos.
Bueno, lo primero que podemos ver es, ¿hay propiedades que tengan sentido agruparlas? Una buena prueba para esto es preguntar, ¿cuál de estas propiedades es probable que se actualicen juntas? En nuestro ejemplo, está claro que los campos Dirección, Código postal, Ciudad y País deben agruparse (si se muda de casa, es probable que todas o la mayoría de estas propiedades se actualicen juntas). Refactoricemos estas propiedades en su propia clase entonces.
public class Address { public Address ( string address, string postCode, string city, string country ) { // initialisation logic } public string Address { get ; set ; } public string PostCode { get ; set ; } public string City { get ; set ; } public string Country { get ; set ; } } public class Person { public Person ( string id, string firstName, string lastName, Address address ) { // initialisation logic } public string Id { get ; set ; } public string FirstName { get ; set ; } public string LastName { get ; set ; } public Address Address { get ; set ; } }
Nuestra clase Person ya se ve mucho mejor. Toda la lógica relacionada con la dirección ahora está encapsulada en la clase de dirección y hemos logrado eliminar muchos parámetros de cadena del constructor.
Es posible que haya notado que, en general, todavía tenemos la misma cantidad de propiedades de cadena. Esto está bien ya que, en última instancia, es probable que necesite almacenar sus datos en una primitiva. La parte importante de evitar la obsesión primitiva es encapsular esas primitivas en objetos bien definidos que realmente representen su significado.
Lo siguiente que tenemos que tratar es la identificación. Para devolver la seguridad de tipo a la identificación, podemos crear lo que se conoce como una identificación fuertemente tipada. En resumen, este es solo el valor primitivo envuelto en un objeto contenedor específico para esa entidad. Por ejemplo, una implementación de un objeto PersonId podría verse como (adaptado de aquí ):
public readonly struct PersonId : IComparable<PersonId>, IEquatable<PersonId> { public string Value { get ; } public PersonId ( string value ) { Value = value ; } public static PersonId New ( ) => new PersonId(Guid.NewGuid().ToString()); public bool Equals ( PersonId other ) => this .Value.Equals(other.Value); public int CompareTo ( PersonId other ) => Value.CompareTo(other.Value); public override bool Equals ( object obj ) { if (ReferenceEquals( null , obj)) return false ; return obj is PersonId other && Equals(other); } public override int GetHashCode ( ) => Value.GetHashCode(); public override string ToString ( ) => Value.ToString(); public static bool operator ==(PersonId a, PersonId b) => a.CompareTo(b) == 0 ; public static bool operator !=(PersonId a, PersonId b) => !(a == b); } public class Person { public Person ( PersonId id, string firstName, string lastName, Address address ) { // initialisation logic } public PersonId Id { get ; set ; } public string FirstName { get ; set ; } public string LastName { get ; set ; } public Address Address { get ; set ; } }
Esto es bueno ya que ahora la clase Person usa su propio PersonId para la ID. No hay forma de que pueda usarse accidentalmente como ID para otro tipo, ya que tendrá su propia ID fuertemente tipada.
Sin embargo, es posible que haya notado que esta definición de PersonId es algo grande y sería engorroso tener que declarar esto para cada ID fuertemente tipado que desee crear. Para ser honesto, este obstáculo es probablemente la razón por la que los ID fuertemente tipados aún no son tan comunes en el desarrollo de C#.
Afortunadamente, en C# 9, tenemos el nuevo tipo de registro que facilita mucho la definición de ID fuertemente tipados, por lo que esperamos que su uso sea mucho más común (puede leer más sobre los registros aquí ).
// PersonId as a record public record PersonId ( string Value ) ; // how to initialise var personId = new PersonId( "my-id" );
Sí, eso es literalmente. Esto declara un registro PersonId con una propiedad de cadena llamada Valor, que se puede pasar a través del constructor. Los registros basan automáticamente la igualdad en los valores de sus propiedades, por lo que no es necesario agregar nada más en la declaración.
Aunque este artículo se ha centrado principalmente en C#, y todos los principios aplicados anteriormente se pueden usar en F#, hay una característica única en F# que creo que vale la pena mencionar.
Las uniones discriminadas son un tipo de datos en F# (y en muchos otros lenguajes, pero no en C#) que permiten devolver diferentes tipos de datos según la situación (para obtener más información sobre las uniones discriminadas, consulte aquí ). Un caso de uso común es el manejo de errores:
type Result < 'a > = | Data of 'a | Error of string
Este ejemplo anterior permitiría que una función devuelva algún tipo de datos genérico, pero si hay un error, devolverá una cadena.
Sin embargo, una unión discriminada de un solo caso puede actuar como un envoltorio para un tipo primitivo y brinda seguridad de tipo a sus funciones. Por ejemplo, podemos tener este registro de Persona:
type Person = { FirstName: string; LastName: string; EmailAddress: string; }
El problema aquí es que una dirección de correo electrónico no es realmente solo una cadena: tienen un formato específico y, por lo tanto, es posible que deseemos diseñar una lógica de validación específica para ella. Debido a esto, es mejor encapsularlo en su propio tipo. Con uniones discriminadas de un solo caso, esto es realmente fácil:
type EmailAddress = EmailAddress of string type Person = { FirstName: string; LastName: string; EmailAddres: EmailAddress; }
Ahora el registro de Persona solo aceptará direcciones de correo electrónico del tipo Dirección de correo electrónico.
En este artículo, he introducido el concepto de obsesión primitiva, qué problemas puede causar y cómo solucionarlos. Con suerte, puede quitar parte de esta información y usarla en su propio código base para crear un código más seguro y fácil de mantener.
Publico principalmente sobre desarrollo web de pila completa .NET y Vue (¡y pronto tal vez más contenido de F#!). Para asegurarse de no perderse ninguna publicación, siga este blog y suscríbase a mi boletín . Si te ha resultado útil esta publicación, dale me gusta y compártela. También puedes encontrarme en Twitter .
También publicado en https://dev.to/dr_sam_walpole/a-cure-for-primitive-obsession-14l6