IT eSky.pl

RSS

Czy powinniśmy współdzielić stałe pomiędzy kodem i specyfikacją?

Podczas jednego z code review w naszym zespole padło ciekawe pytanie: jeżeli w produkcyjnym kodzie mamy stałą [1], czy w specyfikacji powinniśmy się do niej odnieść, czy użyć bezpośredniej wartości?

Z jednej strony, użycie bezpośredniej wartości to duplikacja. Gdy taka wartość się zmieni, musimy zaktualizować i kod, i specyfikację – powinniśmy więc współdzielić stałą.

Z drugiej strony, specyfikacja powinna między innymi służyć jako mechanizm „podwójnej weryfikacji”. W związku z tym musi unikać współdzielenia kodu – w przeciwnym razie może fałszywie zgłaszać poprawność kodu, pomimo że wartość stałej jest błędna.

Oba argumenty wydają się sensowne. Które podejście powinniśmy więc wybrać?

Odpowiedź, jak zwykle, brzmi: to zależy.

Możemy wyróżnić dwa odmienne przypadki użycia stałych, wymagające przeciwstawnych strategii:

1. Wartość stałej ma znaczenie, ale fakt że jest to stała jest detalem implementacji.

Rozważmy funkcję konwertującą wartość podaną w metrach na tekst. Wartości mniejsze niż jeden metr powinny dodatkowo zostać przeliczone na centymetry, np. wartość 10.27 powinna zostać sformatowana jako „10.27m” a wartość 0.27 jako „27cm”.

Naiwna implementacja mogłaby wyglądać tak [2]:

Jak powinniśmy ją wyspecyfikować?

W ten sposób:

Czy w ten:

Wariant '27' + UNITS.centimeter może się wydawać mniej kruchy: jeśli będziemy chcieli zmienić format z „27cm” na np. „27 centimeters”, zadziała to w przezroczysty sposób, podczas gdy wariant '27cm' wymagałby aktualizacji specyfikacji.

Jednakże, zmiana formatu z „cm” na „centimeters” jest zmianą wymagań. Jest to jak najbardziej poprawne, że musimy zmienić specyfikację, gdy zmieniają się wymagania!

Z drugiej strony, rozważmy co się stanie gdy niechcący wprowadzimy błąd w wartości stałej:

Specyfikacja z bezpośrednią wartością '27cm' to wyłapie. Specyfikacja współdzieląca stałą, nie – będzie nadal, fałszywie, zgłaszać że kod jest poprawny.

Albo rozważmy następującą refaktoryzację:

Zmiana stałej z obiektu na tablicę to detal implementacji. Nie zmienia wymagań, nie powinna więc wymagać jakichkolwiek zmian w specyfikacji. Jednak w wariancie, który współdzieli stałą, specyfikacja się „wysypie”.

Tak więc, jeżeli wykorzystanie stałej jest detalem implementacji, NIE powinniśmy korzystać z niej w specyfikacji.

2. Wartość stałej nie ma znaczenia, ale fakt że jest ona stałą (konkretnego typu) jest ważny

Rozważmy inny przykład – funkcję, która sprawdza czy kolor karty jest „czarny” czy „czerwony”.

Implementacja mogłaby wyglądać tak [2]:

Pytanie jest takie samo jak wcześniej.

Czy powinniśmy ją wyspecyfikować w ten sposób:

Czy w ten:

Mimo, że może to nie być oczywiste na pierwszy rzut oka, mamy tu zupełnie odmienną sytuację niż w poprzednim przykładzie. Tym razem, nie ma dla nas znaczenia wartość stałej.

Możemy wyobrazić sobie taką refaktoryzację:

Zmiana wartości stałej z ciągów znaków na liczby (lub obiekty albo cokolwiek innego) jest detalem implementacji i nie ma znaczenia z punktu widzenia wymagań. Jednak w przypadku isBlack('spade'), specyfikacja się „wysypie”.

Przypadek isBlack(SUITS.spade) jest odporny na zmiany wartości stałej i będzie nadal działał poprawnie.

Co więcej, w przeciwieństwie do pierwszego przykładu, nie musimy się martwić o „fałszywe pozytywy” w przypadku błędu w wartości stałej.

Nawet jeśli popełnimy tego typu pomyłkę:

Zarówno kod jak i specyfikacja będą nadal działać poprawnie, nie musimy się więc przed tego typu sytuacją zabezpieczać.

Tak więc, jeśli stała jest istotną częścią struktury naszego modelu, POWINNIŚMY korystać z niej w specyfikacji.

A najlepiej w ogóle unikać stałych…

W obu powyższych przykładach milcząco założyliśmy, że stała jest w nich konieczna, i skupiliśmy się jedynie na tym, co z nią zrobić w naszej specyfikacji. Jednak w większości przypadków, możemy ulepszyć zarówno nasz kod jak i specyfikację, ukrywając lub całkowicie unikając wykorzystania stałej.

W pierwszym przykładzie, moglibyśmy np. ukryć stałą wewnątrz funkcji w ten sposób:

Dzięki temu, stała nie byłaby w ogóle dostępna z poziomu specyfikacji, co uwolniłoby nas od naszych wątpliwości.

W drugim przykładzie, moglibyśmy zastąpić stałą jakąś bardziej obiektowo-zorientowaną strukturą, która pozwoliłaby nam pisać tego typu czystą specyfikację:

Jednakże, jeśli rzeczywiście potrzebujesz w swoim kodzie stałej, przedstawione w tym poście wskazówki powinny pomóc Ci napisać lepszą specyfikację.

[1] Używam terminu „stała” w bardzo szerokim sensie. Różne języki programowania oferują wiele różnych konstrukcji składniowych, które mogą być potraktowane jako „stała” w kontekście tej dyskusji. Przedstawione w tym poście wskazówki odnoszą się równie dobrze do „prawdziwych” stałych (globalnych jak i na poziomie klasy), enum-ów, zmiennych statycznych czy nawet „pseudo-stałych” bazujących wyłącznie na konwencji nazewniczej.

[2] UWAGA: Przykłady, których używam, są wyjątkowo złym antywzorcem tego, jak powinno się implementować enum-y w JavaScripcie! Implementuję to w taki sposób wyłącznie ze względu na prostotę przykładów. Nigdy nie używaj czegoś podobnego w produkcyjnym kodzie!


Jak często zdarza Ci się używać stałych w Twoim kodzie? W jaki sposób specyfikujesz taki kod? Podziel się swoją opinią w komentarzach poniżej!


Zobacz również