Es un poco raro iniciar con una entrada así, pero una encuesta que realicé en el Discord del Traductor de Ingeniería habló.
Como sea, de vez en cuando dedicaré entradas a hablar de estilo de codificación y este es probablemente uno de los tips más sencillos.
Primero que nada, conviene preguntarse, ¿qué tiene de malo usar un parámetro bool?, el siguiente código lo ejemplifica:
updatePriorityList(true, game_object);
Sólo leyendo ésa línea, ¿qué te imaginas que hace?, obviamente actualiza una lista de prioridades, pero, ¿qué pinta tiene game_object ahí?, además ¿qué significa true en ése contexto?, si te encontraras con un código así:
updatePriorityList(false, game_object);
¿Qué podrías deducir?, ¿que es falso que actualiza la lista de prioridades?, ahora, supon que al leer la implementación, encuentras algo así:
void updatePriorityList(bool is_inserting, GameObject* game_object) {
if (is_inserting) {
sorted_list.push_back(game_object);
for (int i = sorted_list.size() - 1; i > 0; --i) {
if (sorted_list[i-1]->priority > sorted_list[i]->priority) {
std::swap(sorted_list[i-1], sorted_list[i]);
}
}
} else {
for (int i = 0; i + 1 < sorted_list.size(); ++i) {
if (sorted_list[i] == game_object) {
std::swap(sorted_list[i], sorted_list[i+1]);
}
}
if (sorted_list.size() > 0 && sorted_list.back() == game_object) {
sorted_list.pop_back(game_object);
}
}
}
Enumeraciones
La forma más directa de arreglar este problema de legibilidad sería utilizando un enum:
enum class PriorityListOperation {
INSERT,
REMOVE
};
void updatePriorityList(PriorityListOperation operation, GameObject* game_object) {
if (operation == PriorityListOperation::INSERT) {
sorted_list.push_back(game_object);
for (int i = sorted_list.size() - 1; i > 0; --i) {
if (sorted_list[i-1]->priority > sorted_list[i]->priority) {
std::swap(sorted_list[i-1], sorted_list[i]);
}
}
} else if (operation == PriorityListOperation::REMOVE) {
for (int i = 0; i + 1 < sorted_list.size(); ++i) {
if (sorted_list[i] == game_object) {
std::swap(sorted_list[i], sorted_list[i+1]);
}
}
if (sorted_list.size() > 0 && sorted_list.back() == game_object) {
sorted_list.pop_back(game_object);
}
}
}
De esta manera, las llamadas a función se leerían mucho más claro:
updatePriorityList(PriorityListOperation::INSERT, game_object); updatePriorityList(PriorityListOperation::REMOVE, game_object);
Otra ventaja que tienen los enum sobre los parámetros bool, es que pueden representar más de 2 valores, y cuando surgen este tipo de funciones en los proyectos es bastante común que empiecen usando 2 valores pero con el tiempo requieran más.
Separar en varias funciones
Esta implementación aún presenta un problema: en el fondo, la inclusión de una bandera lo que está implicando es que la función hace una cosa si la bandera está activada y hace otra cosa diferente si la bandera no está activada.
Un principio muy importante es que una función debe de hacer una cosa y sólamente una, y este anti-patrón desde el principio rompe con ése planteamiento, por lo cual debería de haber 2 funciones en lugar de una:
void addToPriorityList(GameObject* game_object) {
sorted_list.push_back(game_object);
for (int i = sorted_list.size() - 1; i > 0; --i) {
if (sorted_list[i-1]->priority > sorted_list[i]->priority) {
std::swap(sorted_list[i-1], sorted_list[i]);
}
}
}
void removeFromPriorityList(GameObject* game_object) {
for (int i = 0; i + 1 < sorted_list.size(); ++i) {
if (sorted_list[i] == game_object) {
std::swap(sorted_list[i], sorted_list[i+1]);
}
}
if (sorted_list.size() > 0 && sorted_list.back() == game_object) {
sorted_list.pop_back(game_object);
}
}
Ambas funciones quedan más legibles que la función original y también las llamdas a función se simplifican.
Algunas veces puede ser conveniente usar un enum(mas que nada en ocasiones en las cuales se recibe la operación a realizar en una variable) y otras veces(como esta), resulta aún mejor simplemente separar en varias funciones.
Builder con Múltiples Banderas
Hay otro tipo de ocasión en la que podría ser tentador usar parámetros bool , y es cuando se trata de crear un objeto o entrada que tiene un montón de atributos que pueden ser verdadero o falso.
Por ejemplo, agregar un caracter a un texto, el caracter puede tener letra negrita, cursiva, subrayada o tachada. Escribir un enum con todas las configuraciones resulta impráctico, así que es tentador declarar algo como esto:
void addCharacter(char character, bool is_bold, bool is_italic, bool is_underlined, bool is_strikethrough);
Esto presenta el mismo problema(o incluso peor) que el antes descrito, imagina leer esto:
addCharacter('a', true, false, false, true);Hay 2 maneras de solucionar esto:
Máscaras
Una manera de evitar el montón de parámetros es reemplazando todas las banderas con un número entero, de manera que cada bit del número represente una de las banderas. En este caso podría ser algo así:
namespace CharacterAttributes {
enum Attributes {
BOLD = 1,
ITALIC = 2,
UNDERLINED = 4,
STRIKETHROUGH = 8
};
}
void addCharacter(const char character, const int attributes) {
const bool is_bold = (attributes & CharacterAttributes::BOLD);
const bool is_italic = (attributes & CharacterAttributes::ITALIC);
const bool is_underlined = (attributes & CharacterAttributes::UNDERLINED);
const bool is_strikethrough = (attributes & CharacterAttributes::STRIKETHROUGH);
...
De esta manera las llamadas se vuelven mucho más legibles:
addCharacter('a', CharacterAttributes::BOLD);
addCharacter('x', CharacterAttributes::ITALIC | CharacterAttributes::UNDERLINED);
Estructuras
Otra posible solución (que tiene más sentido cuando la cantidad de parámetros es enorme y no todos son bool), es utilizar una estructura para reemplazar a los parámetros:
struct AddCharacterParams {
char character;
bool is_bold;
bool is_italic;
bool is_underlined;
bool is_strikethrough;
uint32_t color;
std::string font;
};
void addCharacter(const AddCharacterParams& params) {
Para lectores no familiarizados con las versiones más recientes de C++ podría parecer que esto es contraproducente, ya que inicializar los valores de una estructura solo para llamar a una función no añade muchos puntos de legibilidad. Sin embargo, C++ 20 añadió algo que soluciona el problema de lleno: designated initializers.
Gracias a ellos es posible simplificar la llamada a la función de una manera que recuerda a Python o a TypeScript:
addCharacter({
.character = 'a',
.is_bold = true,
.is_italic = false,
.is_underlined = false,
.is_strike_through = false,
.color = 0xFF0000 // red,
.font = "Arial"});
En general es muy raro el caso en que agregar un parámetro bool es buena idea, pero en lo que se refiere al estilo de código, ninguna regla es absoluta. En particular, en el caso de los setters, los parámetros bool pueden resultar ser lo más práctico.
No hay comentarios.:
Publicar un comentario