C++
Técnicas de refactorización
Buscar..
Introducción
Refactorización se refiere a la modificación del código existente en una versión mejorada. Aunque la refactorización a menudo se realiza mientras se cambia el código para agregar características o corregir errores, el término en particular se refiere a mejorar el código sin necesariamente agregar características o corregir errores.
Recorrer la refactorización
Aquí hay un programa que podría beneficiarse de la refactorización. Es un programa simple que utiliza C ++ 11, cuyo objetivo es calcular e imprimir todos los números primos del 1 al 100 y se basa en un programa que se publicó en CodeReview para su revisión.
#include <iostream>
#include <vector>
#include <cmath>
int main()
{
int l = 100;
bool isprime;
std::vector<int> primes;
primes.push_back(2);
for (int no = 3; no < l; no += 2) {
isprime = true;
for (int primecount=0; primes[primecount] <= std::sqrt(no); ++primecount) {
if (no % primes[primecount] == 0) {
isprime = false;
break;
} else if (primes[primecount] * primes[primecount] > no) {
std::cout << no << "\n";
break;
}
}
if (isprime) {
std::cout << no << " ";
primes.push_back(no);
}
}
std::cout << "\n";
}
La salida de este programa se ve así:
3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
Lo primero que notamos es que el programa no imprime 2
que es un número primo. Podríamos simplemente agregar una línea de código para simplemente imprimir esa constante sin modificar el resto del programa, pero sería mejor refactorizar el programa para dividirlo en dos partes: una que crea la lista de números primos y otra que las imprime. . Así es como podría verse:
#include <iostream>
#include <vector>
#include <cmath>
std::vector<int> prime_list(int limit)
{
bool isprime;
std::vector<int> primes;
primes.push_back(2);
for (int no = 3; no < limit; no += 2) {
isprime = true;
for (int primecount=0; primes[primecount] <= std::sqrt(no); ++primecount) {
if (no % primes[primecount] == 0) {
isprime = false;
break;
} else if (primes[primecount] * primes[primecount] > no) {
break;
}
}
if (isprime) {
primes.push_back(no);
}
}
return primes;
}
int main()
{
std::vector<int> primes = prime_list(100);
for (std::size_t i = 0; i < primes.size(); ++i) {
std::cout << primes[i] << ' ';
}
std::cout << '\n';
}
Al probar esta versión, vemos que sí funciona correctamente ahora:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
El siguiente paso es observar que la segunda cláusula if
no es realmente necesaria. La lógica en el bucle busca los factores primos de cada número dado hasta la raíz cuadrada de ese número. Esto funciona porque si hay algunos factores primos de un número, al menos uno de ellos debe ser menor o igual a la raíz cuadrada de ese número. Trabajando solo esa función (el resto del programa sigue siendo el mismo) obtenemos este resultado:
std::vector<int> prime_list(int limit)
{
bool isprime;
std::vector<int> primes;
primes.push_back(2);
for (int no = 3; no < limit; no += 2) {
isprime = true;
for (int primecount=0; primes[primecount] <= std::sqrt(no); ++primecount) {
if (no % primes[primecount] == 0) {
isprime = false;
break;
}
}
if (isprime) {
primes.push_back(no);
}
}
return primes;
}
Podemos ir más lejos, cambiando los nombres de las variables para que sean un poco más descriptivos. Por ejemplo, primecount
no es realmente un conteo de primos. En su lugar, es una variable de índice en el vector de números primos conocidos. Asimismo, si bien no
se utiliza a veces como una abreviatura de "Número", en la escritura matemática, es más común el uso de n
. También podemos hacer algunas modificaciones eliminando la break
y declarando las variables más cerca de donde se usan.
std::vector<int> prime_list(int limit)
{
std::vector<int> primes{2};
for (int n = 3; n < limit; n += 2) {
bool isprime = true;
for (int i=0; isprime && primes[i] <= std::sqrt(n); ++i) {
isprime &= (n % primes[i] != 0);
}
if (isprime) {
primes.push_back(n);
}
}
return primes;
}
También podemos refactorizar main
para usar un "rango para" para hacerlo un poco más limpio:
int main()
{
std::vector<int> primes = prime_list(100);
for (auto p : primes) {
std::cout << p << ' ';
}
std::cout << '\n';
}
Esta es solo una de las formas en que se puede realizar la refactorización. Otros pueden hacer elecciones diferentes. Sin embargo, el propósito de la refactorización sigue siendo el mismo, que es mejorar la legibilidad y posiblemente el rendimiento del código sin necesariamente agregar características.
Ir a la limpieza
En las bases de código C ++ que solían ser C, uno puede encontrar el patrón que se va a goto cleanup
. Como el comando goto
hace que el flujo de trabajo de una función sea más difícil de entender, a menudo esto se evita. A menudo, puede ser reemplazado por declaraciones de devolución, bucles, funciones. Sin embargo, con la goto cleanup
uno necesita deshacerse de la lógica de limpieza.
short calculate(VectorStr **data) {
short result = FALSE;
VectorStr *vec = NULL;
if (!data)
goto cleanup; //< Could become return false
// ... Calculation which 'new's VectorStr
result = TRUE;
cleanup:
delete [] vec;
return result;
}
En C ++ se podría usar RAII para solucionar este problema:
struct VectorRAII final {
VectorStr *data{nullptr};
VectorRAII() = default;
~VectorRAII() {
delete [] data;
}
VectorRAII(const VectorRAII &) = delete;
};
short calculate(VectorStr **data) {
VectorRAII vec{};
if (!data)
return FALSE; //< Could become return false
// ... Calculation which 'new's VectorStr and stores it in vec.data
return TRUE;
}
A partir de este punto, uno podría continuar refactorizando el código real. Por ejemplo, reemplazando el VectorRAII
por std::unique_ptr
o std::vector
.