• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Refaktoryzacja kodu

Object Storage Arubacloud
0 głosów
767 wizyt
pytanie zadane 3 października 2015 w C i C++ przez TenTakiTam Bywalec (2,460 p.)

Czy ten kod można jakoś zrefaktoryzować?

#include <iostream>
#include <windows.h>
#include <stdio.h>


using namespace std;

int s;
int m;
int g;

int main()
{
    while(1)
    {
        s++;
        int sec=s;

        Sleep(1000);

        if (s==60)
        {
            m++;
            s=0;

            if (m==60)
            {
                g++;
                m=0;
            }
        }

        system("cls");
        cout<<g<<":"<<m<<":"<<s<<endl;


    }
    return 0;
}

 

5 odpowiedzi

+2 głosów
odpowiedź 3 października 2015 przez draghan VIP (106,230 p.)
Przy tak krótkim programie nie za bardzo jest miejsce na refaktoryzację. Jedyne, co możnaby poprawić, to usunięcie zmiennych globalnych, ale też za wiele sensu w tym nie ma.

Ewentualnie mógłbyś się pokusić o uczynienie tego programiku multiplatformowym, poprzez usunięcie nagłówka "windows.h" i wprowadzenie odpowiedników funkcji z innych systemów operacyjnych. :)
komentarz 3 października 2015 przez draghan VIP (106,230 p.)
edycja 3 października 2015 przez draghan

Ach, jeszcze jedno zauważyłem. W każdym kolejnym obiegu pętli tworzysz nową zmienną
int sec - powinieneś definicję tej zmiennej wyrzucić nad pętlę, a przypisanie zostawić tam, gdzie jest. :)

komentarz 3 października 2015 przez adrian17 Ekspert (345,160 p.)
Nope, powinno się je deklarować jak najbliżej miejsca pierwszego użycia (to, że nie jest tu używana, to inna sprawa). Przy złożonych typach można by się martwić o konstrukcję w każdej iteracji, ale w przypadku prostych typów kompilator raz alokuje miejsce na nie na stosie i nic więcej z nim nie robi, więc koszt tego jest dokładnie zerowy.
komentarz 3 października 2015 przez draghan VIP (106,230 p.)
Dobry argument. :)
+1 głos
odpowiedź 3 października 2015 przez adrian17 Ekspert (345,160 p.)
Usunięcie zmiennych globalnych zawsze ma sens. Radzę też zawsze dodawać spacje między operatorami, znacząco poprawia czytelność.
Przy okazji, kompilator nie narzeka na nieużytą zmienną sec?
komentarz 3 października 2015 przez draghan VIP (106,230 p.)

Niekoniecznie zawsze. :) Jaki sens jest w tym programie?

Nie poprawia czytelności, nie powoduje zwiększenia poziomu bezpieczeństwa programu... Tutaj takie zmienne nie powodują też problemów z zasięgiem, bo cały kod programu to tylko krótki main, w dodatku mamy jedną jedyną jednostkę translacji. Nie ma się gdzie zgubić. :)

Oprócz sec, która faktycznie jest nieużyteczna - tego nie dopatrzyłem i za to ode mnie yes. :)

komentarz 3 października 2015 przez adrian17 Ekspert (345,160 p.)
To proste - jako wzmacnianie dobrych praktyk. Nie powinno się myśleć "meh, krótki program, wrzucę zmienne przed main", tylko domyślnie od razu pisać je w funkcji, bo to nic nie kosztuje intelektualnie ani nawet w liczbie wciśniętych klawiszy na klawiaturze, a na dłuższą metę pomaga.
komentarz 3 października 2015 przez draghan VIP (106,230 p.)
Skoro tak podchodzisz, to odruchowo powinno się również eliminować definicje zmiennych z pętli (oprócz jakichś warunkowych ścieżek, co oczywiste).
komentarz 3 października 2015 przez adrian17 Ekspert (345,160 p.)
Tego zdania akurat nie zrozumiałem, sorry. Wyjaśnisz?
komentarz 3 października 2015 przez draghan VIP (106,230 p.)

Oczywiście, że wyjaśnię. Eliminowanie definiowania zmiennych z pętli ma wzmocnić dobrą praktykę - zerowym kosztem nihilujemy narzut, związany z wielokrotnym tworzeniem obiektu.

Jak sam napisałeś, przy typach wbudowanych nie ma większej różnicy... Jednak dla obiektów klasy nietrywialnej już ma.

Nie mówię tutaj o okazjonalnym stworzeniu pomocniczej zmiennej, przy pewnym spełnionym warunku (spełnionym w m obiegach pętli, gdzie m << n, przy czym n jest liczbą iteracji) - to jest wygodne, czytelne i nie opóźnia naszego programu.

komentarz 3 października 2015 przez adrian17 Ekspert (345,160 p.)

Umieszczenie deklaracji jak najbliżej miejsca faktycznego użycia jest naturalnym następstwem zasady nie używania zmiennych globalnych i ma takie samo uzasadnienie - zmniejsza zagracenie zewnętrznych scope'ów, co ułatwia rozumienie zależności w kodzie i poprawia jakość analizy/sugestii IDE.

W dodatku w przypadku obiektów ciężkich zadeklarowanych w jakimś warunku różnica jest pozytywna:

A a; // konstruowany za kazdym razem
if(warunek) {
    B b; // miejsce zarezerwowane na stosie, ale konstrukcja nastepuje tylko jesli kod dojdzie tutaj
}

Co do narzutu wielokrotnego tworzenia obiektu w pętli:

for(...) {
    A a; // konstruowany za kazdym razem
}

To można rozumować że jeśli to w zasadzie jest ten sam obiekt więc prawdziwe miejsce jego użycia jest poza pętlą.

No a jeśli za każdym razem i tak zupełnie zmieniasz wartość obiektu, na przykład:

std::string str;
for(int i = 0; i < 100000; ++i) {
    str = std::to_string(i);
    // zrob cos z str
}

To może się okazać że w zasadzie nie jest to w żaden sposób lepsze od normalnego konstruowania od nowa:

for(int i = 0; i < 100000; ++i) {
    std::string str = std::to_string(i);
    // zrob cos z str
}

 

komentarz 3 października 2015 przez event15 Szeryf (93,790 p.)

A ja to bym chciał zobaczyć jak kompilator to zamienia na ASM:

std::string str;
for(int i = 0; i < 100000; ++i) {
    str = std::to_string(i);
    // zrob cos z str
}

Oraz to:

for(int i = 0; i < 100000; ++i) {
    std::string str = std::to_string(i);
    // zrob cos z str
}

Żeby mieć rzeczywisty obraz zamiast domysłów. Bo dziś można się spodziewać wszystkiego :) 

komentarz 3 października 2015 przez draghan VIP (106,230 p.)
edycja 3 października 2015 przez draghan

W ASMa to się nigdy nie bawiłem, chociaż niedługo pewnie się to zmieni (mam to w programie studiów). Ale zrobiłem mały - nieprofesjonalny wręcz - test, który jednak obrazuje to, o co mi chodziło.

Weźmy prosty program, pokazowy i wręcz... debilny. Kompilowany g++ 4.8.

#include <iostream>
using namespace std;

//#define IN_LOOP

struct Foo
{
    int *t;
    Foo()
    {
        t = new int[100];
        for(unsigned i = 0; i < 100; ++i)
            t[i] = 0;
    }
    ~Foo(){ delete[] t; }
    Foo(const Foo & f)
    {
        t = new int[100];
        for(unsigned i = 0; i < 100; ++i) t[i] = f.t[i];
    }

    Foo & operator=(const Foo &f)
    {
        if(this == &f) return *this;
        for(unsigned i = 0; i < 100; ++i) t[i] = f.t[i];
        return *this;
    }
};

int main()
{
    Foo f1;
    // jakieś operacje na f1
    f1.t[30] = 3;
    f1.t[50] = 5;
#ifndef IN_LOOP
    Foo f2;
#endif // IN_LOOP
    for(unsigned i = 0; i < 1000000; ++i)
    {
        #ifdef IN_LOOP
        Foo
        #endif // IN_LOOP
        f2 = f1;

    }
    return 0;
}

Po przedstawieniu średnich czasów wykonania dla 15 uruchomień tego programu (na tej samej maszynie, z porównywalnym obciążeniem systemu [uruchomiony "na czysto" system z otwartym C::B]) ze zdefiniowanym makrem IN_LOOP oraz z niezdefiniowanym, sądzę że nie trzeba się silić na rozbudowany komentarz.

IN_LOOP: 0.791667 s
IN_LOOP: 0.583467 s

Także jeśli nie podasz mi dowodu, że definiowanie obiektów nietrywialnych wewnątrz pętli nie wprowadza dodatkowego narzutu, to Ci po prostu nie uwierzę. :)

komentarz 3 października 2015 przez adrian17 Ekspert (345,160 p.)
edycja 3 października 2015 przez adrian17

No tak, tutaj to jak najbardziej ma sens, bo masz już zaalokowany bufor. Nie powiedziałem że deklarowanie poza pętlą nigdy nie ma sensu, tylko że nie zawsze ma sens. Mogę pokazać odwrotny przypadek, w którym deklaracja przy przypisaniu jest lepsza, bo pozwala na RVO:

Foo func() {
    Foo f1;
    // jakieś operacje na f1
    f1.t[30] = 3;
    f1.t[50] = 5;
    return f1;
}
 
int main()
{
#ifndef IN_LOOP
    Foo f2;
#endif // IN_LOOP
    for(unsigned i = 0; i < 1000000; ++i)
    {
        #ifdef IN_LOOP
        Foo
        #endif // IN_LOOP
        f2 = func();
    }
}

Tutaj wersja IN_LOOP była u mnie mniej więcej dwa razy szybsza.

A ja to bym chciał zobaczyć jak kompilator to zamienia na ASM:

Dokładnie ASM nie pokażę bo to burdel, ale zrobiłem analogiczny kod z std::vector w którym dodałem do jego implementacji parę printf'ów (do std::string nie mogłem, pewnie używa implementacji z libstdc++) żeby pokazać różnicę w wywoływanych konstruktorach: http://hastebin.com/inolerufaj.cpp

+1 głos
odpowiedź 3 października 2015 przez rafal.budzis Szeryf (85,260 p.)
1. zmianna sec nie jest używana

2. nazwy s,m,g nie dają nam zadnych informacji co robią i są do siebie podobne co sprawia ze nie dopatrzenie jakiejś literówki może zrobić błąd. Powinieneć zmienić je na pełne nazwy.

3. zawsze staraj się eliminować zagłebienia if (m==60) można wyciągnąć na zewnącz if(s==60)

4. po wyciągnieciu dochodzimy do wniosku ze nie mamy potrzeby za każdym razem sprawdzania if(m==60) więc robimy funkcje i stosujemy return aby nie wykonywać nie potrzebnie tego porównania

5. teraz można zauważyć ze oba if`y są bardzo podobne i można dojśc do wniosku ze można je wykonywać w pętli gdybyśmy zmienne zamienili na 3 elementową tablice

6 pewnie jeszcze coś by się znalazło.
komentarz 3 października 2015 przez draghan VIP (106,230 p.)
A ja nadal nie widzę sensu w refactoringu takiego banału. :)
komentarz 3 października 2015 przez rafal.budzis Szeryf (85,260 p.)

@draghan Nie wiem w jakim wieku jesteś ale jak zaczniesz pracować docenisz refraktoryzowanie kodu. Wystarczy pierwsze spotkanie z czyimś kodem gdzie są jednoliterowe zmienne i już bedzie ci się chciało klnąć i zmienić prace bo nic nie mozesz znaleźć. Pisanie przejszystego kodu to też szanowanie współpracowników. Chodzio o szybkośc analizowania kodu ile zajeło ci przeanalizoweanie kodu z przykładu ? Pewnie z minute lub dwie i dowiedziałeś się ze dodaje jeden do zmiennej s zatrzymuje program na sekunde i pokazuje dane. A teraz ile zajmie ci przeanalizowanie tego ?

int main()
{
    while(1)
    {
		Sleep(1000);
		addOneSecond();
		showTime();
    }
    return 0;
}

O wiele szybciej dowiedziałeś się ze program jest zatrzymywany ze jest dodawana sekunda i ze czas jest potem pokazywany już wiesz co robi program! Teraz zmienne masz zmienne s,m,g możesz tylko zgadywać co to jest. Każdy programista lubi zagadki więc każdy się zatrzymuje i myśli hmmm może chodzić o czas. Taki zapis spowodował by brak zastanawiania się.

int hour, minute, second;

Dodatkowo łatwiej znaleźć błąd w kodzie dzięki opisowym nazwą.

if (s==60)
{
	m++;
	s=0;
	if (m==60)
	{
		s++;
		m=0;
	}
}

Prawdopodobnie troche ci zajmie gdzie jest błąd lecz po dodaniu nazw znajdziesz go szybciej 

if (second==60)
{
	hour++;
	second=0;
	if (minute==60)
	{
		hour++;
		minute=0;
	}
}

a tak moim zadniem powinien wyglądać kod całości 

#include <iostream>
#include <windows.h>
#include <stdio.h>

using namespace std;

int hour, minute, second;
void addOneSecond();
void showTime();

int main()
{
	while(1)
	{
		Sleep(1000);
		addOneSecond();
		showTime();
	}
	return 0;
}

void addOneSecond()
{
	second++;

	if (second<60)
	{
		return;
	}

	minute++;
	second=0;

	if (minute<60)
	{
		return;
	}

	hour++;
	minute=0;
}

void showTime()
{
	system("cls");
	cout<<hour<<":"<<minute<<":"<<second<<endl;
}
komentarz 3 października 2015 przez draghan VIP (106,230 p.)
Nawet nie śmiem zaprzeczyć - byłbym skończonym głupcem. Zgadzam się ze wszystkim - tak to powinno wyglądać. :) Ale proszę, powiedz mi: jaki jest sens refactoringu konkretnie tego programu?

Przecież to jest HelloWorld++, a refactoring przeprowadza się w określonym celu, którym jest ułatwienie pielęgnacji programu.
komentarz 4 października 2015 przez rafal.budzis Szeryf (85,260 p.)

draghan aby ćwiczyć umięjętności pisania czystego kodu. Poza tym gdyby ktos wkleił swój kod na 500 linijek nikt by nie odpowiedział a tak ma już sporo podstaw które może zastosować w dłuższym kodzie. Pamiętajmy ze każdy najmniejszy kod się rozrasta ja tu widze czas gry wyświetlany w roku ekranu teraz czas na gre! Zawsze zaczyna się od małych kodów :)

komentarz 4 października 2015 przez event15 Szeryf (93,790 p.)
@draghan, czytając Twoją wypowiedź śmiem twierdzić, że nie masz w sobie duszy programisty. Nie jesteś zakochany w pięknym kodzie i nie jest to dla Ciebie ważne. W każdym programie, nawet najbanalniejszym i najmniejszym powinno się stosować takie zasady, jakie podał @rafal612b. Wyobraź sobie, że ten program wykorzystuje tylko proste QuickSort. QuickSorty czy inne algorytmy wyszukiwania czy sortowania to przykłady programów o ekstremalnie niskiej czytelności.

Ale wyobraź sobie, że zamiast różnych zmiennych typu t[100] m1 a t b itp. to jest to tak czytelny algorytm, który sam tłumaczy siebie. Nie dość że programista wie z nazwy funkcji że to quicksort to jeszcze może sobie na szybko przeanalizować czy robi to dobrze. A uwierz mi, że często się zdarza przy refaktoringu że kilka rzeczy w takich algorytmach można spokojnie usunąć :) CHociażby na cele czytelności jak i optymalizacji.

Roy Osherove w swojej książce podaje, że refaktoryzacja jest takim procesem zmieniania kodu, aby nie zmienić jego zachowania a uprościć jego użycie i uelastycznić na zmiany.

Teraz jest funkcja addOneSecond, ale w przyszłości chłopak będzie chciał to zmienić i zrobi funkcję addSecond(int seconds) i wtedy będzie dodawać określoną ilość sekund. Będzie mógł ten kod wykorzystać w jakimkolwiek innym programie, nie będzie musiał tego przepisywać, tworzyć na nowo. Zaincluduje odpowiedni plik i ma.
komentarz 4 października 2015 przez draghan VIP (106,230 p.)

@draghan, czytając Twoją wypowiedź śmiem twierdzić, że nie masz w sobie duszy programisty

Dzięki... ;__; Wychodzi więc na to, że jestem bezduszny. >.-

komentarz 4 października 2015 przez event15 Szeryf (93,790 p.)
Odniosłeś się akurat do najmniej znaczącego zdania w mojej wypowiedzi
komentarz 4 października 2015 przez draghan VIP (106,230 p.)
Bo resztę akceptuję i nie zamierzam polemizować. :)
+1 głos
odpowiedź 3 października 2015 przez event15 Szeryf (93,790 p.)
Tak swoją drogą co masz na myśli używając tego słowa? Chodzi o optymalizację aby program szybciej działał? Czy może, eliminację kroków?

Bo refaktoryzacja to także wydzielenie takich fragmentów kodu na funkcje, tworzenie fabryk, nadawanie zmiennym znaczących nazw a nie "a, s, m" które nic Ci nie powiedzą za 2 miesiące kiedy będziesz czytać kod. Poza tym g m s to już bardziej H m s ewentualnie h m s (w zależności jaki format by się przyjęło. W pętli while np zamiast 1 to ładniej byłoby "true". TO jest refaktoryzacja, a optymalizacja moim zdaniem to część refaktoryzacji.
+1 głos
odpowiedź 3 października 2015 przez efiku Szeryf (75,160 p.)
Od siebie mogę dać radę taką, aby nie skracać zmiennych gdyż to utrudnia czytanie takiego kodu.

Podobne pytania

0 głosów
0 odpowiedzi 450 wizyt
0 głosów
1 odpowiedź 369 wizyt
pytanie zadane 12 lipca 2020 w JavaScript przez Arcywojak Początkujący (370 p.)
0 głosów
2 odpowiedzi 378 wizyt
pytanie zadane 24 grudnia 2019 w JavaScript przez Paweł Szewczyk Obywatel (1,410 p.)

92,631 zapytań

141,498 odpowiedzi

319,869 komentarzy

62,011 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...