Blog
Czy powinniśmy współdzielić stałe pomiędzy kodem i specyfikacją?
Tagi: tdd
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]:
function toText(meters) {
if (meters >= 1) {
return '' + meters + UNITS.meter;
} else {
return '' + (meters * 100) + UNITS.centimeter;
}
};
UNITS = {
meter: 'm',
centimeter: 'cm'
};
Jak powinniśmy ją wyspecyfikować?
W ten sposób:
expect(toText(0.27)).toEqual('27cm');
Czy w ten:
expect(toText(0.27)).toEqual('27' + UNITS.centimeter);
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:
UNITS = {
meter: 'm',
centimeter: 'blah'
};
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ę:
function toText(meters) {
if (meters >= 1) {
return '' + meters + UNITS[0];
} else {
return '' + (meters * 100) + UNITS[1];
}
};
UNITS = ['m', 'cm'];
};
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]:
function isBlack(suit) {
if (suit == SUITS.spade || suit == SUITS.club) {
return true;
} else {
return false;
}
};
SUITS = {
spade: 'spade',
heart: 'heart',
diamond: 'diamond',
club: 'club'
};
Pytanie jest takie samo jak wcześniej.
Czy powinniśmy ją wyspecyfikować w ten sposób:
expect(isBlack('spade')).toBeTruthy();
Czy w ten:
expect(isBlack(SUITS.spade)).toBeTruthy();
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ę:
function isBlack(suit) {
// ...
};
SUITS = {
spade: 1,
heart: 2,
diamond: 3,
club: 4
};
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ę:
SUITS = {
spade: 'BLAH',
heart: 'heart',
diamond: 'diamond',
club: 'club'
};
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:
function toText(meters) {
var UNITS = {/*...*/};
// ...
};
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ę:
var spade = Suits.spade();
expect(spade.isBlack()).toBeTruthy();
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!