2020/05/05

Este no es un artículo sobre DDD

Hace poco publiqué en twitter dos mensajes sobre la relación entre Domain Driven Design y la fase de análisis o diseño que siempre nos recalcaron en la universidad:

DDD es la parte de la programación orientada a objetos en la que más énfasis nos hicieron durante la Universidad, pero que olvidamos por andar pensando en entregar rápido. Ahora toca estudiarla otra vez, para arreglar el despelote que también nos advirtieron que íbamos a armar.
En esa época tenía un nombre menos fancy: le llamaban análisis. Con variantes como “divide y vencerás”, “primero resuelve el problema y luego escribe el código”, “identifica el contexto”, etc.
 
Yo no soy experto en DDD -lo escuché por primera vez en septiembre de 2019-, pero después de estudiarlo un poco vi que muchos de los conceptos ya los conocía. Empezaron a encajar en vacíos que tenía en mi código y caí en cuenta que había cometido muchos errores en la fase de diseño. El principal de ellos: no dedicar suficiente tiempo a identificar todos contextos del problema, es decir, no dividir para vencer... o dividir mal. Esa fase duró casi un mes; no quiero imaginar con qué hubiera terminado de haberla ignorado totalmente.

Quiero hablar de contexto y no entrar en tecnicismos de Bounded Context, Aggregates y el resto del lenguaje que nos propone la metodología. Contexto en el sentido de que una entidad puede ser distinta dependiendo del requerimiento específico en el que estamos trabajando.

¿Qué me alertó? primero, crear una clase que representa la misma entidad pero con menos atributos -hasta allí todo bien- y ponerle un nombre distinto -¡OUCH!-. Este ejemplo puede parecerte familiar:
Supongamos un perfil de usuario con muchos datos de contacto. Es probable que la base de datos tenga una tabla profile con columnas userId, name, lastName, email, address, phoneNumber y bio. Seguramente existe la clase Profile que tiene todos los atributos/propiedades:

public class Profile
{
    public Guid UserId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Address { get; set; }
    public string PhoneNumber { get; set; }
    public string Bio { get; set; }
}
 

Sabemos de programación orientada a objetos, así que tenemos una clase User muy parecida a esta:

public class User
{
    public Guid Id { get; }
    public Profile Profile { get; }

    private IList<Task> _tasks;
    public IList<Task> Tasks => _tasks.AsReadOnly();

    public User(Guid id, Profile profile)
    {
        Id = id;
        Profile = profile;
        _tasks = new List<Task>();
    }

    public User(Guid id, Profile profile, IEnumerable<Task> tasks)
        : this(id, profile)
    {
        _tasks.AddRange(tasks);
    }

    public void AddTask(Task t)
    { 
        _tasks.Add(t);
    }
}
Pero conforme la aplicación crezca irán apareciendo páginas en donde debemos mostrar información básica del usuario, por ejemplo, el nombre y el correo electrónico. Como sabemos que cargar todo el perfil sería un desperdicio de recursos, podríamos darle vía libre a nuestros némesis: SimpleProfile, ShortProfile, ProfileDetails, DetailedProfile, ExtendedProfile, ... Como sea que se llamen. Y Profile es sólo una de las quince entidades que tenemos.

Piensen por un instante en qué sucede con la clase User, que tiene una referencia a Profile. Sin darnos cuenta nos olvidamos de ella y destruimos la orientación a objetos de nuestro software. ¿O cuál de todas las versiones de Profile usamos allí?. A estas alturas ya tenemos servicios que retornan trozos de información que de alguna forma se enlazan con la interfaz gráfica. La lógica de negocios quedó en esos servicios y no en las entidades, como nos enseñaron. Los métodos entre capas ya no reciben objetos sino parámetros independientes según las distintas funcionalidades. ¿Les parece familiar?.

Qué tal esta invocación:

_ = await _store.CreateProfile(
    id: Guid.NewGuid(),
    name: profile.Name,
    lastname: profile.Lastname,
    email: profile.Email,
    address: profile.Address,
    phoneNumber: profile.PhoneNumber,
    bio: profile.Bio
);
 
Cuando pudo ser:

_ = await _store.SaveProfile(profile);
// o dependiendo del diseño:
_ = await _store.SaveProfile(user.Profile);
 
O qué tal cargar las tareas directamente y mostrarlas en la interfaz:

var tasks = await.GetTasksForUser(id);
var model = tasks.Select(t => MapTaskToViewModel(t));
 
Cuando nos insistieron un montón en respetar las jerarquías:

var user = await GetUser(id);
var model = user.Tasks.Select(t => TaskViewModel.FromTask(t));
 
Allí es cuando debemos parar, tomar lápiz y papel y volver a diseñar. Esta vez recordando que hay distintos contextos, que se vale tener varias clases Profile y que algunas tendrán lógica de negocios y otras serán de sólo lectura. Que es válido retornar o recibir como parámetros instancias de Profile o de otras clases en distintas capas de nuestro software, siempre que estemos en el mismo contexto.

Si la aplicación es pequeña, vale el esfuerzo hacer el refactoring. Si no, es mejor esperar e ir reparando conforme las nuevas funcionalidades lo requieran. Créanme que en muy poco tiempo habrán cubierto todo el código sin tomar riesgos innecesarios.

Seguro hay casos sencillos en donde sirve una entidad como SimpleProfile. Se me ocurre el de mostrar el nombre de usuario y la foto de perfil en el menú. Pero hay que saberlo manejar y ser conscientes de que es una libertad puntual que nos estamos dando como desarrolladores; una excepción y no la regla.

DDD me gusta mucho y creo que todos debemos estudiar -al menos- sus fundamentos, pero debo decir que no es una novedad. Allí no hay nada distinto a lo que aprendí entre 2004 y 2009 en la Universidad. Mis profesores de algoritmos siempre enseñaron lo que debían enseñar: conceptos que nunca cambian. Me acordé mucho de ellos mientras escribía este post.

2020/05/01

Mis indispensables de C#. Parte 3: Configuraciones

Esta es una serie de 3 artículos cortos sobre características de C# que uso en todas mis aplicaciones.

Configurar el serializador de JSON


Cuando quiero usar la misma configuración en toda la aplicación, creo mi propio serializador. Este esquema me permitió migrar fácilmente desde Newtonsoft.Json a System.Text.Json. Reconocimiento especial al método ToHttpJsonContent()... parece que .NET 5 va a incluir extensiones al HttpClient que ayudan a eso.

using System.Net.Http;
using System.Text;
using System.Text.Json;

namespace MyApplication.Helpers
{
    public static class JsonGenerator
    {
        public static readonly JsonSerializerOptions serializerOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };

        public static readonly JsonSerializerOptions deserializerOptions = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        };

        public static string Serialize(object obj)
        {
            return JsonSerializer.Serialize(obj, serializerOptions);
        }

        public static StringContent ToHttpJsonContent(object obj)
        {
            return new StringContent(Serialize(obj), Encoding.UTF8, "application/json");
        }

        public static T Deserialize<T>(string content)
        {
            return string.IsNullOrWhiteSpace(content) ?
                default(T) :
                JsonSerializer.Deserialize<T>(content, deserializerOptions);
        }
    }
}

Configurar el cliente HTTP


Usando inyección de dependencias, es posible configurar el IHttpClientFactory con algunos parámetros fijos.

services.AddHttpClient("myClient", client =>
{
    client.BaseAddress = new Uri(Configuration["ConfigGroup:BaseUrl"]);
    client.DefaultRequestHeaders.Add("fn-key-header", Configuration["ConfigGroup:Key"]);
});

services.AddScoped<IMyService, MyService>();

Así se reduce mucho el código del servicio.

public class MyService : IMyService
{
    private readonly IHttpClientFactory _clientFactory;

    public MyService(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task<MyDTO> GetDTO(Guid id)
    {
        var client = _clientFactory.CreateClient("myClient");
        var response = await client.GetAsync($"/api/functionName/{id}");

        if (response.IsSuccessStatusCode)
        {
            var body = await response.Content.ReadAsStringAsync();
            var element = JsonGenerator.Deserialize<MyDTO>(body);
            return element;
        }

        return null;
    }
}