2.6 Código Limpio

A lo largo de este tutorial vamos a modificar el código para generar un código más fácil de entender y de probar.

Robert C. Martin escribió un libro (Clean Code) para explicar cómo crear un código limpio para que sea más fácil de entender y entender por otros programadores. Otro libro también muy bueno es el siguiente Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series) . Un libro enfocado en C# aunque no esta tan actualizado es: Clean Code in C#: Refactor your legacy C# code base and improve application performance by applying best practices

Algunas de sus sugerencias son:

  • Escribir nombres de variables y funciones descriptivos.

  • En lugar de repetir código crear funciones.

  • Que tus funciones no tengan muchas líneas de código. Alrededor de 10 líneas máximo .

  • Tus funciones no deben tener más de 3 parámetros.

  • El código debe ser suficientemente claro para que no necesite comentarios.

  • Evita utilizar números que signifiquen algo como 0 Cancelado 1 Activo 2 Vendido, es mejor utiilzar Enumeraciones.

  • Que tus funciones hagan lo que dice de acuerdo al nombre. Por ejemplo una función que se llame enviaEmail y que aparte de email imprima un recibo y guarde en base de datos.

  • No almacenar passwords en el código.

También explica unos principios llamados SOLID los cuales son:

S - Single responsibility principle (SRP)

Este principio indica que cada clase o función debe realizar solamente una actividad. Por ejemplo. el método insertar de una clase categoría solo debe insertar no debe tener parámetro por ejemplo acción el cual indique que la función debe insertar o modificar una categoría. O una clase que guarde en base de datos una factura y la imprima, se debe tener 2 clases una para comunicarse con la base de datos y otra para imprimir.

O - Open/Closed principle (OCP)

Tú código debe ser abierto a ser extendido por otros programadores para que lo puedan ajustar a sus necesidades y debe ser cerrado para que no necesites cambiar tu código para adecuarlo a las necesidades de los demás programadores.

Por ejemplo puedes tener un clase que calcule los impuestos de acuerdo al tipo de producto. Unos productos tienen un impuesto donde debes pagar el 16% del valor del producto como impuesto, otro impuesto puede ser de forma fija por ejemplo cada que compres el producto debes pagar $4.00 de impuesto. El código podría ser el siguiente

public class Producto
{
    public int Clave;
    public decimal Precio;
    public decimal Tasa;
    public int TipoImpuesto;
}

public class Impuesto
{
    public decimal CalculaImpuesto(List<Producto> productos)
    {
        decimal total = 0;
        foreach (var producto in productos)
        if (producto.TipoImpuesto == 1)
            total += producto.Precio * producto.Tasa;
        else
            total +=  producto.Tasa;
        return total;
    }
}

Si luego surge un nuevo tipo de impuesto que sea una combinación de los dos, tendrías que modificar tu clase para agregar el nuevo tipo de impuesto y pasar el cambio a todos los programadores que estén utilizando tu clase. Con el principio abierto cerrado esto no es necesario.

Para esto usa el concepto de interfaces el cual te permite definir las propiedades y métodos comunes a todos los objetos.

//La interface contiene los métodos comunes
public interface IProducto
{
   int Clave { get; set; }
   decimal Precio { get; set; }
   decimal Tasa { get; set; }
       
   decimal CalculaImpuesto();
}
//Al crear una clase que implementa la interfaz defines 
//la forma de calcular el impuesto para este tipo de
//producto
public class ProductoPorcentaje : IProducto
{
    public int Clave { get; set; }
    public decimal Precio { get; set; }
    public decimal Tasa { get; set; }
    
    public decimal CalculaImpuesto()
    {
        return Precio * Tasa;
    }
}

public class ProductoCuota : IProducto
{
     public int Clave { get; set; }
     public decimal Precio { get; set; }
     public decimal Tasa { get; set; }
     
     public decimal CalculaImpuesto()
     {
         return Tasa;
     }
}
//Esta clase permite calcular el impuesto de una lista de productos
public class Impuesto
{
     public decimal CalculaImpuesto(List<IProducto> productos)
     {
         decimal total = 0;
         foreach (var producto in productos)
              total+= producto.CalculaImpuesto();
         return total;
     }
}

Aunque parece que es mucho más código de esta forma se cumple el principio 1, 2 en el cual cada clase solo tiene un objetivo y si luego surgen nuevos tipos de impuestos solo creas una clase que implemente la interfaz producto y no tienes que pedirle a la persona que diseño la clase Impuesto que agregue el cálculo cuando surja un nuevo tipo de impuesto. Si luego surgen muchos tipos de impuestos diferentes tendrías un código muy largo lleno de cases o de ifs

Este es un ejemplo sencillo ya un ejemplo más complicado es un método que borre un registro de acuerdo a su Id, el código siempre es el mismo buscar el registro por su id y luego borrarlo, lo único que cambiamos es el nombre de la tabla.

L - Liskov substitution principle

Este principio indica que cuando una clase hereda de otra clase se pueden sustituir sin que el funcionamiento sea diferente.

Continuando con el ejemplo anterior si creas otra clase que herede de ProductoPorcentaje la función CalculaImpuesto deberéa regresar el impuesto con la misma fórmula Precio * Tasa y no deberia hacer algo diferente como Precio * Tasa + Tasa ya que el funcionamiento seria diferente y se debería crear otra clase nueva que implemente la interfaz IProducto.

// Esta clase no cumple con el principio
public class ProductoPorcentaje : ProductoPorcentaje
{
    public int Clave { get; set; }
    public decimal Precio { get; set; }
    public decimal Tasa { get; set; }
    
    public decimal CalculaImpuesto()
    {
        return Precio * Tasa + Tasa;
    }
}

// Esta otra clase no cumple con el principio ya que se modifican
// las propiedades de la clase Base de una forma diferente 
// al funcionamiento esperado
public class ProductoPorcentaje : ProductoPorcentaje
{
    public int Clave { get; set; }
    public decimal Precio { get; set; }
    public decimal Tasa { get; set; }
    
    public ProductoPorcentaje()
    {
       Precio = Precio + Tasa;
       Tasa = Tasa * 2
    }
    public decimal CalculaImpuesto()
    {
        return Precio * Tasa;
    }
}

En el proyecto utilizaremos este principio para cambiar en enitity framework que en lugar de utilizar una base de datos de mysql utilice una de sql server o una base de datos en memoria.

El funcionamiento del código es el mismo, por lo cual podemos cambiar de base de datos utilizando el paquete nuget correspondiente.

I - Interface segregation principle

Este principio indica que las interfaces solo deben tener los métodos y propiedades que realmente son necesarios. Así los programadores que implementan tu interfaz solo implementan los métodos que son útiles para ellos.

Continuando con nuestro ejemplo nuestra interfaz IProducto si necesitamos tener productos que si manejan control de existencias es decir que no se pueden vender si no tienen existencias y productos sin existencia como por ejemplo la compra de un dominio de internet, sería un error agregar a nuestra interfaz IProducto un campo que diga existencia, lo mejor seria tener 2 interfaces una para productos con existencia otra para productos sin existencia

public interface IProducto
{
    int Clave { get; set; }
    decimal Precio { get; set; }
    decimal Tasa { get; set; }
    int existencia { get; set; }
    
    decimal CalculaImpuesto();
    //No cumple con el principio porque obligas 
    //a los programadores a implementar este método
    //cuando no lo necesitan
    int ObtieneExistencia();
}

public class ProductoExistencia : IProducto
{
     public int Clave { get; set; }
     public decimal Precio { get; set; }
     public decimal Tasa { get; set; }
     public int existencia { get; set; }

     public decimal CalculaImpuesto()
     {
         return Tasa;
     }

     public int ObtieneExistencia()
     {
          //Cálculos para obtener existencia         
          return existencia;
     }
}

public class ProductoVirtual : IProducto
{
      public int Clave { get; set; }
      public decimal Precio { get; set; }
      public decimal Tasa { get; set; }
      public int existencia { get; set; }

      public decimal CalculaImpuesto()
      {
           return Tasa;
      }
      //Los programadores tienen que escribir el código de este método
      //aunque no lo necesiten
      public int ObtieneExistencia()
      {
            return 0;
      }
}

D - Dependency Inversion Principle

Este principio indica que los módulos de alto nivel no deben depender de módulos de bajo nivel. Ya que si cambias una clase mas sencilla debes cambiar la clase mas complicada.

Veamos un ejemplo, tenemos una clase casa, la cual incluye un objeto de tipo Puerta y Ventana

public class Puerta
{
}

public class Ventana
{
}

public class Casa
{
   private Puerta _puerta;
   private Ventana _ventana;
   
   public Casa()
   {
       _puerta = new Puerta();
       _ventana = new Ventana();
   }
}

Esta clase no cumple con este principio ya que si luego necesitamos otro tipo de puerta o de ventana tenemos que modificar la clase casa, para esto lo mejor es crear interfaces para la clase Puerta y la clase Ventana y pasar en el constructor un objeto puerta y un objeto ventana para personalizar la casa. Este principio se utiliza mucho en .NET Core para pruebas unitarias

public class Casa
{
   private IPuerta _puerta;
   private IVentana _ventana;
   
   public ICasa(IPuerta puerta, IVentana ventana)
   {
       _puerta = puerta;
       _ventana = ventana;
   }
}

Nuestro ejemplo del principio 2 también cumple con este principio ya que pasamos la lista de productos como parámetro en lugar de que la clase cree los productos.

public class Impuesto
{
     public decimal CalculaImpuesto(List<IProducto> productos)
     {
         decimal total = 0;
         foreach (var producto in productos)
              total+= producto.CalculaImpuesto();
         return total;
     }
}

En el proyecto utilizamos este principio para pasar un objeto para guardar el log de errores y para poder cambiar la base de datos y para validar los permisos a los servicios.

Last updated