Fundamentos de la POO con C# [10/10]: Los principios SOLID
La aplicación de estos 5 principios significará un despegue como programador o un estancamiento, así de importante son 💪

Contenido de la Serie de artículos

Estos son todos los temas de la serie:

  1. 1. La Programacion orientada a objetos (POO) y el diseño orientado a objetos (DOO)
  2. 2. Los requerimientos: funcionales y no funcionales
  3. 3. Objetos y clases
  4. 4. Tipos de datos en C#: Por valor y por referencia
  5. 5. Constructores
  6. 6. Signaturas e Interfaces
  7. 7. Clases especiales: estáticas y abstractas
  8. 8. Modificadores de acceso
  9. 9. Los 4 Pilares de la POO: Herencia, abstracción, encapsulamiento y polimorfismo
  10. 10. Los Principios SOLID

Principios en el Software

Para empezar debemos entender bien qué se entiende por "principios" cuando hablamos de Software.

Un principio es una base, desde la cual se asientan:

  • Técnicas
  • Metodologías
  • Herramientas y
  • Métodos

Estos elementos no son exclusivos de la Ingeniería de software, sino que son propios de toda ciencia y proceso creativo, por su puesto la ingeniería de software no está excenta.

Es aquí donde entra a tallar dos grupos de principios en la Ingeniería de Software: GRASP y sobretodo SOLID. Este último es el más importante de todos y el cual te detallaré a continuación.

¿Qué es SOLID?

Es un acrónimo en el cual cada una de sus cinco letras representa un principio, entonces SOLID son 5 principios aplicados al desarrollo de software.

Fueron propuestos a principios de los 2000 por Robert Martin.

El hecho de seguir estos principios aumenta significativamente la calidad en nuestro software al obtener dos de las cualidades más deseadas en todo software:

  • Mantenibilidad
  • Escalabilidad

En resumen SOLID serían principios que nos guían para desarrollar un mejor software ya que están basados en buenas prácticas que la industria maneja.

Explicaré cada uno de los principios y acompañaré algunos con ejemplos con código. Más adelante en este blog haré un artículo con un ejemplo con sólo código aplicando SOLID.

Single Responsibility (SRP)


El primer principio es el de responsabilidad única o en inglés Single responsibility Principle de ahí devienen sus siglas SRP.

Este principio postula que una clase debe tener sólo una razón para existir.

Con esto evitamos una mala práctica conocida como la "clase dios" o la "super clase" en la cual pretendemos que una clase haga de todo. Eso es un vicio y hay que evitarlo.

Veamos un ejemplo real:

Tenemos una clase que tiene una mala práctica:

    public class ProcessManager
    {
        public async Task Procesar()
        {
            try
            {
                Console.WriteLine("Inicio del manejo de procesos en segundo plano");

                #region Llamada a API                
                var cliente = new HttpClient();
                var urlProcesos = "https://jsonplaceholder.typicode.com/todos";
                var response = await cliente.GetAsync(urlProcesos);
                response.EnsureSuccessStatusCode();
                var body = await response.Content.ReadAsStringAsync();
                Console.WriteLine(body.Substring(0, 200));

                var procesos = JsonConvert.DeserializeObject<Proceso[]>(body);
                var procesosPendientes = procesos.Where(x => !x.Completed).ToList();
                #endregion

                #region Mapeo entre entidad y DTO (patrón data transfer object)
                Console.WriteLine("Mapeo hacia DTO");
                var procesosDTO = new List<ProcesoDTO>();

                foreach (var proceso in procesosPendientes)
                {
                    var dto = new ProcesoDTO()
                    {
                        Id = proceso.Id,
                        Title = proceso.Title.Trim()
                    };
                    procesosDTO.Add(dto);
                }
                #endregion

                #region Manejo de archivos
                Console.WriteLine("Escribiendo en archivo local");
                var rutaArchivo = @$"{Directory.GetCurrentDirectory()}\ProcesosPendientes.txt";

                using (StreamWriter sw = new StreamWriter(rutaArchivo))
                {
                    foreach (var proceso in procesosDTO)
                    {
                        sw.WriteLine(
                            $"{DateTime.Now.ToString().PadRight(10)}" +
                            $"{proceso.Id.ToString().PadRight(10)}" +
                            $"{proceso.Title}"
                       );
                    }
                }
                #endregion

                Console.WriteLine("Fin del procesamiento");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Hubo un error: {ex.Message}");
            }
        }
    }

Hice uso de la cláusula region en el método Procesar para visualizar mejor las muchas responsabilidades que actualmente tiene la clase, en Visual studio se ve de la siguiente forma:

Sí, la mala práctica que tiene es que se una sola clase hace cargo de muchas cosas a la vez, esto va en contra del principio que dice que una clase debe tener sólo UNA única responsabilidad bien definida, entonces tras aplicar el principio SRP tendríamos varias clases, cada una encargada de su propia responsabilidad.

Aplicando SRP

Comencemos a aplicar el SRP, separando responsabilidades, hagamos entonces 3 clases además de la clase ProcessManager.

Clase RepositorioProcesos

    public class RepositorioProcesos
    {
        public async Task<List<Proceso>> ObtenerProcesos()
        {
            var cliente = new HttpClient();
            var urlProcesos = "https://jsonplaceholder.typicode.com/todos";
            var response = await cliente.GetAsync(urlProcesos);
            response.EnsureSuccessStatusCode();
            var body = await response.Content.ReadAsStringAsync();
            Console.WriteLine(body.Substring(0, 200));

            var procesos = JsonConvert.DeserializeObject<List<Proceso>>(body);
            return procesos;
        }
    }

Clase RepositorioArchivos

    public class RepositorioArchivos
    {
        public void Guardar(List<ProcesoDTO> procesos)
        {
            var rutaArchivo = @$"{Directory.GetCurrentDirectory()}\ProcesosPendientes.txt";

            using (StreamWriter sw = new StreamWriter(rutaArchivo))
            {
                foreach (var proceso in procesos)
                {
                    sw.WriteLine(
                        $"{DateTime.Now.ToString().PadRight(10)}" +
                        $"{proceso.Id.ToString().PadRight(10)}" +
                        $"{proceso.Title}"
                    );
                }
            }
        }
    }

Clase Mapping

    public class Mapping
    {
        public List<ProcesoDTO> Mapear(List<Proceso> procesos)
        {
            var procesosDTO = new List<ProcesoDTO>();

            foreach (var proceso in procesosDTO)
            {
                var dto = new ProcesoDTO()
                {
                    Id = proceso.Id,
                    Title = proceso.Title.Trim()
                };
                procesosDTO.Add(dto);
            }
            return procesosDTO;
        }
    }

Y finalmente la clase ProcessManager ahora quedaría sencillamente así:

    public class ProcessManager
    {
        //Aquí podríamos hacer uso de inyección de dependencias
        RepositorioProcesos repositorioProcesos = new RepositorioProcesos();
        RepositorioArchivos repositorioArchivos = new RepositorioArchivos();
        Mapping mapper = new Mapping();

        public async Task Procesar()
        {
            try
            {
                Console.WriteLine("Inicio del manejo de procesos en segundo plano");
                var procesos = await repositorioProcesos.ObtenerProcesos();
                var procesosPendientes = procesos.Where(x => !x.Completed).ToList();
                var procesosDTO = mapper.Mapear(procesosPendientes);
                repositorioArchivos.Guardar(procesosDTO);
                Console.WriteLine("Fin del procesamiento");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Hubo un error: {ex.Message}");
            }
        }
    }

Este proyecto es una aplicación de consola, en ambos casos se hace uso de una entidad Proceso y una clase de mapeo (aplicando el patrón DTO):

    public class Proceso
    {
        public int UserId { get; set; }
        public int Id { get; set; }
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    public class ProcesoDTO
    {
        public int Id { get; set; }
        public string Title { get; set; }
    }

Practícala por tu cuenta crack.

Open Close (OCP)

Este principio es Abierto cerrado y establece que nuestro código debe ser abierto a la extensión y cerrado a la modificación, esto lo logramos aplicando el polimorfismo y la abstracción ya sea mediante clases o interfaces.

Siguiendo con el ejemplo anterior, nuestro código ya tiene las responsabilidades únicas, sin embargo es posible mejorar el código aún más y se puede hacer mediante aislar la funcionalidad de log en una clase aparte y esto es para tener nuestras código abierto a la extensión.

Esto quiere decir que si sale un nuevo requerimiento del negocio que indica que el formato de logs ahora debe incluir la fecha y hora no tengamos que modificar la clase ProcessManager, sino la clase encargada del log.

Tu tarea ahora es aplicar lo que expliqué en el proyecto que estamos viendo de ejemplo, y también aplicar los principios que siguen, investiga 😉

Liskov substitution (LSP)

El principio de sustitución de Liskov se define así:

Si S es un subtipo de T, las apariciones de tipo T en un programa pueden ser reemplazadas por otra de tipo S sin que el funcionamiento del programa se vea alterado.

En palabras sencillas esto quiere decir que sólo debemos heredar de una clase para añadir funcionalidades nunca para modificar lo existente.

Interface Segregation (ISP)

En cuanto al principio de Segregación de interfaces este propone que no debemos dar más información de la necesaria a los módulos para funcionar.

Esto lo podemos lograr mediante separar las interfaces.

Dependency Inversion (DIP)

Finalmente el principio llamado inversión de dependencias nos dice que debemos reducir la dependencia entre los módulos de nuestra aplicación, los módulos no deben ser los encargados de crear los objetos con los que trabajan sino que deben ser creados por alguien más y pasárselos a un constructor para que los use cuando quiera.

Una aplicación de este principio sería por ejemplo haciendo uso de la inyección de dependencias.

Nada como terminar lo que empezamos. Foto de Amanda Perez en Unsplash

En conclusión...

Como verás los principios SOLID nos permiten tener un código mucho más virtuoso, así que no dudes en aplicarlo en todos tus proyectos. Pronto haré una entrada en donde se verán estos principios directamente aplicándolos a un proyecto, ahondando aún más el ejemplo que hemos visto aquí y que lo apliqué en la explicación de SRP.

Ha sido un placer hacer esta serie de 10 artículos de Programación orientada a objetos, espero te sirva un montón y puedas aplicarlo en tu carrera a convertirte un programador fantástico. Hasta la próxima entrada!

Si esta entrada te ha gustado crack, compártela! 🔥

Deja una respuesta

Tu dirección de correo electrónico no será publicada.