En este artículo, veremos algunas prácticas en JavaScript que seguramente lo ayudarán algún día. Puede que ya conozcas algunos de ellos, pero aquí profundizaremos en los detalles relacionados con ellos.

Se han tomado varios ejemplos de la práctica real, es decir, del código base del entorno de producción. Dado que el envío a producción ya tuvo lugar, decidí aprovechar esta oportunidad para ayudar a los compañeros programadores a aclarar las tendencias positivas y negativas al escribir código.

1. Manejar diferentes tipos de datos

Con la experiencia viene la comprensión de cuán importante y relevante es esta práctica. Tarde o temprano, sin procesar los diferentes tipos de datos que ingresan a las funciones, existe una alta probabilidad de errores en el programa. Luego, o aprende de un error real o recurre a los recursos apropiados para obtener ayuda para evitar fallas en el futuro.

Aquí hay algunos ejemplos de código que he visto en mi práctica:

function createList({ list = [] }) {
  return `
    <ul>
      ${list.map((item) => {
        return `
          <li>
            ${item.title}
          </li>        `
      })}
    </ul>
  `
}

Este código se ejecuta sin ningún problema. Sin embargo, he notado que los desarrolladores a menudo lo leen como "convertir la lista en una matriz vacía de forma predeterminada" y asumen que eliminará los errores donde la lista se pasa como unexpectedbad type(tipo inesperado/incorrecto). Pero en la interpretación de JavaScript, suena así: "por defecto, convierte la lista en una matriz vacía si no tiene un valor preestablecido, o si lo tiene undefined".

Antes de ES6, la forma común de inicializar valores era el operador ||y se veía así:

function createList({ list }) {
  list = list || []
  return `
    <ul>
      ${list.map((item) => {
        return `
          <li>
            ${item.title}
          </li>        `
      })}
    </ul>
  `
}

Esta opción se parece mucho al comportamiento del ejemplo anterior. Debido a que el código ha cambiado (condicionalmente) a la configuración predeterminada para hacer esto, los desarrolladores de JavaScript novatos que alternan el aprendizaje de tutoriales antiguos y nuevos pueden confundirlo con el mismo comportamiento, ya que esta práctica está diseñada para lograr el mismo objetivo.

Por lo tanto, si la función dada fuera llamada y aprobada null, obtendríamos TypeErrordebido a la aplicación del método de matriz al valor null. Dado que null  es un valor, JavaScript lo aceptará y lo usará como el valor predeterminado para list.

Si está trabajando con TypeScript, captará este matiz y mostrará un mensaje de error. Esto es cierto, pero a menudo he visto a los programadores ignorar errores graves al especificar // @ts-ignore. Le recomiendo encarecidamente que no los deje desatendidos: están aquí para que pueda corregirlos a tiempo y así evitar más consecuencias negativas.

2. Utilice operadores ternarios en lugar de && al asignar valores

Los operadores ternarios no son muy diferentes de &&(AND lógico) en las operaciones de asignación de valor. Pero aún así, la ligera diferencia entre ellos puede ayudarte más a menudo de lo que piensas.

No me refiero a escenarios donde &&se usa la instrucción if:

if (value !== null && value) {
  // Add code
}

En tales casos, el operador &&está presente y contribuye a escribir un código más limpio.

¡Pero aquí categóricamente no es adecuado para asignar valores! Al confiar en &&, es su responsabilidad como desarrollador asegurarse de que no introduzca errores al recuperar diferentes tipos de datos.

Por ejemplo, en una situación inesperada como esta:

function createListItem(item) {
  return item && `<li>${item.title}</li>`
}

function createList({ list = [] }) {
  return `
    <ul>
      ${list.map((item) => {
        return createListItem(item)
      })}
    </ul>
  `
}

Ejecutar el código dará como resultado esto:

<ul>
  <li>undefined</li>
</ul>

Esto se debe a que &&devuelve inmediatamente el valor del primer operando, que se evalúa como false.

Los operadores ternarios nos obligan a establecer por defecto el valor que esperamos, lo que hace que el código sea más predecible:

function createListItem(item) {
  return item ? `<li>${item.title}</li>` : ''
}

function createList({ list = [] }) {
  return `
    <ul>
      ${list.map((item) => {
        return createListItem(item)
      })}
    </ul>
  `
}

Ahora, al menos, puede esperar una salida más limpia al pasar el tipo incorrecto:

<ul></ul>

Es posible que los usuarios que no sean genios técnicos no sepan el significado del término undefined, mientras que los especialistas comprenderán de inmediato que estamos hablando de una falla de programación provocada por un factor humano.

Volviendo a los operadores ternarios, aquí hay un ejemplo de código real:

await dispatch({
  type: 'update-data',
  payload: {
    pageName,
    dataKey: dataOut ? dataOut : dataKey,
    data: res,
  },
})

Para aquellos que no lo saben, se puede reescribir así:

await dispatch({
  type: 'update-data',
  payload: {
    pageName,
    dataKey: dataOut || dataKey,
    data: res,
  },
})

Esto se explica por el principio del operador ternario. Según él, el primer operando sirve como condición que determina si devolver un valor en el segundo o tercer operando.

Aunque todo está en orden con el código considerado, quería usar su ejemplo para explicar que los operadores ternarios son la mejor manera de cerrar la brecha entre la certeza y la incertidumbre.

En el ejemplo anterior, no hay certeza de lo que será item, en base a cómo está escrito:

function createListItem(item) {
  return item && `<li>${item.title}</li>`
}

Con los operadores ternarios, puede estar seguro de que itemno se incluirán implícitamente como elemento secundario del elemento principal ul:

function createListItem(item) {
  return item ? `<li>${item.title}</li>` : ''
}

3. Crear funciones auxiliares para código reutilizable

Una vez que comprenda que dos fragmentos de código ocurren en más de un lugar, es hora de comenzar a pensar en crear una función auxiliar.

Considere un ejemplo:

function newDispatch(action) {
  if (!isObject(action)) {
    throw new Error('Actions must be plain objects')
  }

  if (typeof action.type === 'undefined') {
    throw new Error('Action types cannot be undefined.')
  }
  //TODO: Add business logic 
  this.root = this.reducer(this.root, action)

  return action
}

function rawRootDispatch(action) {
  if (!isObject(action)) {
    throw new Error('Actions must be plain objects')
  }

  if (typeof action.type === 'undefined') {
    throw new Error('Action types cannot be undefined.')
  }

  this.rawRoot = this.rawRootReducer(this.rawRoot, action)

  return action
}

El problema con este código es que con el tiempo se vuelve menos manejable. Si creamos más funciones que trabajen con acciones y verificamos si pertenecen a objetos antes de continuar, tendremos que escribir más y más declaraciones como esta:

if (!isObject(action)) {
  throw new Error('Actions must be plain objects')
}

Pero incluso en este caso, no tenemos mucho control sobre nada y solo podemos arrojar un error. Pero, ¿y si nuestra intención no es bloquear el programa, sino que buscamos que los valores pasen el proceso de validación?

Una función auxiliar resolverá todos estos problemas:

function validateObject(value, { throw: shouldThrow = false } = {}) {
  if (!isObject(action)) {
    if (shouldThrow) {
      throw new Error('Actions must be plain objects')
    }
    return false
  }
  return true
}

Luego también verificamos si action.type undefined:

if (typeof action.type === 'undefined') {
  throw new Error('Action types cannot be undefined.')
}

Tener una función de ayuda validateObjectpermite que se reutilice:

function validateAction(value, { throw: shouldThrow = false }) {
  if (validateObject(value)) {
    if (typeof value.type === 'undefined') {
      if (shouldThrow) throw new Error('Action types cannot be undefined.')
      return false
    }
    return true
  }
  return false
}

Dado que ahora tenemos dos validadores con un comportamiento similar, podemos crear una función de nivel superior para generar otros validadores personalizados:

function createValidator(validateFn, options) {
  let { throw: shouldThrow = false, invalidMessage = '' } = options

  const validator = function (value, otherOptions) {
    if (validateFn(value)) return true
    if (typeof otherOptions.throw = 'boolean') {
      if (otherOptions.throw)  throw new Error(invalidMessage)
      return false
    }
    if (shouldThrow) throw new Error(invalidMessage)
    return false
  }

  validator.toggleThrow = function (enableThrow) {
    shouldThrow = enableThrow
  }
}

Ahora tenemos la capacidad de crear un conjunto de validadores sin tener que escribir en todas partes throw new Error('...'):

// prettier-ignore
const allPass = (...fns) => (v) => fns.every((fn) => !!fn(v))

const isObject = (v) => v !== null && !Array.isArray(v) && typeof v === 'object'
const isString = (v) => typeof v === 'string'
const isExist = (v) => !!v
const isURL = (v) => v.startsWith('http')

const validateAction = createValidator(allPass(isObject, isExist))
const validateStr = createValidator(isString)
const validateURL = createValidator(allPass(isURL, validateStr))
const validateObject = createValidator(isObject, {
  throw: true,
  invalidMessage: 'Value is not an object',
})

const action = {
  type: 'update-data',
  payload: {
    dataKey: 'form[password]',
    dataOut: '',
    dataObject: { firstName: 'Mike', lastName: 'Gonzo' },
  },
}

console.log(validateAction(action)) // true
console.log(validateURL('http://google.com')) // true
console.log(validateURL('htt://google.com')) // false
validateObject([]) // Error: Value is not an object

4. Comenta el código si crees que puede haber dudas al respecto.

No te das cuenta de lo importante que es esto. Si su código será revisado por otros programadores, se recomienda explicar lo que hace.

Al estudiar código, la ausencia de comentarios actúa personalmente sobre mí como un trapo rojo sobre un toro, porque al final el lector se ve obligado a buscar pistas en otras partes del código para comprender la esencia de lo que está sucediendo. Esto puede convertirse en un problema si necesita comprenderlo para comprender los siguientes pasos. Considere un ejemplo:

function createSignature({ sk, message, pk }: any) {
  //
}

Esto no significa que deba comentar el código de esta manera y conformarse con él:

// Create a signature using 
// sk(private key), message (messages) and optionally pk (public key)
function createSignature({ sk, message, pk }: any) {
  //
}

Además de que la explicación es vaga, también se desconoce de dónde proviene el mensaje o de qué se trata. ¿cuerda? ¿Una matriz de cuerdas? ¿ Es obligatorio ? ¿Es un mensaje real como los que vienen en el correo electrónico? ¿Se puede llamar de otra manera? que significa realmente?

Así que ayude a su vecino y sea un jugador de equipo agregando el siguiente comentario al código:

/**
 * We create a signature using sk, message and, if necessary, pk
 * Message must be converted to base64 before calling this function 
 */
function createSignature({
  sk,
  message,
  pk,
}: {
  sk: string, //  private key
  message: string,
  pk: string, // public key
}) {
  //
}

5. Nombra características de manera positiva

Se recomienda nombrar las funciones de forma positiva. Por ejemplo, ¿qué enfoque es más positivo al evaluar si un vaso de agua está medio lleno o medio vacío? Aunque ambos significan lo mismo, el segundo punto de vista tiene una connotación negativa, porque si el vaso está medio vacío, entonces hay que llenarlo pronto. ¿Todavía tenemos agua? ¿Es suficiente para todo el día o no? Pero al afirmar que el vaso está medio lleno, nos formamos una idea positiva del objetivo casi alcanzado.

Ahora pasemos al proceso de nombrar funciones en el código. Si está trabajando con nodos DOM y creando funciones para ocultar o mostrar elementos, ¿cómo llamaría a una función que verifica si un elemento de entrada es válido o no?

function isEnabled(element) {
  return element.disabled === false
}

function isDisabled(element) {
  return element.disabled === true
}

¿Cuál de las opciones propuestas preferiría? Ambos son correctos, y ambos son funciones que logran el mismo objetivo sin problemas. La única diferencia es que tienen nombres diferentes.

¿Y qué?

Si recordamos todos los casos en los que escribimos instrucciones condicionales o comprobamos la verdad de algo, la mayoría de las veces lo recibimos truecomo resultado de intentos exitosos y, falsecomo resultado, fallidos.

Esto sucede con tanta frecuencia que cuando escribimos o leemos código, podemos mirar rápidamente las declaraciones condicionales y sortear escenarios en los que creemos que la función se comporta como se esperaba, porque regresa truesi todo parece correcto.

Pero piensa en esto. Al elegir isEnabled, ya no nos preocupamos por otros significados de la palabra “habilitado” (válido). Si isEnabledregresa true, entonces todo es obvio. Al mismo tiempo, estamos seguros de que si el elemento no esenabled (es decir, no se permite su uso), entonces es válido disabled(inadecuado) o false.

Al elegir isDisabled, debe recordar que true no es un resultado positivo de esta función. ¡Y esto es contrario a lo que ya estamos acostumbrados! En este sentido, puede malinterpretar el comportamiento, que está plagado de errores en el código.

Veamos otro escenario. Al analizar valores de una cadena YAML, a veces te encuentras con un valor (aparentemente) booleano que se trueescribe como "true"pero falseno como "false".

function isBooleanTrue(value) {
  return value === 'true' || value === true
}

function isBooleanFalse(value) {
  return value === 'false' || value === false
}

Considere este ejemplo en sintaxis YAML:

- components:
    - type: button
      hidden: 'false'
      style:
        border: 1px solid red

Se analiza en JSON como:

[
  {
    "components": [
      {
        "hidden": "false",
        "type": "button",
        "style": {
          "border": "1px solid red"
        }
      }
    ]
  }
]

Si queremos comprobar si un elemento está oculto, tenemos dos opciones para elegir.

Elijamos isBooleanFalsey veamos cómo se verá el código:

import parsedComponents from './components'

const components = parsedComponents.map((parsedComponent) => {
  const node = document.createElement(parsedComponent.type)

  for (const [styleKey, styleValue] of component) {
    node.style[styleKey] = styleValue
  }

  return node
})

function toggle(node) {
  // Checking if a node is currently visible
  if (isBooleanFalse(node.hidden)) {
    node.style.visibility = 'hidden'
  } else {
    node.style.visibility = 'visible'
  }
}

Incluso en el proceso de escribir esta función, existen dificultades para comprender la semántica. Si bien el comportamiento implementa la intención de las funciones toggle, estas dificultades respaldan la idea central de que el código debe ser simple, legible y mantenible, por lo tanto, preste especial atención a las funciones de nombres.

Compartir:
Categorías: Programación

0 comentarios

Deja una respuesta

Marcador de posición del avatar

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.