Inmutabilidad del código

 

Llegamos a otro punto que yo entiendo como primordial a la hora de hacer aplicaciones, la inmutabilidad de los objetos. Lo que viene significando que los objetos no puedan cambiar de estado una vez están creados.

Comúnmente la inmutabilidad es algo visto o enfocado a F# o a programación funcional, pero si algo funciona bien, porque no comprenderlo y adaptarlo si es posible. De esta forma podemos adaptar nuestro código a lo que se denomina “Arquitectura Inmutable”. 

Nota: No he programado en F# en la vida.

 

1 - Diferencia entre una clase mutable y una Inmutable

Como su nombre indica, una clase mutable, es aquella que puede mutar o cambiar una vez está creada. Es decir, podemos actualizar sus valores. Comúnmente en C# lo hacemos a través de un `setter`

Por contrario, inmutable es que la clase NO puede cambiar una vez está creada.

En C# disponemos de dos opciones para la inmutabilidad:

Palabra clave “const” que sirve para crear constantes.

Palabra clave “readonly” que sirve para que esa propiedad sea únicamente de lectura.

Aunque la inmutabilidad no acaba con estas dos palabras claves.

 

2 - Por qué preocuparnos por que nuestro código sea inmutable?

El motivo principal por el que hacer código inmutable es muy importante es porque así podemos prever una gran cantidad de errores.

Para los que estáis empezando, os daréis cuenta que códigos que tienen todas las clases mutables son mucho má difíciles de mantener y mucho más difícil de debuguear que los que son inmutables. Y eso por no hablar de código asíncrono que lo complica mucho más. 

Estos errores comúnmente vienen de que cierta propiedad no contiene valor cuando por la lógica de nuestra aplicación si debería tenerlo. Esto es debido a que podemos asignar el valor a una propiedad en cualquier parte del objeto, mientras que si lo hacemos readonly, únicamente podremos hacerlo en el constructor.

2.1 - Actualizar objetos inmutables

Un dato “curioso” sobre la inmutabilidad es que para actualizar un valor o una propiedad, no la actualizamos como tal a través de un setter, sino, que lo que hacemos es una copia de la misma con el valor actualizado. 

public class EducationEntity
{
    public readonly int Id;
    public readonly DateTime StartDate;
    public readonly DateTime? EndDate;
    public readonly string CourseName;
    public readonly string UniversityName;

    private EducationEntity(int id, DateTime startDate, DateTime? endDate, string courseName, string universityName)
    {
        Id = id;
        StartDate = startDate;
        EndDate = endDate;
        CourseName = courseName ;
        UniversityName = universityName;
    }

    public static EducationEntity Create(int id, DateTime startDate, DateTime? endDate, string courseName, string universityName)
    {
        return new EducationEntity(id, startDate, endDate, courseName, universityName);
    }

    public EducationEntity UpdateEndDate(DateTime? endDate)
    {
        return EducationEntity.Create(Id, StartDate, endDate, CourseName, UniversityName);
    }
}

Como podemos ver en el ejemplo, todas las propiedades son “readonly” por lo que tienen que ser creadas a través del constructor. Y actualizamos la fecha de finalización a través del método `UpdateEndDate` haciendo una “copia” del mismo objeto únicamente cambiando la propiedad que necesitamos. En un escenario normal, actualizaremos todos los valores necesarios, en este caso es un único valor para poder mostrar el ejemplo claramente.

nota: este código es de la serie web C#

2.2 - Beneficios del código inmutable

  1. Más fácil de leer y mantener.
    • Si implementamos un patrón de inmutabilidad en nuestro código, haremos que el mismo sea mucho más legible y fácil de entender. 
  2. Código más seguro
    • La validación de las variables se realiza una única vez, en el constructor. Una vez la instancia de la clase, la cual es inmutable, está creada, podemos estar seguros al 100% de que esa clase es válida. Los que nos previene de tener un “state” invalido.
  3. Seguro ante código asíncrono
    • Seguro cuando trabajamos con programación asíncrona. Como el código que vamos a crear es inmutable, no tendremos ningún problema de sincronización entre hilos ya que la información no debería cambiar. 
  4. Mejor Encapsulación
    • Pasar nuestro objeto a través de distintos métodos algunas veces puede llevar a cambiar algunos de sus valores, esto con código inmutable NO es posible, ya que no podemos cambiar sus valores.
  5. Fácil de testear
    1. Es obvio que si nuestras clases no pueden cambiar una vez están creadas, realizar test sobre las mismas será mucho más fácil y por supuesto simple ya que nuestro código si es inmutable estará escrito para evitar “efectos secundarios” que pueden venir de cambiar el valor de algunas de las propiedades. 

 

3 - Ejemplo de inmutabilidad en el código

Para este ejemplo voy a realizar un service de la web que he mencionado antes, por ello tenemos un endpoint que lo que hace es leer nuestro perfil personal de la base de datos. 

El perfil es un DTO que contiene la información del usuario, como puede ser el nombre, apellidos y por supuesto el id. Además de esta información también disponemos de una lista de habilidades que queremos destacar en nuestro perfil. Pero estas habilidades están en otra tabla de la base de datos. 

La primera vez que leemos el usuario de la base de datos, lo hacemos utilizando un id de texto, no numérico, el motivo es porque esta información viene de la URL que el usuario está utilizando para consultar nuestra web y para que sea mejor experiencia para el usuario, nuestra URL es algo asi webpersonal.com/perfilpersonal/ivanabad como vemos esta ivanabad en la URL en vez de estar el id que corresponde. 

Este es el modelo o las clases que vamos a utilizar.

public class UserIdEntity
{
    public readonly string UserName;
    public readonly int UserId;
}
public class PersonalProfileEntity
{
    public readonly int UserId;
    public readonly string FirstName;
    public readonly string LastName;
    public readonly string Description;
    public readonly string Phone;
    public readonly string Email;
    public readonly string Website;
    public readonly string GitHub;
}
public class InterestEntity
{
    public readonly int UserId;
    public readonly string Description;
}
public class SkillEntity
{
    public readonly int UserId;
    public readonly int Id;
    public readonly string Name;
    public readonly int Punctuation;
}

Como vemos tenemos un objeto central que nos enlaza la url con el ID de la base de datos.

Además el objeto principal es`PersonalProfileEntity y SkillEntity e InteresEntity son objetos que irán dentro del dto`PersonalProfileDto

Nota: por espacio, he quitado los constructores. 

El objeto`UserIdEntity no es necesario realmente, esta para que sea un ejemplo más entendible.

Y lo que vamos a hacer es crear un servicio que va a devolver ese dto, a simple vista está claro, necesitamos las  4 entidades mencionadas previamente, así que vamos a crear un servicio (mutable) donde disponemos de estas entidades.

Nota: el servicio va a estar implementado utilizando programación asíncrona.

public class PersonalProfileService
{
    private readonly IPersonalProfileServiceDependencies _dependencies;
    private UserIdEntity UserId { get; set; }
    private List<InterestEntity> Interests { get; set; }
    private List<SkillEntity> Skills { get; set; }
    private PersonalProfileEntity PersonalProfile { get; set; }

    public PersonalProfileService(IPersonalProfileServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }

    public Task<PersonalProfileDto> GetPersonalProfileDto(string name)
    {
        throw new NotImplementedException();
    }
}

Como vemos pasamos las dependencias a través de inyección de dependencias al constructor.

En otro vídeo veremos cómo estructurar una aplicación para evitar al cien por cien referencias circulares.

Aquí la interfaz de las dependencias:

public interface IPersonalProfileServiceDependencies
{
    Task<UserIdEntity> GetUserId(string name);
    Task<List<InterestEntity>> GetInterests(int userId);
    Task<List<SkillEntity>> GetSkills(int id);
    Task<PersonalProfileEntity> GetPersonalProfile(int id);
}

Y ahora únicamente necesitamos métodos que reciban esas entidades y asignamos los valores en el método GetPersonalProfileDto.

public class PersonalProfileService
{
    private readonly IPersonalProfileServiceDependencies _dependencies;
    private UserIdEntity _userId { get; set; }
    private List<InterestEntity> _interests { get; set; }
    private List<SkillEntity> _skills { get; set; }
    private PersonalProfileEntity _personalProfile { get; set; }

    public PersonalProfileService(IPersonalProfileServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }


    public async Task<PersonalProfileDto> GetPersonalProfileDto(string name)
    {
        await GetUserId(name);
        _ = Task.WhenAll(
            GetInterests(),
            GetSkills(),
            GetPersonalProfile()
        );

        return Map();
    }

    private async Task GetUserId(string name)
    {
        _userId = await _dependencies.GetUserId(name);
    }

    private async Task GetInterests()
    {
        _interests = await _dependencies.GetInterests(_userId.UserId);
    }
    private async Task GetSkills()
    {
        _skills = await _dependencies.GetSkills(_userId.UserId);
    }

    private async Task GetPersonalProfile()
    {
        _personalProfile = await _dependencies.GetPersonalProfile(_userId.UserId);
    }

    private PersonalProfileDto Map()
    {
        return new PersonalProfileDto()
        {
            Description = _personalProfile.Description,
            Email = _personalProfile.Email,
            FirstName = _personalProfile.FirstName,
            LastName = _personalProfile.LastName,
            GitHub = _personalProfile.GitHub,
            UserId = _userId.UserId,
            Phone = _personalProfile.Phone,
            Website = _personalProfile.Website,
            Interests = _interests.Select(a => a.Description).ToList(),
            Skills = _skills.Select(a => new SkillDto()
            {
                Id = a.Id,
                Name = a.Name,
                Punctuation = a.Punctuation
            }).ToList()
        };
    }
}

Bueno como vemos si hiciéramos nuestro código mutable, ya tendríamos el servicio terminado, pero desafortunadamente, este estilo de programación tiene varios fallos.

Primero, mutamos el state de la clase en la propia clase, asignando los valores de sus propiedades en métodos. 

Segundo, en nuestro método principal GetPersonalProfileDto tenemos una dependencia muy importante a tener en cuenta, que es el orden de los métodos a los que llamamos, no hay nada que nos prevenga o indique el orden. podríamos llamar al método GetSkills antes de GetUserId y no hay nada que nos indica que lo estamos haciendo mal, ya que el código compila correctamente. 

Para arreglar este entuerto, lo único que debemos hacer es hacer nuestra clase inmutable, lo que lleva a devolver de los métodos el tipo necesario y para hacer las consultas pasar por parámetro el id que necesitamos. 

public class PersonalProfileService
{
    private readonly IPersonalProfileServiceDependencies _dependencies;


    public PersonalProfileService(IPersonalProfileServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }


    public async Task<PersonalProfileDto> GetPersonalProfileDto(string name)
    {
        UserIdEntity userid = await GetUserId(name);
        List<InterestEntity> interests = await GetInterests(userid);
        List<SkillEntity> skills = await GetSkills(userid);
        PersonalProfileEntity personalProfile = await GetPersonalProfile(userid);

        return Map(userid, personalProfile, interests, skills);
    }

    private async Task<UserIdEntity> GetUserId(string name) =>
        await _dependencies.GetUserId(name);


    private Task<List<InterestEntity>> GetInterests(UserIdEntity userId) =>
        _dependencies.GetInterests(userId.UserId);

    private Task<List<SkillEntity>> GetSkills(UserIdEntity userId) =>
    _dependencies.GetSkills(userId.UserId);


    private Task<PersonalProfileEntity> GetPersonalProfile(UserIdEntity userId) =>
        _dependencies.GetPersonalProfile(userId.UserId);


    private PersonalProfileDto Map(UserIdEntity userId, PersonalProfileEntity personalProfile, List<InterestEntity> interests, List<SkillEntity> skills)
    {
        return new PersonalProfileDto()
        {
            Description = personalProfile.Description,
            Email = personalProfile.Email,
            FirstName = personalProfile.FirstName,
            LastName = personalProfile.LastName,
            GitHub = personalProfile.GitHub,
            UserId = userId.UserId,
            Phone = personalProfile.Phone,
            Website = personalProfile.Website,
            Interests = interests.Select(a => a.Description).ToList(),
            Skills = skills.Select(a => new SkillDto()
            {
                Id = a.Id,
                Name = a.Name,
                Punctuation = a.Punctuation
            }).ToList()
        };
    }
}

Como vemos ya NO tenemos las propiedades en la clase, sino que las devolvemos de los métodos necesarios. y en caso de que quisiéramos poner el método GetSkills el primero de todos, el código no nos dejaría, ya que no compilara debido a la dependencia en su parámetro de entrada.

He querido hacer el ejemplo en un servicio porque me parece que la funcionalidad que quiero transmitir se ve mucho mas clara.

 

Conclusión

Crear código inmutable tiene múltiples ventajas, entre las que podemos incluir la que para mi es la más importante que es fácil de leer y mantener, ya que es una parte primordial en nuestro trabajo diario. 

No todo el código tiene que ser siempre inmutable, puede ser que tengamos algún caso en el que debamos ser capaces de modificar las propiedades, así que no debemos limitarnos siempre a la inmutabilidad.

En el ejemplo hemos visto un caso de uso donde hacemos un servicio inmutable, pero podemos extender esta lógica a lo largo de toda nuestra aplicación. 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café