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

30 listopada 2015, Monika Mitura

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

  • zastosowanie/ funkcja w procedurze
  • jej zasięg
  • typ: czy jest wejściowa, wyjściowa czy stała
  • 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ę.

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

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. Umiejętność łatwego 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 same 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 😛

Więc zamiast:

możemy napisać tak:

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:

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:

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
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 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 błędów 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

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

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 dlazapytania, żeby sprawdzić jego koszt to albo użyjemy zmiennej zbindowanej:

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ć.

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

Czy możemy coś na to poradzić?

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 pomyłek, 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 inforamcje 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 zmiennych takich jak i, j, z, x, y. Każdy programista widząc coś takiego

wie dokładnie o co chodzi.

Zapis:

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.

To chyba lepiej pozostać przy starych dobrych i,j, x

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ę pilnowanie, 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:

  • Nazwa zmiennej powinna określać jej zadanie/funkcję w procesie
  • Nazwa zmiennej powinna określać jej zasięg
  • Nazwa zmiennej powinna określać jej typ
  • 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

Wpis był oryginalnie opublikowany na moim blogu  how2ora.blogspot.com

Tagi: , , , ,

Zapraszamy do kontaktu!

Pretius jest firmą tworząca oprogramowanie wspierające biznes.
Tworzymy aplikacje webowe wykorzystując: Java, Oracle DB, Oracle Apex, AngularJS.
Skontaktuj się z nami, aby porozmawiać o tym jak możemy pomóc w realizacji Twojego projektu!