Best practices: wygrać ze złożonością – nazewnictwo zmiennych



- O, widzę, że stosujecie standardy nazewnicze! To wspaniale!
- A dlaczego stosujecie taką konwencję?
- Yyy…Noooo w poprzedniej firmie tak robiliśmy… 

Standardy - irytujący nakaz czy przydatne narzędzie?
Źle przygotowane standardy, nie wnoszące nic w proces tworzenia oprogramowania mogą frustrować. Programista zamiast się skupiać na kreatywnej pracy programistycznej – pilnuje czy aby na pewno jego kod spełnia Święte Standardy, które nie rzadko spisane są na kilku czy nawet kilkunastu stronach i potrafią być naprawdę drobiazgowe. Spotkać można i takich biedaków, którzy w pocie czoła liczą spacje, których ilość była ściśle określona w obowiązującej konwencji. Zbyt drobiazgowe standardy nawet jeśli poprawiają przejrzystość i czytelność kodu – przynoszą więcej złego niż dobrego. Dlatego przy przygotowywaniu standardów należy je mocno ograniczyć tylko do rzeczy, które istotnie wpływają na poprawę jakości kodu. A wbrew pozorom zalety stosowania standardów nazewniczych nie ograniczają się tylko do poprawy przejrzystości i czytelności kodu. Mogą przynieść nam dużo więcej korzyści.

W nazwach zmiennych dzięki zastosowaniu prefixów i suffixów możemy zakodować ogromne ilości kluczowych dla programowania informacji. Informacje takie dostępne od ręki, bez przeszukiwania kodu – ułatwiają zrozumienie i analizę procedur i minimalizują popełnienie wielu prostych a mimo to częstych błędów. Natychmiastowa wiedza o głównych atrybutach zmiennej przyspiesza programowanie oraz ułatwia zrozumienie całego procesu.

Kluczowe atrybuty zmiennych, które są istotne w procesie programowania to:
1. zastosowanie/ funkcja w procedurze
2. jej zasięg
3. typ: czy  jest wejściowa, wyjściowa czy stała
4. typ danych

Konwencja nazewnicza powinna umożliwić proste zakodowanie tych informacji w nazwie zmiennej.

Zadanie zmiennej w procedurze
Prawidłowe nazywanie zmiennych to absolutna konieczność jeśli chcemy aby nasz kod był czytelny. Wydaje się to bardzo proste – jednak w praktyce okazuje się niezwykle trudne. Nazwanie zmiennych tak, by precyzyjnie odzwierciedlały zadanie tej zmiennej w procesie nie jest łatwe. Nie raz można natknąć się na kod upchany zmiennymi typu: x_data, data_tmp, y_data, cur_data. Nazwy tych zmiennych nie informują  o zadaniu zmiennej w procesie i utrudniają analizę procedury. Kod powinien być czytelny i zrozumiały. Na wyższych poziomach dekompozycji – powinno się go wręcz czytać jak książkę.


Begin
  zm1 := prc + prc_r * r;
end;

Powyższy kod komuś coś mówi? Spróbujmy przerobić:

begin
  new_employee_sal := employee_sal + (company_income* sal_rise_percnt);
end;

Prawidłowe, przemyślane nazwy zmiennych to pierwszy i najważniejszy element konieczny do podniesienia jakości naszego systemu.

Zasięg zmiennej
Możliwość określenia na pierwszy rzut oka jaki zasięg ma zmienna jest bardzo istotna, szczególnie w skomplikowanych procesach, gdzie używamy wielu zmiennych różnych typów: lokalnych, globalnych, parametrów

Pomyłki polegające na podstawieniu wartości czy użyciu zmiennej niewłaściwego typu są bardzo trudne do wykrycia! W jednym systemie szukaliśmy takiego błędu dobre kilka dni, zanim dopiero pod debuggerem został prześledzony kod linijka po linijce i wychwycony błąd. Okazało się, że w jednym miejscu do wyliczeń była brana zmienna globalna zamiast zmiennej lokalnej. Obie zmienne miały bardzo podobne nazwy i nic nie wskazywało na to, jaki zasięg miała zmienna. Użycie niewłaściwej zmiennej powodowało bardzo duży błąd w wyliczeniach. Możliwość rozróżniania zasięgu zmiennej całkowicie wyeliminuje tego typu pomyłki.

Zasięg zmiennych można rozróżnić stosując prefixy w nazwach zmiennych:
g<nazwa> - zmienne globalne np. gCurrentDate
p<nazwa> – parametry wejściowe np. pdCurrentDate

A zmienne lokalne? A zmienne lokalne najlepiej pozostawić bez prefixów. Zmiennych lokalnych używamy najczęściej więc po co dokładać sobie roboty i za każdym razem pisać dodatkową literkę? Jeśli zmienna nie ma przedrostka g lub p  - oznacza to, że mamy właśnie do czynienia ze zmienną lokalną. Zawsze to jedna literka więcej do wykorzystania w nazwie zmiennej, by precyzyjnie odzwierciedlała jej przeznaczenie.

Zmienna wchodzi czy wychodzi?
Parametry mogą być wejściowe lub wyjściowe, mogą być też stałymi. Ich zastosowanie oraz zachowanie znacznie się między sobą różni. Wiedza na temat typu takiej zmiennej znacznie potrafi ułatwić orientację w kodzie oraz sam development.

Zmienne typu CONSTANT tak jak i zmienne wejściowe – nie mogą być w procedurze zmieniane. Za to dla zmiennych wyjściowych trzeba w procedurze nadrzędnej przygotować zmienną do przechwycenia wartości parametru wyjściowego. Rozróżnienie typu zmiennej już po samej nazwie ułatwi pracę. Od razu będzie wiadomo jak z którą zmienna postępować.

Przy wywołaniu procedury z procedury nadrzędnej istotne jest by rozróżniać parametry wejściowe od wyjściowych. Oczywiście, można sprawdzić typ zmiennej w specyfikacji danej procedury. Ale czy nie szybciej  byłoby „dekodować” tę informację już z samej nazwy? Zmniejszy to frustrujące skakanie w kodzie od fragmentu kodu nad którym pracujemy do specyfikacji i z powrotem.

Ponieważ prefixy już zużyliśmy, może zastosować suffixy,:
p<nazwa>io – parametr IN/OUT
p<nazwa>o – parametr OUT
<NAZWA_DUŻYMY_LITERAMI> - zmienna constant lub c<nazwa>

Hm, i znów brakuje jednego typu parametru, parametru IN. Ale z drugiej strony jeśli po prefixie „p” widzimy, że zmienna jest parametrem, a nie ma suffixu ani io ani o – nie mamy innej możliwości, to musi być parametr  wejściowy IN, najpopularniejszy. Więc po co pisać za każdym razem suffix „i” jeśli suffix ten nic nie wnosi? Jak to mówią, DRY – Don't Repeat  Yourself :P

Więc zamiast:
Begin
  zm1 := prc + prc_r * r;
end;

możemy napisać tak:

begin
  new_employee_sal_o :=p_employee_sal + (company_income * g_SAL_RISE_PERCENT);
end;

Na pierwszy rzut oka widać, że zmienna  new_employee_sal_o jest zmienna wyjściową, zostanie wyliczona i zwrócona przez procedurę. Zmienna  p_employee_sal jest parametrem wejściowym. Company_income jest zmienna lokalną Zaś g_SAL_RISE_PERCENT jest zmienną stałą, globalną.

Podczas wywoływania procedury podrzędnej zastosowanie suffixów daje jeszcze większe korzyści.

Spójrzmy:

Begin
   calc_salary(Employee, NewSalary);
end;

Czy coś wiemy na podstawie kodu o tej procedurze? Niewiele prawda. Żeby coś więcej się dowiedzieć musimy albo obejrzeć kod procedury calc_salary albo / i sprawdzić typy zmiennych Employee oraz newSalary.

Ale jak dodamy prefixy i suffixy:

Begin
     calc_salary(pnEmployee=> nEmployee, pnNewSalary_o => nNewSalary);
end;

Widzimy, że oba parametry są numeryczne, parametr pnEmployee jest parametrem wejściowym, zaś parametr pnNewSalary_o → jest parametrem wyjściowym, więc nie możemy wstawić tu wartości czy zmiennej constant lub parametru wejściowego, tylko musimy zdefiniować zmienną numeryczną nNewSalary do przechwycenia wartości parametru z procedury.

Trzeba od razu podkreślić, że korzyści w tej sytuacji można odnieść jedynie wtedy, gdy wywołanie procedur będzie się odbywało według notacji nazewniczej a nie pozycyjnej (parametry do procedur będą przekazywane po nazwie parametru tak jak powyżej a nie według kolejności parametrów jak w przykładzie wcześniejszym. Co ma kilka innych zalet ale może o tym innym razem).

Zestawienie prefixów określających typ parametru/zmiennej

ZASIĘG ZMIENNEJ
PREFIX
PRZYKŁAD
GLOBAL
g<nazwa>
gdCurrentDate
LOCAL
Bez prefiksu, tylko prefix oznaczający typ zmiennej
vUserName
CONSTANT
<prefix zasięgu><prefix typu><NAZWA> (wielkie litery)
gnUSER_ID
PARAMETR IN
P<prefix typu><nazwa>
pnUserId
PARAMETR OUT
P<prefix typu><nazwa>_O
pvUserName_o
PARAMETR IN OUT
P<prefix typu><nazwa>_io
pvFeeAmt_io


Typ danych zmiennej
Określanie typu danych w nazwie zmiennej jest chyba najmniej rozpowszechnioną konwencją. Konwencja nie jest trudna do zastosowania – wystarczy w prefixie nazwy zmiennych dodać pierwszą literę typu danych np. v-varchar2, n-number. Informacja ta ułatwi nam programowanie i uchroni od nieprzyjemnych a łatwych do uniknięcia błędów.

Dzięki dodatkowego prefixowi w nazwie zmiennej, szczególni dotyczy to zmiennych lokalnych, które pozostawiliśmy jak dotychczas bez żadnych prefixów pozwoli nam odróżniać nazwy zmiennych od nazw kolumn w zapytaniach SQL. Po co nam to potrzebne? Rozważmy poniższy przykład:

emp_id varchar2(100);
begin
 select salary into nsal from employee
 where emp_id = emp_id;
end;

Oracle przy kompilacji procedury nie widzi nic podejrzanego w tym zapytaniu. Jeśli i my nie dopatrzymy się niczego niecodziennego – zapytanie to przemknie się w aplikacji. A konsekwencje mogą być znaczne. W warunku WHERE porównujemy tutaj kolumnę emp_id z tabeli z … kolumną emp_id z tabeli. Mimo, że mamy w kodzie zmienną o tej nazwie – Oracle nie podstawi tej zmiennej tylko przyrówna kolumnę do samej siebie. I zamiast rekordu dla pracownika – dostaniemy wszystkie rekordy w tabeli. Prawdopodobnie błąd ten nie zostanie wykryty na testach jednostkowych. Przecież do testów wystarczy jeden wiersz w tabeli employee…

No to spróbujmy poprawić:
begin
 select salary into sal from employee
 where emp_id = l_ emp_id;
end;

No, teraz powinno działać. Ale czy na pewno?
To zależy JAKIEGO TYPU DANYCH  jest zmienna l_emp_id! Jeśli jest tego samego typu jak emp_id – to mamy szczęście. Jeśli nieopatrznie zmienna l_emp_id jest innego typu niż kolumna emp_id w tabeli, a do tego na tej kolumnie jest index  - Oracle wykona niejawną konwersję danych  na  kolumnie, by dostosować ją do zmiennej parametru l_emp_id. Skutkiem tego zamiast szybkiego zapytanie po indexie – mamy full scan całej tabeli.

A żeby wykryć tego typu błąd potrzeba doświadczenia, łutu szczęścia albo pracochłonnego debugu. Gdy spróbujemy uruchomić explain plan dla zapytania, żeby sprawdzić jego koszt to albo użyjemy zmiennej zbindowanej:

 select salary into sal from employee
 where emp_id = :l_ emp_id;

Wtedy Oracle wyliczy koszt zapytania zakładając poprawny typ zmiennej, czyli nie taki jaki mamy w procesie! I powie nam, że zapytanie jest wspaniałe i szybkie i korzysta z indexu.

Możemy też podstawić właściwą wartość zamiast l_emp_id do zapytania. Ale skoro nie wiemy, że jest błąd w procedurze – podstawimy prawdopodobnie wartość prawidłową, numeryczną. I Oracle znów nam powie, że zapytanie jest super i będzie korzystać z indexu.

A w procesie zapytanie jak na złość idzie wolno….

Czy możemy coś na to poradzić?
begin
 select salary into sal from employee
 where emp_id = v_ emp_id;
end;

W tej sytuacji na pierwszy rzut oka widzimy, że zmienna v_emp_id jest typu varchar2 dzięki czemu dużo łatwiej jest zauważyć problem. Z reguły podstawowe indexowane pola są dobrze znane programistom i widok klucza głównego porównywanego ze zmienna typu v – varchar2– może wzbudzić uzasadnione wątpliwości. Dzięki informacji o typie danych możemy uniknąć niestety częstych pomyłek w typach danych i oszczędzić oraclowi niejawnych konwersji, które zwykle niegroźne, czasem mogą mieć znaczne wydajnościowe konsekwencje.


TYP ZMIENNEJ
PREFIX
PRZYKŁAD
VARCHAR
v<nazwa>
vUserName
NUMBER
n<nazwa>
nUserId
BOOLEAN
b<nazwa>
bIfUserActive
ROWTYPE
Rec<nazwa>  lub row<nazwa>
recUSerData
TABLE
t<nazwa> lub tab<nazwa>
tUsers
CURSOR
Cur<nazwa>
curUserTxn
INTEGER
I<nazwa>
iTxnCnt
DATE
D<nazwa>
dCurrentDate

Prefixy i suffixy łączymy w zależności od zasięgu, typu oraz typu danych zmiennej. Jeśli mamy parametr IN/OUT typu number to w nazwie powinniśmy zawrzeć wszystkie te informacje czyli np. pnEmpSal_io.

Czy zawsze i wszędzie stosować prefixy i suffixy?
Nie, od zasady jak zawsze są wyjątki. Wyjątkami są zmienne typowe, używane i rozumiane przez programistów niejako odruchowo czyli – wszelkiego rodzaju iteratory. Przyjęło się, że do iteratorów stosujemy zmienne o krótkich często jednoliterowych nazwach takich jak i, j, z, x, y. Każdy programista widząc coś takiego

for i  in 1..100 loop

wie dokładnie o co chodzi.

Zapis

For nEmployeeId in 1..100 loop

wprowadza już lekki niepokój, nieco mąci obraz a do tego jest długie a iteratory często stosujemy w tablicach czy kolekcjach i pisanie ciągle długiej nazwy iteratora może być frustrujące.

For nEmployeeId in 1..100 loop
      tabEmplyees(nEmployeeId).employee_id := nEmployeeId;
end loop;

To chyba lepiej pozostać przy starych dobrych i,j, x
For i in 1..100 loop
      tabEmplyees(i).employee_id := i;
end loop;

Od razu lepiej :)

CamelCase czy pod_kreślniki
Tak, temat został specjalnie pominięty, odłożony na koniec. Bo tak naprawdę czy CamelCase czy pod_kreślniki nie mają wielkiego znaczenia. Wielkość liter niewiele nam wniesie w czytelność kodu ani nie ułatwi programowania. Jeśli tylko nazwy zmiennych są dobrze dobrane zaś konwencja umożliwia szybkie rozróżnienie typów i zasięgów zmiennych i parametrów – to jest to co chcemy osiągnąć za pomocą standardów. Bicie piany czy lepszy CamelCase czy pod_kreśliniki mija się z celem. Jednym jest tak wygodniej, innym tak. Prefixy i suffixy, które niosą za sobą pożyteczne informacje – należy stosować, bo warto. Wszystkie inne standardy, które nic nie wnoszą można sobie odpuścić. Konwencje, które na kilku lub kilkunastu stronach opisują zastosowanie wielkich/małych liter, wymuszają liczenie spacji we wcięciach – nie dają nic oprócz frustracji. Zamiast myśleć kreatywnie programista zajmuje się pilnowaniem, czy jego kod nie złamał jakiejś reguły. Czy na pewno o to chodzi?


Przygotowanie standardów
Dobrze przemyślane standardy nazewnicze potrafią bardzo ułatwić pracę programistom oraz ustrzec kod przed łatwymi do popełnienia a trudnymi do naprawy błędami. Można zastosować zaproponowane standardy ale można też je zmodyfikować czy przygotować własne. Ważne jest, by spełniały podstawowe zadania, dzięki którym praca devloperska będzie łatwiejsza i przyjemniejsza

1. Nazwa zmiennej powinna określać jej zadanie/funkcję w procesie
2. Nazwa zmiennej powinna określać jej zasięg
3. Nazwa zmiennej powinna określać jej typ
4. Nazwa zmiennej powinna określa typ danych zmiennej

Przykładowe standardy:

ZASIĘG ZMIENNEJ
PREFIX
PRZYKŁAD
GLOBAL
g<nazwa>
gdCurrentDate
LOKAL
Bez prefiksu, tylko prefix oznaczający typ zmiennej
vUserName
CONSTANT
<prefix zasięgu><prefix typu><NAZWA> (wielkie litery)
gnUSER_ID
PARAMETR IN
P<prefix typu><nazwa>
pnUserId
PARAMETR OUT
P<prefix typu><nazwa>_O
pvUserName_o
PARAMETR IN OUT
P<prefix typu><nazwa>_io
pvFeeAmt_io

TYP ZMIENNEJ
PREFIX
PRZYKŁAD
VARCHAR
v<nazwa>
vUserName
NUMBER
n<nazwa>
nUserId
BOOLEAN
b<nazwa>
bIfUserActive
ROWTYPE
Rec<nazwa>  lub row<nazwa>
recUSerData
TABLE
t<nazwa> lub tab<nazwa>
tUsers
CURSOR
Cur<nazwa>
curUserTxn
INTEGER
I<nazwa>
iTxnCnt
DATE
D<nazwa>
dCurrentDate

Komentarze