Estándar de codificación - Solify-IT/psyche-ing GitHub Wiki
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.
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.
Los nombres de los archivos y funciones deberán seguir el formato camelCase.
index.ts
patientForm.ts
recordInfo.tsx
calculateAge.tsx
function calculateAge(birthDate: Date)
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.
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,
};
import { Patient } from 'domain/model';
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.
❌ import Form from '../../interfaces/form';
✔️ import Form from 'src/interfaces/form';
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.
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.
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.
Si la función no se explica por sí sola, se recomienda documentar la función utilizando JSDoc
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
.
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.
type Patient = {
age: number;
}
const patient : Patient = data;
const isOld : boolean = patient.age > 30;
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.
function AppMain(props : AppMainProps) {
const { children } = props;
return (
<FadeIn>
<main>
{children}
</main>
</FadeIn>
);
}
export default AppMain;
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 |
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.
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.
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.
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.
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.
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.
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;
}
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.
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);
}
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.
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.
Para mantener una consistencia en los endpoints de backend, se deberá seguir el formato camelCase para los urls.
/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.
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.
/expediente/:id(\d+)/encuestas/:formId(\d+)
/dashboard-area
/dashboard-atencion-psicologica
/register-patient/:area/:group
- https://www.robinwieruch.de/react-function-component
- https://www.freecodecamp.org/news/functional-components-vs-class-components-in-react/#:~:text=Functional%20components%20are%20basic%20JavaScript,mainly%20responsible%20for%20rendering%20UI
- https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html