Blog

23 stycznia 2015 Wojciech Zawistowski

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

Tagi:

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]:

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!

Zobacz na blogu

09.09.2022
Marcin Jahn
It’s Not Just HTTP It’s Not Just HTTP

In today’s world of cloud-based solutions, distributed systems, and microservices-based architectures, network communication is a...

23.08.2022
Adam Mrowiec
Konferencja IPC 2022 Berlin Konferencja IPC 2022 Berlin

Pandemia wreszcie się kończy, dlatego w tym roku postanowiliśmy wrócić do naszych wyjazdów na konferencje....