Value Objects en .NET: la cura contra el code smell Primitive Obsession
Value Objects eliminan el code smell “Primitive Obsession” al dar identidad a los tipos del dominio

Hey devs, nos ponemos técnicos y vamos directo al corazón de muchos de los problemas de código allá afuera. Este tema lo enseño en un curso de código limpio que dicto, y considero que es tan importante que quise hacerle su propio artículo, así que empecemos!

Uno de los code smells más comunes y a menudo invisibles en el desarrollo diario es “Primitive Obsession”.

Ocurre cuando usamos tipos primitivos (string, int, decimal, etc.) para representar conceptos del dominio que merecen una identidad propia.

Aquí te hablo más acerca de los code smells.

Por ejemplo, mira este código:

public class Order
{
    public string Email { get; set; }
    public decimal Amount { get; set; }
}

A simple vista, no tiene nada de malo, es una clase común y silvestre.

Pero a un ojo entrenado, como asumo que tú quieres ser, sí contiene potenciales problemas.

Te repito, a simple vista parece correcto, pero hay un problema:

Email y Amount no son simples primitivos en el dominio. Son conceptos con reglas:

  • Un email debe tener un formato válido.
  • Un monto no puede ser negativo.
  • Etc...

Si dejamos esas validaciones dispersas en el código, terminaremos con duplicación, inconsistencias y lógica regada por todas partes.
Ahí es donde entran los Value Objects.

Qué es un Value Object

Un Value Object es un tipo que:

  • Representa un concepto del dominio (por ejemplo, EmailAddress, Money, PhoneNumber).
  • No tiene identidad propia (dos Money(10, "USD") son iguales si tienen los mismos valores).
  • Encapsula reglas y comportamientos que pertenecen al concepto.

Este concepto de Value Object nace en Domain Driven Design, es decir, allí se formaliza, pero no pertenece sólo a DDD, al ser una idea general de buen diseño orientado a objetos se puede aplicar a cualquier arquitectura.

Ejemplo real en C#

Reescribamos el ejemplo anterior usando Value Objects:

public record Email
{
    public string Value { get; }

    public Email(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Email cannot be empty.");

        if (!value.Contains("@"))
            throw new ArgumentException("Invalid email format.");

        // Puedes añadir todas las validaciones que necesites...

        Value = value;
    }

    public override string ToString() => Value;
}

public record Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative.");
        if (string.IsNullOrWhiteSpace(currency))
            throw new ArgumentException("Currency is required.");

        Amount = amount;
        Currency = currency;
    }

    public override string ToString() => $"{Amount} {Currency}";
}

Y la entidad ahora luce mucho más limpia:

public class Order
{
    public Email CustomerEmail { get; private set; }
    public Money Total { get; private set; }

    public Order(Email email, Money total)
    {
        CustomerEmail = email;
        Total = total;
    }
}

Ahora, Order se centra en su propósito (gestionar órdenes) y no en validaciones de bajo nivel.
El dominio se vuelve expresivo, seguro y autoexplicativo.

En conclusión dev...

Los Value Objects no son exclusivos de DDD o Clean Architecture, son una buena práctica universal.
Usarlos te ayudará a:

  • Eliminar el code smell “Primitive Obsession”.
  • Centralizar reglas del dominio.
  • Hacer tu código más claro y menos propenso a errores.

Así que ya sabes, estimado dev, cada vez que un tipo primitivo tenga significado propio en el dominio, conviértelo en un Value Object.

¿Te encantó esta entrada, no? Haz tu buena obra del día y comparte este artículo entonces en tus redes sociales y por supuesto a todo tu equipo de ingeniería 😉🐿️

Créditos de imagen de portada: Basado en Foto de Amy Baugess en Unsplash

3 comentarios en «Value Objects en .NET: la cura contra el code smell Primitive Obsession»

  1. Gerson, buenos días

    No es relacionado al tema publicado, pero tengo un problemón, espero me puedas apoyar. Estoy en un proyecto WebForms y estoy realizando la petición http para obtener un token a una aplicación de Azure, el problema es que pareciera que WebForms no maneja muy bien las funciones async y los await que no puedo ni obtener el token. Ya he creado un servicio de Windows con las mismas credenciales, pero ahora no puedo en WebForms. La versión es 4.6 de .NET Framework 🙁

    1. Hola Jorge, este tipo de problema es muy común, y no es que lo estés haciendo mal sino que el problema es que WebForms no fue diseñado originalmente para trabajar con código asíncrono moderno, especialmente antes de .NET 4.7.
      Ya buen tiempo que no he tocado código webforms, sin embargo si no estoy mal, podrías intentar esto (voy a poner el código raw 😄):


      public partial class TokenPage : Page
      {
      protected void Page_Load(object sender, EventArgs e)
      {
      RegisterAsyncTask(new PageAsyncTask(ObtenerTokenAsync));
      }

      private async Task ObtenerTokenAsync()
      {
      using (var client = new HttpClient())
      {
      var body = new StringContent(
      "grant_type=client_credentials&client_id=TU_CLIENT_ID&client_secret=TU_CLIENT_SECRET&scope=https://graph.microsoft.com/.default",
      System.Text.Encoding.UTF8, "application/x-www-form-urlencoded");

      var response = await client.PostAsync("https://login.microsoftonline.com/TU_TENANT_ID/oauth2/v2.0/token", body);
      response.EnsureSuccessStatusCode();

      var json = await response.Content.ReadAsStringAsync();
      // Guardas el token en Session o variable global
      Session["Token"] = json;
      }
      }
      }

      Y no olvides añadir Async=»true» en la directiva @Page.

      Saludos y espero te sirva!
      Comparte el blog.

  2. Qué bien escrito, la idea de evitar la primitive obsession me hizo reír porque mi código a veces actúa como si fuera un robot obsesionado con variables básicas. Y la parte del code smell me recordó cuando mi PC decía ¡Error! porque el código era tan ruidoso que ni yo mismo lo entendía… Pero bueno como dices, cada vez que un tipo primitivo se ponga a hablar en serio mejor lo convertimos en Value Object! 😉

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *