Código limpio 2: Funciones

Tiempo estimado de lectura: 25 minutos

En esta entrada continuamos la serie de código limpio centrándonos en las funciones.

En la entrada anterior se habló de buenas formas de escoger nombres, todas ellas aplican para las funciones, pero usar nombres correctos no es lo único que importa para crear buenas funciones.

La idea central a la hora de lograr esto es la brevedad, y como en todo lo que sea código, la claridad. Estos son los consejos:

Las funciones deben de ser pequeñas

¿Qué tan pequeño es pequeño? Es difícil encontrar un número mágico, pero los siguientes puntos facilitarán mucho nuestra búsqueda de la ecuación mas pequeña posible. Para esta entrada trabajaremos con un solo ejemplo: un algoritmo que encuentra la distancia entre dos puntos en un lano coordenado sin usar librerías(con fines de ilustración, no se alarmen). Este sería el “mal ejemplo”, para simplificar se usan solo enteros:

double distance(int xA, int yA, int xB, int yB){

int x=xB-xA;

int y=yB-yA;

int squareX=x*x;

int squareY=y*y;

int squareSum=squareX+squareY;

double squareRoot=0;

//Encuentra raiz cuadrada con precisión de dos dígitos

outer://identificador de for externo

for(int decimal=0;decimal<3;decimal++){

int factor=1;

//for que agrega ceros a factor, que se usará para dividir el decimal a prueba

for(int cero=1;cero<decimal;cero++){

factor*=10;

}

for(double digito=1;squareRoot<squareSum;digito++){

double aproximation=digito/factor+squareNumber;

if(aproximation*aproximation>squareSum){

aproximation=(digito-1)/factor+squareRoot;

squareRoot+=aproximation;

continue outer;

}

else if(aproximation*aproximation==squareSum){

aproximation=(digito-1)/factor+squareRoot;

squareRoot+=aproximation;

continue outer;

}

}

}

return squareRoot;

}

Como se puede notar, el algoritmo es largo(demasiado) si no se usan librerías y se coloca todo en una función. La sección de código mas complicada es en la que obtiene la raiz cuadrada, veamos como se puede simplificar todo.

Las funciones deben hacer solo una cosa

¿Qué se busca en la función ejemplo?Una distancia, pero para lograrlo se hacen un montón de operaciones intermedias, así que algo debe estar mal. Una buena técnica para analizar este aspecto de las funciones es intentar contar una secuencia de operaciones inmediatas para lograr el objetivo. Por ejemplo:

Para obtener la distancia entre dos puntos debo obtener la componente de la distancia en los ejes, debo elevar ambas componentes al cuadrado, debo sumarlas y finalmente debo sacar la raiz cuadrada de la última suma. Se tienen cuatro oraciones con la palabra debo.

Si la operación puede hacerse inmediatamente se hace y ya. Algunas de ellas tal vez necesiten mas operaciones intermedias, así que también se pueden crear historias del mismo tipo sobre ellas: Para obtener el cuadrado de un número debo multiplicarlo por si mismo. Este último tipo de oraciones son candidatos a funciones: 

 

double square(double number){

return number*number;

}

Este es el resultado de aplicar lo explicado:

//En negritas el uso de las nuevas funciones, la función

//distance se ha reducido a unas cuantas líneas

double distance(int xA, int yA, int xB, int yB){

int x=xB-xA;

int y=yB-yA;

int squareX=square(x);

int squareY=square(y)

int squareSum=squareX+squareY;

double squareRoot=sqrt(squareSum);

return squareRoot;

}

 

double square(double number){

return number*number;

}

 

double sqrt(double number){

double squareRoot=0;

outer://identificador de for externo

for(int decimal=0;decimal<3;decimal++){

int factor=1;

//for que agrega ceros a factor, que se usará para dividir el decimal a prueba

for(int cero=1;cero<decimal;cero++){

factor*=10;

}

for(double digit=1;squareRoot<number;digito++){

double aproximation=digit/factor+squareNumber;

if(square(aproximation)>number){

aproximation=(digit-1)/factor+squareRoot;

squareRoot+=aproximation;

continue outer;

}

else if(square(aproximation)==number){

aproximation=(digito-1)/factor+squareRoot;

squareRoot+=aproximation;

continue outer;

}

}

}

return squareRoot;

}

En esta primera iteración se descubrieron dos funciones candidatas: square y sqrt, pero volviendo a iterar es fácil encontrar nuevas funciones candidatas dentro de sqrt, como multiplicar o dividir por un factor, pero las dejaremos para ilustrar otros puntos.

No deben ser lo suficientemente largas para tener estructuras anidadas

Queremos funciones cortas y claras. Es obvio que estructuras anidadas(for, if, while) no harán nuestras funciones cortas y aumentarán la confusión. No siempre es posible evitarlas, pero se debe procurar el nivel de indentación cerca de uno.

En el ejemplo, específicamente en la función sqrt se tienen tres niveles de indentación: for, for, if. ¿Cómo deshacerse de ellos?Analicemos la historia: Para obtener la raiz cuadrada de un número A debo probar números uno a uno, usando incrementos fijos hasta encontrar aquel cuyo cuadrado quede mas cerca del número sin superarlo, después debo repetir con incrementos mas pequeños partiendo del número encontrado y así sucesivamente hasta lograr la exactitud deseada.

La historia revela una estructura interesante. Tenemos una acción que se repite a si misma y las estructuras indentadas parecen una mala simulación de la misma. Veamos una solución mejorada:

double sqrt(double number, int decimals){

double step=10;

double squareRoot=1;

for(int i=0;i<=decimal;i++)

 squareRoot= aproximateRootFromBase(

number,squareRoot,step/10);

}

 

double aproximateRoot(double number, double base, double step){

double aproximation=base;

while(  square( aproximation )    <=    number  )

aproximation+=step;

return   square(aproximation) == number   ?  aproximation  :  aproximation-step;

}

Hemos reducido el nivel de indentación a uno, creando una función extra con el mismo nivel de indentación. Está mucho más claro lo que se quiere lograr.

El número ideal de argumentos para una función es cero. Mientras mas cercano a este número mejor

Debemos tener muy buenas razones para poner cualquier cantidad de argumentos en una función. Con cada uno de ellos los estados posibles de una función crecen exponencialmente aumentando la complejidad del código. Además es difícil mantener en la memoria el orden correcto y el tipo de los argumentos que se deben pasar cuando son muchos.

Normalmente se usa un argumento cuando se planea operar sobre él u obtener información de él.

Dos  y hasta tres argumentos se justifican cuando tienen cohesión, o en realidad son partes de la misma cosa. Pero en ese caso es recomendable crear un objeto que mantenga unida la información y sea el el que la pase. En el ejemplo la función distance recibe cuatro argumentos, se puede mejorar creando el objeto punto y cambiando la signatura así:

class point{

int x;

int y;

Point(int x, int y){

this.x=x;

this.y=y;

}

}

 

//metodo modificado

double distance(Point a, Point b){ ….}

Unas cuantas modificaciones mas son necesarias, las puedes ver al final de esta  entrada en el resultado total.

Los marcadores/banderas como argumentos son mala idea

Aunque no aparecen en el ejemplo, no hay mucho que decir al respecto. El hecho de pasar un valor boleano a una función implica que hará dos cosas, una en el caso true yotra en el caso false.

Las funciones no deben tener efectos secundarios

Generalmente esto pasa cuando una clase está mal diseñada. Si no puedes evitar modificar el estado de variables cuando el propósito de la función no es ese, debes dejarlo bien claro en la documentación

El nombre ideal de una función incluye un verbo que describe lo que hace y a sus blancos como nombres de los argumentos

Nada mas claro que escribir:

doubledividir(doubledividendo, double divisor){…}

Una segunda aclaración, no temas hacer un nombre de función largo. Recuerda que funciones de varios argumentos son susceptibles de confundir el orden. Atrás se encuentra la siguiente signatura de funcipon

double aproximateRoot(double number, double base, double step){…}

Pero es mucho mejor:

double aproximateRootFromBaseWithStep(double number, double base, double step){…}

Que no deja lugar a dudas

Finalmente, aquí está el resultado final de todos nuestros esfuerzos por hacer mejor código. Pude no ser perfecto pero comparándola con la primera versión se ve muchísimo mejor. Se crearon tres nuevas funciones y una estructura de datos en forma de una clase. Ya hablaremos de clases y objetos mas adelante. Ahora a abrir tus viejos archivos fuentes y a practicar. Hasta la próxima!

double distance(Point A, Point B){

int x=B.x-A.x;

int y=B.y-A.y;

double squareRoot=sqrt(square(x)+square(y),2);

return squareRoot;

}

 

class Point{

public int x;

public int y;

Point(int x, int y){

this.x=x;

this.y=y;

}

}

 

double square(double number){

return number*number;

}

 

double sqrt(double number, int decimals){

double step=10;

double squareRoot=1;

for(int i=0;i<=decimal;i++)

 squareRoot= aproximateRootFromBaseWithStep(

number,squareRoot,step/10);

}

 

double aproximateRootFromBaseWithStep(double number, double base, double step){

double aproximation=base;

while(  square( aproximation )    <=    number  )

aproximation+=step;

return   square(aproximation) == number   ?  aproximation  :  aproximation-step;

}

 

Referencias:

Clean Code: A Handbook of Agile Software Craftmanship.
Martin,Robert C.
Prentice Hall

Advertisements
Código limpio 2: Funciones

One thought on “Código limpio 2: Funciones

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s