Estándar de codificación - Solify-IT/psyche-ing GitHub Wiki

Objetivo

Establecer varias políticas y pautas para seguir en código tanto en frontend como backend para disminuir la cantidad de defectos inyectados en la fase de codificación.

Idioma

Todos los archivos, funciones, variables, y cualquier texto escrito en el código fuente deberá ser en ingles para reducir el número de caracteres promedio por palabra. La única excepción a este estándar es cuando se despliega información vista por el socio formador.

Nomenclatura de archivos y funciones

Los nombres de los archivos y funciones deberán seguir el formato camelCase.

Ejemplos:

  • index.ts
  • patientForm.ts
  • recordInfo.tsx
  • calculateAge.tsx
  • function calculateAge(birthDate: Date)

Importaciones

Cada módulo o carpeta deberá tener un archivo index.ts en donde se exportan las clases dentro de la carpeta del index. De esta forma, las importaciones son más rápidas y solo se tiene que modificar la importación en el index si es que la ubicación de un archivo o el nombre del archivo cambia.

Ejemplo del archivo index.ts

import Patient from './patient';
import Doctor from './doctor';
import User from './user/user';
import Form from './form';
import Field from './field';

export {
  Patient, Doctor, User, Form, Field,
};

Ejemplo de importación en otros archivos de lo exportado en el index anterior

import { Patient } from 'domain/model';

Importaciones de módulo

Para hacer importaciones más limpias y más rápidas, se deben usar importaciones de tipo módulo y complementando las importaciones utilizando los archivo index. De esta forma, se evita tener tantos "../../../" arriba de los archivos.

Ejemplo de mala importación

import Form from '../../interfaces/form';

Ejemplo de buena importación

✔️ import Form from 'src/interfaces/form';

Pruebas

Los archivos de prueba deberán ser nombrados con el nombre del archivo que será probado por el mismo test y con el sufijo ".test" y dentro de la carpeta del tipo de componente que se probara.

Ejemplos

Archivo o función a probar Nombre de test
interface/presenter/patient.ts test/presenter/patient.test.ts
interface/interactor/form.ts test/interactor/form.test.ts
interface/repository/login.ts test/repository/login.test.ts
views/forms/newForm.tsx test/views/newForm.test.tsx

La prueba debe cubrir todas las funciones dentro del archivo evaluado o la gran mayoría.

Utilidades

Cualquier función o clase que pueda ser reutilizada por diferentes archivos deberá ser colocada en la carpeta utils. Una vez creada en esta carpeta, se recomienda crear pruebas unitarias de estas funciones para verificar su funcionalidad correcta en las pruebas de regresión.

Documentación

Si la función no se explica por sí sola, se recomienda documentar la función utilizando JSDoc

Typescript

Utilizar tipos dentro del código puede facilitar al momento de encontrar errores y obtener el resultado que esperamos. Es importante respetar las reglas de Typescript y evitar utilizar el tipo de variable any.

❌ Ejemplo de mal uso de Typescript

const patient : any = data;
const isOld : boolean = patient.age > 30;

En el caso anterior, no tienes certeza de que la variable paciente tenga un atributo llamado age. Si es que tiene una variable age, tampoco podrías saber de qué tipo es la variable age y podría dar resultados no esperados.

✔️ Ejemplo de buen uso de Typescript

type Patient = {
  age: number;
}

const patient : Patient = data;
const isOld : boolean = patient.age > 30;

Functional Components en React

Para el ambiente de frontend del proyecto, se utilizaran functional components y no se utilizarán componentes extendidos de clases. El uso de functional components nos permite utilizar React Hooks, que al ser implementado nos permite manejar estados a nivel de aplicación o a nivel de componente.

Ejemplo de functional component:

function AppMain(props : AppMainProps) {
  const { children } = props;
  return (
    <FadeIn>
      <main>
        {children}
      </main>
    </FadeIn>
  );
}
export default AppMain;

Manejo de props

Al utilizar functional components, podemos hacer uso de props para instanciar componentes con diferentes atributos de parámetros.

Para complementar el uso de props con TypeScript, se deberá establecer el tipo de props dentro del mismo archivo del componente. Esto nos permite poder establecer el tipo que se espera en la lista de atributos de los props de un componente. Se recomienda seguir la nomenclatura: Nombre de Componente + Props.

Ejemplo:

Componente Nombre de prop
FieldRow FieldRowProps

❌ Ejemplo de mal uso de props

function FieldRow(props: any) {
  const { field, index, removeField } = props;
  return (
    <TableRow key={index}>
      <TableCell>
        {field.type}
      </TableCell>
      <TableCell>{field.label}</TableCell>
      <TableCell>
        {(field.options.length === 0) ? 'N/A'
          : (
            <ul>
              { field.options.map((option) => <li>{option.label}</li>)}
            </ul>
          )}
      </TableCell>
      <TableCell>
        <IconButton onClick={() => removeField(field)}>
          <Delete style={{ color: '#FF0000' }} />
        </IconButton>
      </TableCell>
    </TableRow>
  );
}
export default FieldRow;

En el ejemplo anterior no existe ninguna validación de que se este recibiendo los atributos esperados en el componente. Esto puede generar errores en runtime si no se pasan correctamente los datos esperados.

✔️ Ejemplos de buen uso de props

type FieldRowProps = {
  field: Field,
  index: number,
  removeField: (field: Field) => void,
};

function FieldRow(props: FieldRowProps) {
  const { field, index, removeField } = props;
  return (
    <TableRow key={index}>
      <TableCell>
        {field.type}
      </TableCell>
      <TableCell>{field.label}</TableCell>
      <TableCell>
        {(field.options.length === 0) ? 'N/A'
          : (
            <ul>
              { field.options.map((option) => <li>{option.label}</li>)}
            </ul>
          )}
      </TableCell>
      <TableCell>
        <IconButton onClick={() => removeField(field)}>
          <Delete style={{ color: '#FF0000' }} />
        </IconButton>
      </TableCell>
    </TableRow>
  );
}
export default FieldRow;

En el ejemplo anterior se define los tipos de atributos del prop. Además, se puede ver que también se puede pasar funciones como callback que nos ayuda a crear componentes que cumplan con DRY.

Manejo de estados

Como se mencionó anteriormente, al utilizar functional components tenemos la oportunidad de usar React Hooks para manipular el estado de un componente. Cuando un componente cambia de estado, React vuelve a "dibujar" ese mismo componente y todos los componentes dentro del mismo. Es importante cuidar cuanto tenemos un componente con estado dentro del otro, ya que al cambiar el estado del componente padre se puede "reiniciar" el estado de los componentes hijos. Por lo anterior, es importante distinguir los bloques de la vista que si tendran un estado dinámico y cuáles no. Es importante mencionar que un componente no puede saber el estado de otro componente.

Ejemplo de manejo de estados con React Hooks

import React, { useState } from 'react';
 
const App = () => {
  const [greeting, setGreeting] = useState(
    'Hello Function Component!'
  );
 
  const handleChange = event => setGreeting(event.target.value);
 
  return (
    <div>
      <Headline headline={greeting} />
 
      <Input value={greeting} onChangeInput={handleChange}>
        Set Greeting:
      </Input>
    </div>
  );
};
 
const Headline = ({ headline }) => <h1>{headline}</h1>;
 
const Input = ({ value, onChangeInput, children }) => (
  <label>
    {children}
    <input type="text" value={value} onChange={onChangeInput} />
  </label>
);
 
export default App;

En en el ejemplo anterior, se define la variable que tendrá un estado greeting. Esta variable (explícitamente declarada como un string) será cambiada por la función setGreeting generada por la función useState. Al llamar setGreeting, se cambiará el estado de greeting y esto provocará que el componente sea dibujado de nuevo. La función useState recibe como parámetro un valor inicial para la variable que representa un estado.

Por último, tenemos la función handleChange. Esta función que recibe un evento DOM llamara a la función setGreeting con un valor específico. En este caso, utiliza el valor del input del evento pasado.

Nota importante

El cambio de estado es una función asíncrona. Por lo tanto, puede ocurrir que si se desea acceder a una variable está aun no sea actualizada. Esto puede pasar cuando queremos imprimir alguna variable de estado. Internamente si se está actualizado correctamente la variable, pero puede parecer que no lo está o que tiene un "delay" al momento de imprimir.

Carga de datos asíncronas en React

Para facilitar la carga de datos en un componente reutilizable que se encargue de mostrar el estado de la llamada asíncrona, se recomienda utilizar el componente de utilidad PromiseLoader. Este componente se encargará de hacer un Promise, mostrar el estado de cargando o el componente que se desea mostrar una vez cargado, o regresa error si se encuentra.

La sintaxis de uso es:

PromiseLoader<T>(
  promise: Promise<AxiosResponse<any>>,
  onLoad: (data: T) => JSX.Element,
  onError: (error: AxiosError) => JSX.Element,
) 

donde T es el tipo de dato que regresará la llamada asíncrona de Axios, onLoad es el callback que mostrará un Componente en caso de éxito, y onError es el callback que mostrará un componente en caso de que haya ocurrido un error.

Ejemplo

function PatientAvailableForms() {
  const { id } : any = useParams();
  const mPromise = listFormsWithRecordId(id);
  const content = PromiseLoader<Form[]>(
    mPromise,
    (forms) => <Forms forms={forms} recordId={id} />,
    (error) => {
      switch (error.response?.status) {
        case 404:
          return <h2>No se encontró el expediente</h2>;
        default:
          return <h2>Ocurrió un error de conexión.</h2>;
      }
    },
  );
  return content;
}

Manejo de errores

Tanto en frontend como backend, se deberá utilizar la función wrapError para manejar posibles errores que pueda ocurrir en la llamada de una función. De esta forma, se evita tener que hacer un bloque de try-catch para cada tipo de llamada de función que pueda tener un error.

wrapError regresa una tupla de valores: [data, error].

Si error es nulo significa que no hubo un error y viceversa.

Ejemplo

 async registerPatient(context: IContext): Promise<void> {
    const [patients, error] = await wrapError(
      this.patientInteractor.register(context.request.body),
    );

    if (error) {
      context.next(error);
      return;
    }
    context.response.status(200).json(patients);
  }

Manejo de modelos en Frontend

Para poder utilizar los datos obtenidos de backend en frontend, se deberan crear interfaces dentro de la carpeta interfaces para cada modelo de datos que se espera recibir de backend, o se espera utilizar localmente.

Ejemplo

export default interface Form {
  id?: number;
  name: string;
  startDate?: string;
  fields: Field[];
  type: string;
}

De esta forma, podemos crear nuevas instancias de la interfaz Form localmente, o dinámicamente desde los datos obtenidos en llamadas de backend. Las variables que terminan con un ? son variables que pueden ser nulos y no es obligatorio que se tengan que implementar.

URLs

Backend

Para mantener una consistencia en los endpoints de backend, se deberá seguir el formato camelCase para los urls.

Ejemplos:

  • /records/:id/patientForms
  • /patients

La acción que se hará con el endpoint será determinada por el tipo de petición HTTP.

Tipo de petición Acción
GET Obtener o consultar
POST Registrar o crear
PATCH Modificar
DELETE Borrar

Si se desea tener un parámetro en la url del endpoint, esta variable debe tener el prefijo :. El tipo de esta variable será convertido después de pasar el objeto de parámetros. Se puede utilizar Regex para generar estas urls.

Frontend

Para las urls internas de frontend, se deberá seguir kebab-case. Si se desea tener un parámetro en la url del endpoint, esta variable debe tener el prefijo :. Se puede utilizar Regex para generar estas urls. Estas urls deben ser una excepción y estar en español ya que estas seran vistas por el cliente.

Ejemplos

  • /expediente/:id(\d+)/encuestas/:formId(\d+)
  • /dashboard-area
  • /dashboard-atencion-psicologica
  • /register-patient/:area/:group

Referencias

⚠️ **GitHub.com Fallback** ⚠️