Strona główna » Blog » A jednak kod może być zduplikowany!

A jednak kod może być zduplikowany!

Wśród programistów krążą różne specyficzne przekonania na temat poprawnego tworzenia kodu. Trudno powiedzieć skąd te przekonania się biorą, bo też ich źródła mogą być rozmaite – czasami to tylko subiektywne opinie oparte na własnych doświadczeniach i przemyśleniach, czasami to dziedzictwo po dawnych językach programowania a jeszcze innym razem może być to wynik złożonych analiz albo nawet badań naukowych. Czasami pewne reguły tworzą się poprzez przeinaczenie lub nadmierne uogólnienie innych reguł. Dzisiaj chciałbym się odnieść do kwestii zduplikowanych fragmentów kodu, których występowanie w całym kodzie zdaje się być posądzane o bycie przyczyną wielu niedobrych rzeczy: m.in. zduplikowany kod ma utrudniać późniejsze wprowadzanie zmian oraz prowadzić do błędów. Czy tak rzeczywiście jest?

Badania naukowe

Ogólnie rzecz biorąc sprawa wydaje się być poważna, bo rozmaite instytucje zaangażowały się w tworzenie mechanizmów wyszukiwania duplikatów w kodzie. Powstało wiele różnych aplikacji i narzędzi oraz napisano rozmaite artykuły naukowe na temat ich tworzenia. Samych jednak badań nad efektami występowania lub nie występowania zduplikowanego kodu nie jest tak dużo. Nie jest ich jednak też mało i stąd mogę polecić opracowanie naukowe omawiające w jednym miejscu wiele z nich: “Harmfulness of Code Duplication – A Structured Review of the Evidence” by Wiebe Hordijk, María Laura Ponisio, Roel Wieringa.

Lektura tego opracowania jest bardzo interesująca ponieważ okazuje się, że aktualnie zduplikowany kod nie wydaje się być szczególnie groźny. Badania wskazują na to, że zjawisko powstawania błędów i niespójności z tytułu istnienia duplikatów występuje dosyć rzadko.

“(…) inconsistent change does not occur very often (…) These findings indicate that duplication can indeed lead to errors in software, but only in very small numbers (…) a minority of clones with context differences were confirmed to contain bugs.”

Ponadto, w kodzie pozbawionym duplikatów odnajduje się 1,7 razy więcej błędów na linię kodu. To trochę zaskakujące, aczkolwiek być może padamy tutaj ofiarą statystyki albowiem kod zduplikowany może po prostu zawierać więcej linii kodu.

“They found that modules without clones have 1.7 times as many errors per line of code than modules with clones. Their explanation is that when one copies code that works, one will not generally introduce bugs; this could imply that the practice of duplication reduces the number of errors in a system. However, with clones, more lines of code are needed to implement the same functionality, and those extra lines dilute the number of errors per line.”

Z drugiej strony, badania zdają się nie znajdywać potwierdzenia dla tej tezy, która wydawać by się mogła tutaj oczywista – czyli że zduplikowane fragmenty kodu rzeczywiście zwiększają rozmiar całego kodu. Okazuje się, że kod optymalizowany-ponownie-używalny, który ma zastępować zduplikowane fragmenty kodu, potrafi sam w sobie przybrać na wadze tyle, co fragmenty, które deduplikuje! Stąd badacze piszą otwarcie:

“We cannot draw conclusions about the relation between duplication and code comprehension.”

“It is simple to measure the total size of code involved in a clone set. (…) It is difficult to estimate the size of code needed in an alternative solution, for example after refactoring. Replacing clones by an alternative does not always result in reducing the code size”

“It cannot be stated in general that duplication increases code size compared to alternatives”

Czyli że statystyka wcale nie musi nas oszukiwać… Kod optymalizowany w jakiś sposób może wprowadzać więcej błędów sam z siebie. Jest to jeden z powodów dla których to wcale niełatwe zadanie ocenić szkodliwość zduplikowanego kodu. Musimy bowiem porównać jego szkodliwość ze szkodliwością kodu zoptymalizowanego.

“When clones are changed inconsistently, that is clearly a source of errors. However, had the code not been duplicated, it might have been more complex, and other errors might have been made when changing it. To know whether duplication causes an increase of errors through inconsistent change, we need to compare the amount of errors introduced into duplicated code with that in non-duplicated code. If the extra errors introduced through inconsistent changes do not lift the amount of errors in duplicated code above the normal amount of errors in a comparable body of code in the same system, then we cannot hold the conclusion that duplication causes more errors.”

No właśnie, właśnie…

Badaczy zaskoczyło coś jeszcze. Okazuje się, że kod zduplikowany nie zmienia się wcale tak często jak ten nie-zduplikowany. Przez to też rzadziej jest źródłem błędów w wynikających z niespójności o które jest tak posądzany. Badacze proponują różne wyjaśnienia tego fenomenu, ale nie badają go głębiej.

“It turns out that overall, cloned code is changed less often than non-cloned code. The reason for this phenomenon has not been investigated. A possible explanation is that programmers try to avoid changing cloned code because it is more difficult to change, which would strengthen the hypothesis that duplication decreases changeability. However, it could also be that code contains fewer clones because it is changed more often, when those changes include refactorings to remove clones. Also the programmers may have been careful enough to only clone code which was not likely to be changed often. Yet another explanation is that most code clones are templates, company standards etc. that are never changed.”

Może jednak trochę Repeat Yourself

Może jednak nie powinniśmy być tym wszystkim tak bardzo zaskoczeni. Skąd właściwie wzięło się przekonanie o wielkiej szkodliwości zduplikowanego kodu? Nie znam oczywiście odpowiedzi, ale może wzięło się to z jakiejś specyficznej interpretacji reguły DRY, czyli Don’t Repeat Yourself, którą sformułowali Andy Hunt i Dave Thomas w książce “Pragmatic Programmer”. Nie czytałem tej książki, więc nie wiem jak tę regułę oryginalnie sformułowano. Wikipedia podaje, że stosowanie DRY przynosi korzyści:

“(…) znaczną oszczędność czasu, bo zmianę trzeba wprowadzić tylko w jednym miejscu, i nerwów związanych z pominięciem któregoś z wystąpień wadliwego kodu (…) [oraz] wymusza lepsze specyfikowanie funkcjonalności i projektowanie interfejsów w kierunku ich uniwersalności”

Być może przyjęta zbyt bezkrytycznie reguła ta może prowadzić do jakiś szkodliwych rozwiązań. Z jednej strony potrafi ona np. poprawić czytelność poprzez zachęcenie do stosowania stałych (constants), ale z drugiej strony czytelność może zostać pogorszona a praca programisty utrudniona, gdy musi on mentalnie “skakać” między deklaracjami powiązanych ze sobą funkcji, przechodzić złożone hierarchie klas czy rozszyfrowywać niezrozumiałe makra. W tym kontekście spodobał mi się komentarz Roberta Harveya na Programmers Stack Exchange:

All other things being equal, linear, line-by-line code is easier to read than jumping around function calls. A non-trivial function call always takes parameters, so you have to sort all that out and make mental contextual jumps from function call to function call. Always favor better code clarity, unless you have a good reason for being obscure (such as obtaining necessary performance improvements).

So why is code refactored into separate methods then? To improve modularity. To collect significant functionality behind an individual method and give it a meaningful name. If you’re not accomplishing that, then you don’t need those separate methods.

Podsumowanie

Badania naukowe wskazują na to, że kod zduplikowany nie jest wcale tak szkodliwy jak się mogłoby wydawać. Może nawet nie opłaca się go deduplikować. Może lepiej poświęcić czas na to, aby dopracować pozostały kod. Być może zajmowanie się duplikatami jest analogiczną pułapką jak przedwczesna optymalizacja, o której tak pisał Donald Knuth:

“Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%”

Dodaj komentarz