viernes, octubre 01, 2021

Refactorizaci贸n: Guard Clauses

Es probable que en alg煤n momento de su carrera de programaci贸n haya escrito declaraciones l贸gicas condicionales masivas con muchos niveles de declaraciones if y else if anidadas.

Es probable que en alg煤n momento de su carrera de programaci贸n haya escrito declaraciones l贸gicas condicionales masivas con muchos niveles de declaraciones if y else if anidadas. Al principio puede parecer una gran idea, ya que encaja perfectamente con la l贸gica que est谩 tratando de hacer cumplir, pero luego pasa un mes y necesita hacer cambios en ese bloque condicional masivo. Lo m谩s probable es que le haya llevado mucho tiempo analizar y comprender la l贸gica condicional, ya que estaba llena de m煤ltiples niveles de anidamiento y sus nuevos cambios probablemente eran muy propensos a errores. Puede parecer que no hay forma de evitar este problema, pero ah铆 es donde entran en juego las cl谩usulas de protecci贸n.

Los principales problemas que aparecen en el c贸digo en el que no se aplica la t茅cnica de las cl谩usulas de protecci贸n son los siguientes:

  • Indentaci贸n excesiva 鈥 el uso excesivo de la estructura de control si est谩 anidada significa que hay un alto nivel de indentaci贸n que dificulta la lectura del c贸digo.
  • Relaci贸n entre if-else 鈥 cuando hay una gran cantidad de fragmentos de c贸digo separados entre if-else, que est谩n conceptualmente relacionados entre s铆, es necesario realizar la lectura del c贸digo saltando entre las diferentes partes.
  • Esfuerzo mental 鈥 una consecuencia de los diferentes saltos en el c贸digo fuente provoca que se genere un esfuerzo extra en la generaci贸n de c贸digo.

Aplicaciones pr谩cticas

La aplicaci贸n pr谩ctica de una cl谩usula de guarda es el siguiente caso:

function doSomething() {
  if (everythingIsGood()) {
    /**
     * 隆隆隆Mucho c贸digo aqu铆 !!!
     */
    return SOME_VALUE;
  } else {
    return ANOTHER_VALUE; // un caso especial
  }
}

En este caso, y la mayor铆a de las veces, debe invertir la l贸gica para evitar usar la palabra reservada else. El c贸digo anterior se reescribir铆a de la siguiente manera:

function doSomething() {
  if (!everythingIsGood()) {
    // esta es su cl谩usula de protecci贸n
    return ANOTHER_VALUE;
  }

  /**
   * 隆隆隆Mucho c贸digo aqu铆 !!!
   */
  return SOME_VALUE;
}

Por tanto, los casos particulares que provoquen una salida del m茅todo se colocar铆an al inicio del m茅todo y actuar铆an como guardas de forma que se evite continuar por el flujo satisfactorio del m茅todo.

De esta forma, el m茅todo es de f谩cil lectura ya que los casos particulares se encuentran al inicio del mismo y el caso de uso de flujo satisfactorio es el cuerpo del m茅todo.

Echemos un vistazo a un ejemplo un poco m谩s complejo sobre el c谩lculo de los deducibles del seguro.

function getInsuranceDeductible(insurance) {
  if (insurance.covered) {
    if (insurance.majorRepair) {
      return 500;
    } else if (insurance.mediumRepair) {
      return 300;
    } else {
      return 100;
    }
  } else {
    return 0;
  }
}

Esta es una funci贸n muy simple, pero la l贸gica anidada if/else if es dif铆cil de seguir a primera vista. Para limpiar esto, podemos usar nuevamente cl谩usulas de protecci贸n.

function getInsuranceDeductible(insurance) {
  if (!insurance.covered) return 0;
  if (insurance.majorRepair) return 500;
  if (insurance.mediumRepair) return 300;

  return 100;
}

Esta funci贸n es mucho m谩s corta que la funci贸n anterior y mucho m谩s f谩cil de entender, ya que toda la l贸gica es aut贸noma y no est谩 anidada entre s铆.


Imagina que tienes que crear un m茅todo que calcule el costo del seguro m茅dico en el que se recibe el ID de usuario como par谩metro.

Se realiza una b煤squeda en una base de datos utilizando este ID para recuperar un usuario. Si el usuario no existe, se lanzar谩 una excepci贸n llamada UserNotFoundException. Si el usuario existe en el sistema, el siguiente paso es verificar que el seguro m茅dico del usuario corresponda a alguno de los que son v谩lidos para este algoritmo: Allianz o AXA. Si el seguro no es v谩lido, se debe devolver una excepci贸n llamada UserInsuranceNotFoundException. Finalmente, este algoritmo solo es v谩lido para usuarios que sean de nacionalidad colombiana. Por lo tanto, debe verificar nuevamente si el usuario es colombiano para realizar el c谩lculo del seguro o devolver una excepci贸n llamada UserIsNotColombianException.

function calculateInsurance(userId: number) {
  const user = myDb.findOne(userId);

  if (user) {
    if (user.insurance === 'Allianz' || user.insurance === 'AXA') {
      if (user.nationality === 'Colombian') {
        const value = any;
        /**
         * Algoritmo complejo
         */
        return value;
      } else {
        throw new UserIsNotColombianException(user);
      }
    } else {
      throw new UserInsuranceNotFoundException(user);
    }
  } else {
    throw new UserNotFoundException('User NotFound');
  }
}

Como puede ver, el c贸digo tiene muchos niveles de indentaci贸n. A continuaci贸n se muestra la misma versi贸n del algoritmo anterior, pero se ha aplicado la t茅cnica de las cl谩usulas de protecci贸n. Esta t茅cnica permite que el c贸digo sea m谩s legible. Tenga en cuenta que se han aplicado tres cl谩usulas de protecci贸n que permiten generar rutas alternativas (lanzar excepciones) que no interfieren en el resultado del algoritmo.

function calculateInsurance(userId: number) {
  const user = myDb.findOne(userId);

  if (!user) {
    throw new UserNotFoundException('User NotFound');
  }

  if (!(user.insurance === 'Allianz' || user.insurance === 'AXA')) {
    throw new UserInsuranceNotFoundException(user);
  }

  if (user.nationality !== 'Colombian') {
    throw new UserIsNotColombianException(user);
  }

  const value = any;
  /**
   * Algoritmo complejo
   */
  return value;
}

Algunas cuestiones que deben resolverse:

  • 驴Por qu茅 no hay casos de if-else if?

    隆Deja de pensar! Si su c贸digo requiere casos como el else if, es porque est谩 incumpliendo el Principio de Responsabilidad 脷nica y el c贸digo toma decisiones de nivel superior, que deben refactorizarse.

  • Las condiciones negativas no se comprenden bien.

    Para ello contamos con otra t茅cnica de refactorizaci贸n llamada m茅todo extract, que consiste en extraer c贸digo en funciones para su reutilizaci贸n o comprensi贸n lectora. En el siguiente ejemplo, modificamos el ejemplo anterior para crear m茅todos que permitan una mejor lectura y comprensi贸n del c贸digo.

En el uso de una cl谩usula de protecci贸n, la l贸gica de las condiciones normalmente se invierte y, dependiendo de la complejidad de la condici贸n, es bastante complejo entender qu茅 se est谩 evaluando en esa condici贸n.

Por eso es una buena pr谩ctica extraer la l贸gica de las condiciones en peque帽as funciones que permitan una mayor legibilidad del c贸digo (y, por supuesto, encontrar errores en ellas) ya que la responsabilidad de evaluar la condici贸n se est谩 delegando a una funci贸n espec铆fica.

Para nuestro ejemplo de seguro m茅dico podemos generar los siguientes m茅todos:

function isValidInsurance({ insurance }: User): boolean {
  return insurance === 'Allianz' || insurance === 'AXA';
}

function isColombian({ nationality }: User): boolean {
  return nationality === 'Colombian';
}

No es necesario crear una funci贸n para comprobar si el usuario existe, ya que basta con comprobar que el usuario es diferente de nulo o indefinido. Por tanto, el c贸digo resultante ser铆a el siguiente:

function calculateInsurance(userId: number) {
  const user = myDb.findOne(userId);

  if (!user) {
    throw new UserNotFoundException('User NotFound');
  }

  if (!isValidInsurance(user)) {
    throw new UserInsuranceNotFoundException(user);
  }

  if (!isColombian(user)) {
    throw new UserIsNotColombianException(user);
  }

  const value = any;
  /**
   * Algoritmo complejo
   */
  return value;
}

Resumen

Existen muchas pr谩cticas para mejorar la calidad del c贸digo. Lo m谩s importante que hay que aprender a la hora de aplicar t茅cnicas de refactorizaci贸n es que deben centrarse en dos puntos, principalmente:

  • Desacoplar el c贸digo 鈥 esto permite peque帽os cambios que no causan grandes cambios encadenados en todo el proyecto de software.
  • Legibilidad 鈥 es muy importante que los desarrolladores comprendan que la mayor parte del tiempo de su trabajo se basa en la lectura de c贸digo y, probablemente, en c贸digo escrito por otro desarrollador. Es muy beneficioso en costo/desarrollo que un desarrollador no pierda tiempo entendiendo la l贸gica elemental porque no es f谩cil de leer.

La refactorizaci贸n comienza desde el punto m谩s elemental, un simple if, hasta un patr贸n de arquitectura. Es importante cuidar todos los aspectos de nuestro desarrollo de software.