C++
Techniki refaktoryzacji
Szukaj…
Wprowadzenie
Refaktoryzacja odnosi się do modyfikacji istniejącego kodu w ulepszoną wersję. Chociaż refaktoryzacja jest często przeprowadzana podczas zmiany kodu w celu dodania funkcji lub naprawienia błędów, termin ten dotyczy w szczególności ulepszania kodu bez konieczności dodawania funkcji lub naprawiania błędów.
Przeprowadź refaktoryzację
Oto program, który może skorzystać z refaktoryzacji. Jest to prosty program wykorzystujący C ++ 11, który ma na celu obliczenie i wydrukowanie wszystkich liczb pierwszych od 1 do 100 i jest oparty na programie opublikowanym do przeglądu w CodeReview .
#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";
}
Dane wyjściowe tego programu wyglądają następująco:
3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
Pierwszą rzeczą, którą zauważamy, jest to, że program nie drukuje 2
który jest liczbą pierwszą. Moglibyśmy po prostu dodać wiersz kodu, aby po prostu wydrukować tę stałą bez modyfikowania reszty programu, ale fajniej byłoby zrefaktoryzować program i podzielić go na dwie części - jedną, która tworzy listę liczb pierwszych i drugą, która je drukuje . Oto jak to może wyglądać:
#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';
}
Po wypróbowaniu tej wersji widzimy, że rzeczywiście działa teraz poprawnie:
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
Następnym krokiem jest zauważenie, że druga klauzula if
nie jest tak naprawdę potrzebna. Logika w pętli wyszukuje czynniki pierwsze każdej podanej liczby do pierwiastka kwadratowego z tej liczby. Działa to, ponieważ jeśli istnieją jakieś czynniki pierwsze liczby, przynajmniej jeden z nich musi być mniejszy lub równy pierwiastkowi kwadratowemu tej liczby. Przerabiając tylko tę funkcję (reszta programu pozostaje taka sama) otrzymujemy ten wynik:
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;
}
Możemy pójść dalej, zmieniając nazwy zmiennych, aby były bardziej opisowe. Na przykład liczba primecount
nie jest tak naprawdę liczbą liczb pierwszych. Zamiast tego jest to zmienna indeksu w wektorze znanych liczb pierwszych. Ponadto, chociaż no
jest czasami używane jako skrót od „liczby”, w piśmie matematycznym częściej używa się n
. Możemy również wprowadzić pewne modyfikacje, eliminując break
i deklarując zmienne bliżej miejsca ich użycia.
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;
}
Możemy również refaktoryzować main
aby użyć „zasięgu”, aby był nieco bardziej uporządkowany:
int main()
{
std::vector<int> primes = prime_list(100);
for (auto p : primes) {
std::cout << p << ' ';
}
std::cout << '\n';
}
Jest to tylko jeden ze sposobów refaktoryzacji. Inni mogą dokonywać różnych wyborów. Jednak cel refaktoryzacji pozostaje ten sam, czyli poprawa czytelności i być może wydajności kodu bez konieczności dodawania funkcji.
Idź do czyszczenia
W bazach kodu C ++, które były C, można znaleźć wzorzec goto cleanup
. Ponieważ polecenie goto
utrudnia zrozumienie przepływu pracy funkcji, często tego się unika. Często można go zastąpić instrukcjami zwrotnymi, pętlami, funkcjami. goto cleanup
należy pozbyć się logiki czyszczenia.
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;
}
W C ++ można użyć RAII, aby rozwiązać ten problem:
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;
}
Od tego momentu można kontynuować refaktoryzację rzeczywistego kodu. Na przykład zastępując VectorRAII
przez std::unique_ptr
lub std::vector
.