Programozás bevezető

Áttekintés

A programozás egy igen nagy terület. Talán helyesebb lenne olyan címet adni, hogy "bevezetés a programozás alapjaiba, első rész, első fejezet". Az Arduino projektek készítéséhez nélkülözhetetlen programozási alapismeretekről van itt szó, kicsit általános megközelítésben, némi kitekintéssel.

Programozási nyelvek

Igen sok programozási nyelv létezik. A legfontosabbakról érdemes tudomást szerezni, hogy lássuk, melyiknek mi a "létjogosultsága", és amit mi használunk, az hol helyezkedik el a nagy képben.

  • Assembly: az elektronikai bevezetőben láthattuk, hogy a hardver maga igen alap dolgokra képes: bit műveleteket végrehajtani, memóriából ki-be olvasni adatokat, bizonyos bit állapotától függően végrehajtani egy alap utasítást (pl. ugrást), és persze az adott processzortól függő egyéb utasításokat is, pl. megszakítások kezelése vagy bonyolultabb matematikai műveletek végrehajtása. Az assembly segítségével közvetlenül ezen a szinten tudunk programozni. (Létezik még ennél is még egy hardver közelibb réteg: a gépi kód, amikor közvetlenül az utasítások számkódját írjuk be.) Ne feledjük: akármilyen programot is futtatunk, legvégső soron ezek az utasítások hajtódnak végre. Assembly-ben tehát elméletileg bármit el tudunk készíteni, a gyakorlatban viszont ezt a legritkább esetben használjuk, mivel iszonyat lassú folyamat (értsd: hosszú, könnyű hibázni, nehéz felderíteni a hibát) az ebben történő programozás.
  • Basic: a történelmi hűség érdekében került második helyre a Basic programozási nyelv. A klasszikus változata ún. interpretált, ami azt jelenti, hogy egy futtató rendszer egyesével próbálja értelmezni az utasításokat, és végrehajtani azt, tehát "röptében" fordítja és futtatja. A kezdeti Basic rendszerek viszont igen nehézkesek voltak, melyek csak egyszerűbb feladatok leprogramozására voltak alkalmasak, létező programok kiterjesztésére mér kevésbé. Pl. nem voltak benne függvények, a kódot nem lehetett strukturálni, egy-egy sor beszúrásán túl nem lehetett kiterjeszteni, a GOTO ugróutasítás viszont megkerülhetetlen volt. Idővel ugyan javítottak bizonyos koncepcionális hibákat, mára viszont már igencsak megkopott régi népszerűsége. A mai Visual Basic visszavezethető a klasszikus Basic-re, viszont az sokkal inkább hasonlít egy modern, objektumorientált programozási nyelvre, mint a klasszikus Basic-re. (Az objektumorientált nyelvekről később lesz szó.)
  • Pascal: a nyelv megalkotói figyelembe vették a Basic hibáit, azokra igyekeztek megoldást találni. Ez már kezdettől fogva futtatható bájtkódra fordított (a bájtkódra fordítás azt jelenti, hogy adott processzoron, adott operációs rendszeren közvetlenül futtatható, mintha assembly-ben írtuk volna), van benne függvény, a programot több forráskódba szervezhetjük, a programozási struktúráknak köszönhetően áttekinthetőbbé vált a kód. Az eredeti Pascal mára kihalt, a ma is népszerű programozási nyelvek közül a Delphi-ben él tovább.
  • C: ennek a programozási nyelvnek az utasításkészlete jobban simul a hardverhez, mint mondjuk a Basic vagy a Pascal esetén. Ebben is vannak függvények, struktúrák (a Pascal-hoz képest valamelyest letisztultabb állapotban), mely lehetővé teszik komolyabb rendszerek C-ben történő fejlesztését. Igazán nagy rendszerek fejlesztésére gyakorlatilag alkalmatlan, a hardver közeli programozás szempontjából viszont ez az elsődleges programozási nyelv. Az Arduino kód ugyan C++, viszont ránézésre inkább natív C-nek tűnik. Így a C programozási nyelv alapjaival mindképpen meg kell ismerkednie annak, aki Arduino-val szeretne foglalkozni.
  • C++: ez a nyelv lényegében a C nyelvnek az objektumorientált kiterjesztése. Igyekeztek a nyelv megalkotói a C-vel felülről kompatibilis nyelvet megalkotni, elvben tehát minden C program egyben C++ is (a gyakorlatban vannak eltérések). Az objektumorientált programozásról nehéz pár szóban bármit is írni (ezen az oldalon van egy rövid összefoglaló). A probléma, melyre megoldást kínált, a következő: a C-ben nem lehet nagyobb logikai egységekbe szervezni a függvényeket, globális változókat. Ez nem probléma, amíg pár függvényből és pár globális változóból áll csak a programunk, viszont ha sok ezer függvényünk és sok ezer globális változónk van, melyek között szoros kapcsolat van, akkor az áttekinthetetlenné teszi az egészet.
  • Java: a gyakorlatban számos nehézség maradt a C++-ban. Egyrészt az igyekezet, hogy kompatibilis maradjon a C-vel azt eredményezte, hogy a fejlesztőtől nem kényszerítette ki a valóban jól átlátható, objektumorientált programok megalkotását. Pl. régi, kellemetlen örökségként megmaradt a goto utasítás, a programozók vegyesen készíthettek globális függvényeket és változókat, valamint objektumokat is. Valamint a C++ megmaradt platformfüggő programozási nyelvnek: az olyan, lényegében alap dolgok, mint pl. a többszálúság vagy a grafikus felület programozása, eltért pl. Windows és Linux alatt. Ezeket a hiányosságokat vették figyelembe a Java nyelv megalkotói. A Java koncepció a bájtkódra fordítás és az interpretálás sajátos egyvelege: olyan bájtkódra fordít, melyet közvetlenül nem lehet futtatni, azt interpretálni kell. Ezzel viszont lehetővé vált a többszálúság, a grafikus felület stb. szabványosítása, magyarán Windows és Linux alatt is ugyanazt a kódot kell elkészíteni, ha pl. egy ablakot szeretnénk megjeleníteni egy nyomógombbal, és az adott rendszer futtatója gondoskodik arról, hogy az ténylegesen meg is jelenjen. Magát a nyelvet is javították: kivették a globális függvények és változók létrehozásának a lehetőségét, megszüntették a goto-t, és azzal, hogy a fájlnévnek meg kell egyeznie a benne definiált osztálynévvel, az osztályokat kötelező csomagokba szervezni, mely a könyvtár szerkezettel kell, hogy megfeleljen, kikényszerít bizonyos áttekinthetőséget. Mindez oda vezetett, hogy a Java ma a legnépszerűbb programozási nyelv. Az Arduino esetén viszont a Java szóba sem kerül, mivel túl gyenge a hardver egy esetleges interpretációhoz.
  • C# (C sharp, ejtsd: szí sarp, mely a zenei keresztre utal): ahogy a nevéből is látszik, ez a C++ továbbgondolása. A Microsoft alkotta meg a .NET (ejtsd: dotnet) keresztrendszer programozásához. Nem sokkal a Java megjelenése után jelent meg ez is, így nem véletlen a hasonlóság a kettő között. A Java-hoz hasonlóan ehhez is futtató rendszerre van szükség. A szükséges keretrendszer a modern Windows operációs rendszereken alapból rajta van, és ma már stabilan fut más operációs rendszereken, pl. Linuxon is. Népszerűsége hullámzó, a hosszú távú tendencia javuló.
  • Scala: Java alapokon nyugvó programozási nyelv. Java bájtkódra fordít, így a futtatáshoz Java virtuális gépre van szükség. Jelentős hozzáadott értéke a funkcionális kiterjesztés: egy változónak vagy egy paraméternek a típusa is lehet függvény. A Scala a Java-ra is visszahatott: a 8-as verzióban bevezették a Java nyelv funkcionális kiterjesztéseit.
  • JavaScript: ez a nyelv böngészőben fut. Ahogy a neve is mondja: ez egy script nyelv, ami azt jelenti, hogy a futtató rendszer egyesével értelmezi és "röptében" végrehajtja az utasításokat (ilyen értelemben hasonlít kicsit a kezdeti Basic-re). A JavaScript-nek nincs sok köze a Java-hoz. Ez is egy objektumorientált, ráadásul funkcionális programozási nyelv. Újabb reneszánszát éli, számos kiterjesztése jelent meg és lett népszerű, és megjelent Node.js néven a szerver oldali (tehát nem böngészőben futó) JavaScript is.
  • PHP: ez a HTML szerver oldali kiterjesztése. Ne feledjük: a böngészés során HTML oldalakat töltünk le, melyet a böngésző megjelenít. Megjelenhetnek kiterjesztések is, mint pl. a JavaScript. A szervernek viszont legvégső soron HTML formázott szöveget kell visszaadnia, ugyanakkor a HTML nem programozási nyelv. Több megoldás is született ennek a problémának a kezelésére, ezek közül az egyik, talán ma legnépszerűbb a PHP: a HTML kódba PHP kódot tudunk beágyazni, mely tartalmazza mindazt, amit egy programozási nyelvtől elvárunk, végrehajthat olyan műveletet, mint pl. egy adatbázis lekérdezés, és végeredményben HTML-t generál.
  • PERL: ez egy script nyelv, tehát interpretálódik. Ellentétben a JavaScript-tel, mely alapvetően a böngészőben fut, ez annál általánosabb. Akkor érdemes használnunk, ha a programunk néhány függvényből áll, egyszerű, tipikusan szövegfeldolgozó feladatot hajt végre (az alap művelet készlet kifejezetten szövegfeldolgozásra van kihegyezve), és nem tartalmaz grafikus felületet. Nagy rendszerek elkészítésére ugyanakkor a gyakorlatban nem alkalmas.
  • Python: ez is egy script nyelv, viszont általánosabb célú, mint a PERL. Ennek van szabványos grafikus felülete, így leginkább olyan egyszerűbb alkalmazások elkészítésére alkalmas, melyben a grafikus felület követelmény. Ez is funkcionális nyelv. Alkalmas nagy mennyiségű adatok feldolgozására. További előnye még az is, hogy könnyen tanulható, így gyerekek számára a blokk programozás után az első szöveg alapú javasolt programozási nyelv a Python. A micro:bit-et pl. Python-ban is lehet programozni.
  • R: kifejezetten adatok feldolgozására és statisztikai riportok létrehozásra kialakított programozási nyelv, mely rendelkezik mindazzal, ami igazi programozási nyelvvé teszi, sőt, ez is funkcionális.
  • SQL: ennek segítségével adatbázis műveleteket tudunk végrehajtani: táblákat létrehozni, módosítani, adatokat beszúrni, kiolvasni. Az SQL önmagában nem programozási nyelv, viszont bizonyos léteznek gyártófüggő kiterjesztések, melyek már programozási nyelvnek mondhatóak. A legismertebbek az Oracle által kifejlesztett PL/SQL, valamint a Microsoft terméke, a T-SQL.
  • Scratch: gyerekek számára kialakított blokk programozási nyelv, melynek célja az algoritmikus gondolkodás elültetése a nagyjából felsős korosztályú diákok fejében. Egyszerűsége ellenére számos komoly programozási elem megjelenik benne: a különböző vezérlő szerkezetek (feltételkezelés, véges és végtelen ciklus), változók, listák, grafikus elemek, többszálú programozás, üzenetküldés stb.

Fejlesztés, fordítás, futtatás

Tetszőleges programozási nyelvet választva is valami módon értelmezettek a címben felsorolt lépések. A programot valami módon el kell készíteni. Ez leggyakrabban szöveges fájl írását jelenti, ritkábban grafikus felületen történik a kód elkészítése. Utána - ha csak nem közvetlenül a hardvert alakítottuk ki - az elkészített kódot futtathatóvá kell tenni az adott eszköz számára, végül el kell indítani ahhoz, hogy lefusson.

Az Arduino esetén a legelterjedtebb (bár em kizárólagos!) fejlesztői környezet az Arduino IDE, ahol elkészítjük a programforrást, az elkészíti az Arduino által futtatható binárist, feltölti, és ezt követően a program azonnal fut az Arduino-n. A Scratch esetén a fordítással nem kell külön bajlódni, azt a rendszer a háttérben elvégzi. A legtöbb esetben viszont a fenti három rendszer élesen elkülönül, és különösen a fordítás lehet igen bonyolult folyamat. (A C++ esetén pl. több részből áll: előfeldolgozás, fordítás, szerkesztés.)

Általános programozási elemek

Ide azokat az elemeket sorolom fel, melyek minden programozási nyelvben megtalálhatóan valamilyen formában, mégpedig nyelvi alapelemként. Ezek tehát az Arduino programozásban is megjelennek, és ezeket nagyon jól meg kell tanulni.

Utasítások

A programkód egymás után írt utasításokból áll. Általában egy utasítást írunk egy sorban. A legtöbb programozási nyelvben (így az Arduino C++-ban is) az utasításokat pontosvesszővel (;) zárjuk le. Mindegyik programozási nyelvben jelölni lehet valahogyan azt, hogy mely utasítások tartoznak egy logikai egységbe (pl. ha bizonyos utasítások csak bizonyos feltétel teljesülésekor futnak le, az azt követőeknek viszont le kell futniuk függetlenül a feltételtől). A legtöbb modern programozási nyelvben (így az Arduino esetében is) ez a kapcsos zárójel ({…}). Ettől eltérések is vannak, pl. a Python esetén az azonos mélységig behúzott utasítások tartoznak össze, a PL/SQL-ben pedig a THEN és az END közötti rész képez egy egységet.

Változók

Minden rendszerben van mechanizmus, mellyel adatokat menthetünk, és az esetek döntő többségében ezt változók használatával tesszük (kivétel: assembly, ahol regiszterekbe ill. memóriahelyekre kerülnek az adatok). Az Arduino esetén is változókat használunk. Az ún. típusos nyelvekben (mint amilyen az Arduino C++ is, de pl. a Scratch nem ilyen) a változóknak egyértelmű típusuk van.

Számok

A legkézenfekvőbb adattípus a szám. Léteznek egész és nem egész (lebegőpontos) számok tárolására alkalmas adattípusok. Az egész számtípus esetén alapvetően két dologra kell ügyelnünk: egyrészt a tároláshoz szükséges memóriaterület mérete bitben kifejezve, másrészt arra, hogy a szám előjeles-e vagy sem. Bár kézenfekvő lenne, olyan általános szabály sajnos nincs, mely a legtöbb programozási nyelvre igaz lenne.

  • Az alapértelmezés szinte mindig az előjeles, ami azt jelenti, hogy a legnagyobb helyi értéket jelző bit jelzi, hogy a számot pozitívként (0) vagy negatívként (1) kell-e értelmezni.
  • Szinte mindig van int (vagy hasonló nevű, pl. INT, Integer) adattípus, a méretére viszont nincs általános érvényű szabály. A C-ben és C++-ban a cél processzortól függően 2 vagy 4 bájt hosszú; az Arduino UNO esetén 2 bájt hosszú, ami előjeles esetben -32768 és 32767, előjel nélküli esetben 0 és 65535 közötti értékek tárolására alkalmas. A nagyobb kapacitású Arduino MEGA vagy DUE esetében ez 4 bájt hosszú. Ha biztosra szeretnénk menni, használhatjuk a short és a long adattípust, mely mindig 2 ill. 4 bájtos. A Java esetén az int mindig 4 bájtos.
  • Ugyancsak jellemzően van byte adattípus, mely egyetlen bájt tárolására alkalmas (-128…127 ill. 0…255).
  • A már említett long szintén gyakori adattípus, de amíg C++-ban 4 bájtos adatok tárolására alkalmas, addig Java-ban 8 bájtos adatokéra.
  • Nem egész számokat lebegőpontos (exponenciális) alakban tudunk tárolni. Itt viszont fontos tudnunk, hogy bizonyos processzorok tudják közvetlenül is kezelni a lebegőpontos aritmetikát, mások nem. A legtöbb modern készülékben levő processzor a teljesebb (Complex Instruction Set Computing, CISC) utasításkészletet támogatja, és azokban jellemzően van hardver szintű lebegőpontos aritmetikai támogatás is, míg mások (ilyen az Arduino UNO-ban levő processzor is) a csökkentett változatot (Reduced Instruction Set Computing, RISC, ill. a gyártóról elnevezve szokás a német szójátékos ARM (szegény) processzorként is hivatkozni rá). Ez utóbbi kategóriába tartozik az Arduino UNO is. Ezt amiatt érdemes tisztában lennünk, mert ugyan tudunk lebegőpontos számtípust használni, viszont ez szimulált nincs hozzá hardver szintű támogatás.
  • Leggyakrabban kétféle lebegőpontos adattípus létezik: a float, mely többnyire 4, és a double, mely leggyakrabban 8 bájton tárolja az adatot. Az Arduino esetén is ez így van.
  • Többnyire létezik logikai adatok tárolására szánt adattípus, mely bool vagy boolean névre hallgat; az Arduino esetén mindkettőt használhatjuk. (Ott a boolean a bool "fedőneve").
  • A különböző típusú adatok egymásba konvertálására nincs általános szabály. A kisebbről a nagyobbra többnyire gond nélkül megy az automatikus konverzió, esetleg figyelmeztet a fordító. Fordítva már problémásabb, ott hibát is jelezhet. Célszerű a konverziót jeleznünk a fordítónak, tipikusan zárójelbe téve az adattípust az érték elé, melyre konvertálni szeretnénk. A C-ben egyébként bármely egész adattípus használható logikaiként; a 0 jelenti a hamisat, minden más az igazat.
  • A változók típusát legtöbbször (Arduino-ban is) azok deklarálása előtt adjuk meg, és vesszővel elválasztva többet is megadhatunk. Kezdeti értéket is adhatunk neki rögtön.
  • Változók számos helyen előfordulhatnak. Lehetnek globális változók, bár ez nem szerencsés programozási gyakorlat, és bizonyos programozási nyelvek (pl. Java) tiltják (az Arduino-ban megengedett, sőt, az ottani architektúrában lényegében kikerülhetetlen). Elhelyezkedhetnek bárhol osztályokban (ezeket attribútumoknak hívjuk), függvényekben, a függvények paraméterlistájában, sőt, egy for ciklus elején is.
  • Értékadásnál leggyakrabban decimális alakban írjuk a számokat, de megadhatjuk hexadecimálisban (általában 0x előtaggal) és bináris formában is (B előtaggal).

Az alábbi példa bemutat néhány lehetőséget, mely az Arduino esetén szabályos kód:

long c;
int a = 3, b = 0xAB;
c = a * b;
float pi = 3.14;
float f = (float) a / (float) b;

A számokkal műveleteket hajthatunk végre. A lehetőségek pontos listája rendszerről rendszerre változik ugyan, viszont igen nagy a közös metszet, és a jelölésben is igen nagy a hasonlóság. Az Arduino-ban végrehajtható műveletek az alábbiak:

  • Aritmetikai műveletek:
    • Összeadás: +, pl. a = b + c, i++ (az i értékének növelés eggyel), ++i (ugyanaz mint az előző, de előre hajtja végre, nem utólag), d += e (d növelése e-vel)
    • Kivonás: -, pl. a = b - c. A fentiek itt is működnek.
    • Szorzás: *, pl. a = b * c.
    • Osztás: /, pl. f = g / h.
    • Osztás maradéka, egész számos esetén (más néven modulo): %, pl. m = a % b.
  • Összehasonlító műveletek (eredménye logikai):
    • Nagyobb: >, pl. a > b
    • Kisebb: <
    • Egyenlő: ==
    • Nem egyenlő: !=
    • Nagyobb vagy egyenlő: >=
    • Kisebb vagy egyenlő: <=
  • Logikai műveletek:
    • Logikai és: &&, pl. (a < b) && (b < c)
    • Logikai vagy: ||
    • Logikai tagadás: !
  • Bit műveletek (itt bitenként végrehajtódik, pl. ha a két operandus int, akkor 16-szor):
    • Bitenkénti és: &
    • Bitenkénti vagy: |
    • Bitenkénti tagadás: ~
    • Bitenkénti kizáró vagy: ^
    • Bitek csúsztatása balra: «, pl. a = b « 2, akkor ez lényegében 4-gyel való szorzás.
    • Bitek csúsztatása jobbra: »
  • A ?: operátor: pl. az a = (b < 3) ? 2 : 5 azt jelenti, hogy ha a b értéke 3-nál kisebb, akkor az a érte 2 lesz, egyébként 5.

Betűk

Az Arduino egy bájton tárolja a karaktert, és szemmel láthatólag nem igazán törődik az ékezetekkel, így képzeletben vissza kell mennünk néhány évtizedet, úgy az 1970-es évekbe, és meg kell békélnünk azzal, hogy nem, vagy csak nagy kínlódás árán használunk ékezetes karaktereket.

A karakter típus jelölésére a legtöbb esetben (az Arduino-n is) ezt használjuk: char. Néhány, az Arduino-n működő példa:

char c1 = 'A';
char c2 = 65;
char t = 'a' + 9;
char *text = "hello";

Az első és a második sor valójában ekvivalens: az első esetben magát az A karaktert adtuk meg, a másodikban pedig annak ASCII kódját. A kód olvashatóság érdekében ha egy bájtot karakterként szeretnénk értelmezni, akkor érdemes magát a karaktert megadni, ha pedig számként, akkor eleve a byte típust használni, és számként megadni, a fordító viszont mindkettőt engedi. Egyébként mivel számról van szó, műveleteket is végrehajthatunk rajta, amit a harmadik utasítás illusztrál: ez az ábécé 10. karakterét fogja jelenteni, ami a 'j'. Az utolsó sor már előre vetíti a következő témát: szöveget karakterek egymás után írásából tudunk létrehozni. A C-ben a szöveget a 0 karakter zárja le (így a 0 is speciális karakter). Mivel az Arduino C++, ami felülről kompatibilis a C-vel, ezért ez működik, viszont érdemes inkább a String típust használni, ld. lejjebb.

Tömbök

A tömb alapvető fontosságú adatszerkezet, nem véletlen, hogy (lista néven) már a Scratch-ben is szerepel. A betűknél bemutatott utolsó példa, a "hello" szintén egy tömb: a h, e, l, l és o karakterek ASCII kódjainak egymásutánja, melyet egy 0 kód zár le, tehát 6 elemű, elemenként egy bájt méretű tömbről beszélhetünk. (A * a C-ben mutatót, angolul pointert jelent: arra a memóriaterületre mutat, ahol a szöveg elkezdődik. Ha pl. 2 bájttal odébb vinnénk, akkor a tartalma ez lenne: 'lo'.)

A tömbnek van elemszáma és típusa. A programozási nyelvek többségében az elemszámot szögletes zárójelbe tesszük, de általában a nyelv lehetőséget ad az elemek felsorolására is; ez utóbbi esetben üres szögletes zárójellel jelezzük, hogy tömbről van szó, és kapcsos zárójelben soroljuk fel az elemeket. Az Arduino is ilyen, és alább látható két példa a

int a1[256];
byte a2[] = {3, 5, 7, 6, 9};
a1[255] = a2[2] + 3;

Feltételkezelés

Ha úgy fogalmazunk, hogy a programozásban a feltételkezelés alapvető fontosságú, akkor azzal nem fejezzük ki eléggé a lényegét. Ennél meredekebb kijelentést kell tennünk: a feltételkezelés és a számítógép egylényegű.

  • Egyrészt a számítógép alapja a feltételkezelés. A számítógép alap építőkövei a tranzisztorok, és egy tranzisztor önmagában egy feltétel: ha a bázist feszültség alá helyezzük, akkor áram folyik a kollektorból az emitterbe. Feltétel nélkül nincs számítógép, mert az nem számítógép, hanem egy determinisztikus gép.
  • Másrészt egy számítógépen belül minden visszavezethető a bitek ide-oda mozgatása mellett a feltételkezelésre. Hardver szinten egyetlen olyan utasítás van, mely megszünteti a programok determinisztikus voltát: egy bit (pl. egy túlcsordulás bit) értékétől függően ugorjon a programban valahova, vagy folytassa, ahol tartott. Az alább felsorolt minden struktúra: feltételkezelések, ciklusok, de ennél összetettebb program elemek is mind-mind erre az egyetlen feltételes elágazásra vezetődnek vissza.

Lássuk tehát, milyen lehetőségeink vannak a feltételkezelésre! A lent felsoroltak a szabvány C nyelvből kerültek a C++-on keresztül az Arduino-ba, de szinte az összes programozási nyelvben léteznek ezek a struktúrák, döntő többségükben pont ezzel a szintaxissal.

if…else

A legalapvetőbb feltétel kezelés az if…else struktúra, melynek Arduino-s specifikációja itt található: https://www.arduino.cc/reference/en/language/structure/control-structure/if/.

Az if-et követi a feltétel, majd azok a műveletek, melyek a feltétel teljesülésekor futnak le, és opcionálisan következi egy else ág, ahol azok az utasítások szerepelnek, melyek a feltétel nem teljesülése esetén futnak le. Az else folytatódhat egy újabb if-fel. Bizonyos nyelvekben az … else if … struktúrának saját szintaxisa van, pl. elif; az Arduino nem ilyen.

Leggyakoribb szintaxis:

if (feltétel1) {
  utasítások1;
} else if (feltétel2) {
  utasítások2;
} else {
  utasítások3;
}

Lássunk egy példát!

if (pontszam >= 60) {
  atment = true;
} else {
  atment = false;
}

switch…case

Ha egy összetettebb esetben túl sok lenne az if … else if … else if … else if …, az nehezen áttekinthetővé tenné a kódot. Ha minden ágon ugyanannak a változónak a különböző értékeit vizsgáljuk, akkor áttekinthetőbbé teszik a kódot a switch…case struktúra. Ez a struktúra is szinte mindegyik programozási nyelvben szerepel, nagyrészt a lenti szintaxissal. Az Arduino specifikációja itt található: https://www.arduino.cc/reference/en/language/structure/control-structure/switchcase/. A működése a következő: attól a ponttól (case) fog lefutni a kód, melyre igaz lesz a feltétel. Fontos, hogy ha nem szeretnénk, hogy a következő feltétel utasításai is lefussanak, break akkor utasítással lezárjuk az adott esetet. A default ágnak a végére kell kerülnie, és akkor fut le, ha semelyik másik ág nem futott le.

Leggyakoribb szintaxis

switch (változó) {
case eset1:
  utasítások1;
  break;
case eset2:
  utasítások2;
  break;
default:
  utasítások;
}

Egy példa, melyben az a és b változók egészek:

switch (muvelet) {
case 1:
  a = b + c;
  break;
case 2:
  a = b - c;
  break;
case 3:
  a = b * c;
  break;
default:
  a = 0;
}

Ciklusok

Amíg az ember nem szereti az ismétlődő, monoton feladatokat, addig a számítógép ebben a legjobb: ha valamit egyszer el tud végezni, akkor százszor is el tudja végezni. Az idők folyamán kialakult néhány ciklus forma, de ne feledjük, hogy ezek mindegyike visszavezethető az egy szem feltételkezelésre és ugró utasítások.

for

A for ciklus már kezdetektől jelen van a magasabb szintű programozási nyelvekben, és a szintaxisa a legtöbb esetben ugyanaz. Segítségével ugyanazt a műveletet adott számú alkalommal végre tudjuk hajtani. Az Arduino specifikációja itt található: https://www.arduino.cc/reference/en/language/structure/control-structure/for/.

Leggyakoribb szintaxis:

for (inicializálás; feltétel; növelés) {
  utasítások;
}

A következő kódrészlet a faktoriális kiszámolását illusztrálja for ciklus segítségével:

int n = 5;
int faktorialis = 1;
for (int i = 1; i <= n; i++) {
  faktorialis *= i;
}

Értelmezés: a for után zárójelben 3 dolog szerepel: a ciklus változó inicializálása, mely egyszer fut le, utána a feltétel, amitől a ciklusmag lefutása függ, végül a ciklus változó módosítása, mely a ciklusmag lefutása után fut le. (Ez azt is jelenti, hogy ha a feltétel már alapból nem teljesül, akkor egyszer sem fut le a ciklusmag.) Bármelyik elhagyható, és ha mindhármat elhagyjuk (for (;;;) {…} ), akkor végtelen ciklust kapunk.

A for ciklusnak létezik egy másik változata is, mely egy adatstruktúra (pl. tömb) elemein lépked végig. Ez a szintaxis az Arduino-ban nem található; máshol többnyire így néz ki:

for (int elem : lista) {
   muvelet(elem);
}

while és do…while

A while ún. elöl tesztelő ciklus: megvizsgálja a while után zárójelben levő feltételt, és ha igaz, lefuttatja a ciklusmagot, és mindezt ismétli mindaddig, amíg a feltétel igaz. Arduino specifikáció: https://www.arduino.cc/reference/en/language/structure/control-structure/while/. Szintaxis:

while (feltétel) {
  utasítások;
}

A faktoriális példa while ciklussal:

int i = 1;
while (i <= n) {
  faktorialis *= i;
  i++;
}

A do … while ún. hátul tesztelő ciklus. A while ciklussal ellentétben itt a ciklusmag legalább egyszer lefut. Ezt ritkábban használjuk, mint az elöl tesztelőt. Arduino specifikáció: https://www.arduino.cc/reference/en/language/structure/control-structure/dowhile/. Szintaxis:

do {
  utasítások;
} while (feltétel);

A faktoriális példa hátul tesztelős ciklussal megvalósítva:

int i = 1;
do {
  faktorialis *= i;
  i++;
} while (i <= n);

Ugró utasítások

Az ugró utasítások olyan szempontból alapvetőek a számítógépeken, hogy a (tágabb értelemben vett) bit műveleteken kívül a processzor két dolgot tud: feltételesen és feltétel nélkül ugrani, azaz máshol folytatni a program végrehajtását. A magasabb szintű programozási nyelvekben megírt kód ezekre az utasításokra fordítódik. Ugró utasításokat a magasabb programozási nyelvekben is használhatunk, ezekkel viszont érdemes csínján bánni, mivel csökkenti a kód áttekinthetőségét.

break és continue

A break és a continue utasításokat hívhatjuk strukturált ugró utasításoknak. A legtöbb programozási nyelvben szerepelnek, az Arduino-ban is használhatjuk. A break (Arduino specifikációja: https://www.arduino.cc/reference/en/language/structure/control-structure/break/) kilép az aktuális ciklusból (tehát a feltételtől függetlenül megszakítja a ciklust), a continue (Arduino specifikációja: https://www.arduino.cc/reference/en/language/structure/control-structure/continue/) viszont a ciklusmag elejére, a feltétel vizsgálathoz ugrik (for ciklus esetén a ciklusváltozó értékének módosítása is megtörténik).

A faktoriális megvalósítása úgy, hogy van benne break:

int i = 1, faktorialis = 1, n = 5;  
while (true) {
  faktorialis *= i;
  i++;
  if (i > n) {
    break;
  }
}

(A continue szintaxisa hasonló.)

goto

A goto utasítás lett a programozás ősbűne. Hardver közeli szinten túl sok más választás nincs, magasabb szintű programozási nyelveken viszont érdemes elkerülni, használatával ugyanis a kód áttekinthetetlenné válik. Bizonyítottan bármely algoritmus megvalósítható goto nélkül. Ahol esetleg megfontolandó a használata: többszörös ciklusból való kiugrás. A C-ben még volt goto, amit megörökölt a C++, a Java-ból viszont kivették. Az Arduino referencia ez: https://www.arduino.cc/reference/en/language/structure/control-structure/goto/. Szintaxis:

cimke:
  ...
  goto cimke;

(A címke bárhol lehet.)

A faktoriális példa megvalósítása goto-val:

  int i = 1, faktorialis = 1, n = 5;  
vissza:
  faktorialis *= i;
  i++;
  if (i <= n) {
    goto vissza;
  }

Függvények

A programozásban függvények nélkül lehet ugyan élni, de nem érdemes. Nehéz megválaszolni a kérdést, hogy mi a függvény úgy, hogy ne legyen neki túlzottan tankönyv íze, de a lényeg átmenjen. Induljunk ki a matematikai értelemben vett függvényekből: ott a kimenet valamilyen bemeneti értéktől vagy értékektől függ. Pl. beszélhetünk hőmérsékletről az idő függvényében (egy paraméter), vagy hőérzetről a hőmérséklet és a páratartalom függvényében (két paraméter). A programozásban is valami hasonlóból indult ki a függvény: tartalmazhat elvben akármennyi (akár 0) paramétert, és visszatérhet valamilyen értékkel (ami lehet a semmi is). A visszatérés nélküli függvényeket bizonyos programozási nyelvekben eljárásoknak hívják, ami tkp. jogos, mert az nem matematikai értelembe vett függvény, és ezt a szintaxisban is nyomatékosítják, pl. a függvényekhez oda kell írni, hogy function, az eljárásokhoz meg hogy procedure. A legtöbb esetben (az Arduino esetében is) viszont nem kell megneveznünk, elég csak megadni a visszatérési érték típusát (ami lehet void, ha nincs visszatérés), a függvény nevét, a paraméter listát, és természetesen a függvény törzsét, mely (elvileg) akármennyi utasításból állhat.

Mire jó a függvény?

  • Ha van olyan feladat, melyet a programban több helyen el kell végezni, akkor ahelyett, hogy mindenhova lemásolnánk a szükséges programsorokat, érdemesebb függvénybe kiszervezni azt. Néhány példa: a használati utasítás kiírása, beolvasás adatbázisból, szöveg ugyanolyan átalakítása.
  • Ha egy jól definiált feladatot csak egyetlen helyen hajtunk végre, akkor is érdemes függvénybe kiszervezni, a kód olvashatósága érdekében. Pl. létrehozhatunk egy faktoriális függvényt, melynek bemenő paramétere az, aminek a faktoriálisát szeretnénk kiszámolni. A hívó oldalon ennyit fogunk látni: eredmeny = faktorialis(5);, a hívott oldalon pedig a faktoriálist kiszámító pár sort; mindkettő jól átlátható, sőt, a függvény, ha nincs mellékhatása, akkor jól tesztelhető is.

Az assembly-ben nem létezik függvény, és a kezdeti Basic nyelvekben sem létezett, de a mai modern programozási nyelvek szinte mindegyikében jelen van. Az Arduino specifikáció itt található: https://www.arduino.cc/en/Reference/FunctionDeclaration, a return utasításé itt: https://www.arduino.cc/reference/en/language/structure/control-structure/return/. A programozási nyelvek többségében (az Arduino-ban is) a függvény szintaxisa az alábbi:

típus függvénynév(típus1 paraméter1, típus2 paraméter2, ...) {
  ...
  return visszatérésiérték;
}
...
eredmény = függvénynév(p1, p2, ...);

Példaként vegyük a már sokszor említett faktoriálist. A legtöbb programozási nyelven az alábbi helyes szintaxis.

int faktorialis(int n) {
  int eredmeny = 1;
  for (int i = 2; i <= n; i++) {
    eredmeny *= i;
  }
  return eredmeny;
}

Léteznek ún. rekurzív függvények, melyek magukat hívják meg. Ebben az esetben vigyázni kell, nehogy végtelenített rekurzió legyen. A faktoriális függvény megvalósítása rekurzióval:

int faktorialis(int n) {
  if (n > 2) {
    return n * faktorialis(n-1);
  } else {
    return 1;
  }
}

(A valóságban részletesebben ki kellene dolgozni, pl. hibát jelezni, ha a paraméter negatív.)

Többnyire a program belépési pontja is egy függvény. Erre nincs általános szabály; szinte ahány nyelv, sőt: ahány rendszer, annyi belépési pont. Pl. a C nyelv esetén ez az int main(). Egy alkalmazás szerveren futó Java program esetén adott rendszertől függ. Az Arduino e szempontból egyedi, ott két függvény létezik alapból: a void setup() és a void loop(); az előbbi a tulajdonképpeni belépési pont, mely egyszer fut le induláskor, az utóbbi pedig folyamatosan ismétlődik.

Interfészek

A programok bonyolulttá válásával megjelent az igény arra, hogy szétválasszuk a külvilág felé nyújtott szolgáltatást annak megvalósításától. Ez egyszerűsíti a használatot. A való életből vett hasonlattal ez olyan, hogy pl. egy boltban a vásárló előtt rejtve marad mindaz a logisztika, mely szükséges ahhoz, hogy az áru a polcra kerüljön, valamint a pénzügyi elszámolás is, és a vásárló csak annyit lát az egészből, mely feltétlenül szükséges ahhoz, hogy meg tudja venni az árut.

A programozásban ezt a párt többféleképpen hívjuk. Amit nyújt, annak a neve leggyakrabban interfész, absztrakció, deklaráció, fejléc. Ahogy nyújtja, annak a tipikus megnevezései: megvalósítás, implementáció, definíció (ez utóbbi leginkább a deklaráció párja).

Az egyszerűbb ill. kezdeti programozási nyelvekből ez a különválasztás hiányzott, pl. az assembly-ben nem értelmezett ez a fogalom (bár szervezhető úgy a kód), kezdetben a Basic-ből is hiányzott, ill. ma az olyan egyszerű rendszerekből, mint a Scratch, a legtöbb esetben viszont valami módon jelen van. Az Arduino-ban a C/C++ világból örökölt fejléc megvalósítással találkozunk: a függvények egy .h kiterjesztésű fájlba kerülnek, míg a megvalósítás .cpp kiterjesztésű fájlba. Ennek köszönhetően tudunk olyanokat írni a kódba, hogy pl. Serial.println("teszt");, és végrehajtás mikéntje el van előlünk rejtve, ami valójában egyszerűsíti a dolgunkat. Kicsit részletesebben, a következőképpen néz ez ki.

Deklaráció (osszead.h):

#ifndef osszead_h
#define osszead_h
 
int osszead(int a, int b);
 
#endif

A kettős kereszttel (#) kezdődő sorok a C-ben az ún. preprocesszor direktívák, melyeknek a kód fordítása előtt van szerepük. Jelen esetben megakadályozzák, hogy többször is beolvasásra kerüljön a fejléc. Az interfészen definiáltunk egy függvényt, mely azt ígéri, hogy összead két számot és visszatér az eredménnyel, a részleteket viszont elrejti a használó elől.

Definíció (osszead.cpp):

#include "osszead.h"
 
int osszead(int a, int b) {
  return a + b;
}

Itt betöltöttük a fejléc állományt, és megadtuk az osszead függvény törzsét, ami jelen esetben visszatér az összeggel. (A valóságban ennél sokkal összetettebb dolgokat képzeljünk el.)

Használat:

#include <osszead.h>
 
...
int osszeg = osszead(2, 3);
...

A használat során a megvalósítással nem kell törődnünk, csak a fejléccel. Az eredmény olvasható lett. Képzeljünk el egy valósabb helyzetet, pl. a fejléc tartalmazzon mondjuk 5 függvényt, és mindegyiknek a megvalósítása legyen 20 sor. Ha nem lenne elkülönítve az absztrakció az implementációtól, akkor a felhasználónak 100 sorból kellene kibogarásznia a használandó függvényeket, így viszont csak 5-ből.

Az ún. objektumorientált nyelvekben az absztrakció kulcsszava tipikusan az interface.

Megjegyzések

A programozás során a legritkább esetben fordul elő az, hogy megírunk egy kódot, és soha többé rá sem kell néznünk. Emiatt törődnünk kell azzal is, hogy a kód olvasható legyen az ember számára is, ne csak a számítógép számára. Az értelmezést egy-egy jól irányzott megjegyzés a kódban jelentősen segítheti.

Még nem találkoztam olyan programozási nyelvvel, rendszerrel, melyben ne lehetett volna megjegyzést írni. Ez egy olyan programrészlet, melyet a fordító eldob, arról a számítógép nem szerez tudomást, nem befolyásolja a futást, viszont nagy segítség lehet később a továbbfejlesztéshez.

A megjegyzés mikéntje már programozási nyelv függő. A legelterjedtebb programozási nyelvekben, beleértve a C++-t (így az Arduino-t) is kétféle megjegyzés típus van.

  • Egysoros megjegyzés, mely így kezdődik: //, és ezáltal a fordító figyelmen kívül hagyja az adott sor hátralevő részét.
  • Általános megjegyzés, mely így néz ki: /* … */; ez akár több soros is lehet (ez a tipikus), akár soron belül véget érhet (pl. ha egy paraméterhez szeretnénk megjegyzést fűzni).

Példa:

/**
 * Ez a függvény kiszámolja két szám összegét.
 * Bemenő paraméterek: két egész szám. Eredmény: az összegük.
 */
int osszead(/* első paraméter */ int a, /* második paraméter */ int b) {
  return a + b; // itt számolja ki az összeget
}

A fenti példa egyébként arra is példa, hogy hogyan ne írjunk megjegyzést: itt valójában mindegyik megjegyzés felesleges, ráadásul a paraméterek előtti megjegyzések lehetetlenné teszik azt, hogy az egész függvényt később ideiglenesen megjegyzésbe helyezzük.

A megjegyzésekhez való hozzáállásunk változott az idők folyamán. A kezdet kezdetén nem volt megjegyzés. Később volt időszak, amikor az volt az elvárás, hogy annyi megjegyzést írjunk, amennyit csak tudunk: fűzzünk megjegyzést az összes függvényhez, paraméterhez, változóhoz, foglaljuk össze szövegesen, hogy melyik kódrészlet mit csinál. A mai iskola inkább azt javasolja, hogy igyekezzünk olyan függvényneveket, változóneveket adni, és a kódot úgy strukturálni hogy azok önmagukban segítsék elő a megértést, és a megjegyzés tényleg olyan legyen, ahol arra a megértéshez szükség van. Egy Arduino-s példával illusztrálva: ha készítünk egy függvényt, mely az ultrahangos távolságérzékelő által mért értéket adja vissza, akkor nevezzük egy egyértelműen, pl. így: getObstacleDistanceInCm(), és ott nem kell megjegyzésben odaírni, hogy az akadály távolságát adja vissza cm-ben. A megvalósításban viszont ki kell használnunk pl. a hangsebességet, a mért értéket ennek megfelelően kell átalakítani, és ott már érdemes egy megjegyzést főzni az átalakító képlethez, pl. hogy az eltelt időt μs-ban kapjuk meg, a hang sebessége 340 m/s, és az ultrahangnak a visszapattanás miatt a távolságot kétszer kell megtennie, emiatt a képlet az, ami. Egyébként lehet, hogy más nem fogja tudni (és pár hónap elteltével mi sem), miért pont 0,017-tel kellett megszorozni a mért értéket ahhoz, hogy megkapjuk a távolságot cm-ben.

Speciális programozási nyelvi elemek

Ide azok a nyelvi elemek kerülnek, melyek már nem feltétlenül részei minden programozási nyelvnek. Az Arduino programozáshoz kezdetben ezekre nincs szükség, később viszont néhányról jó ha tudunk. Ne feledjük: ezek rendszertől független nyelvi eszközök, melyek összetettségük ellenére legvégső soron a vázolt assembly utasításokra fordulnak le.

Objektumorientált programozás

A fent felsorolt nyelvi eszközökkel természetesen bármit meg lehet valósítani, viszont a szoftver rendszerek a növekedésükkel egyre áttekinthetetlenebbé váltak. Gondoljunk bele: ha van több ezer függvényünk, több ezer globális változónk, igen nehéz követni, hogy ki kit hív, ki mit változtat meg, mi hova tartozik. Ugyanakkor vannak logikailag szorosan összetartozó elemek, és olyanok, melyek között a kapcsolat minimális. Kézenfekvő megoldás a logikailag összetartozó elemeket egy egységbe zárni, és csak annyit mutatni kifelé, ami feltétlenül muszáj.

Egy ehhez hasonló gondolatmenet vezethetett az objektumorientált programozás bevezetéséhez. A mai programozási nyelvek többsége objektumorientált. Ez egy összetett téma, nehéz pár sorban összefoglalni, viszont fontos tudnunk róla, mivel az Arduino C++-ban is megjelenik ez a technika (bár maga az Arduino kód inkább hasonlít C-re mint C++-ra). Néhány fontosabb része:

  • Megjelenik az osztály (class) fogalma.1 Egy osztály tipikusan tartalmaz változókat és függvényeket, melyet attribútumoknak (attributes) és metódusoknak (methods) nevezünk. Végső soron egy osztály nem más, mint attribútumok, valamint rajtuk dolgozó metódusok gyűjteménye.
  • Ideális esetben az egy osztályba tartozó attribútumok és metódusok szoros kapcsolatban állnak egymással. Ezt hívjuk kohéziónak (cohesion). Ha kettő vagy több jól elkülöníthető részre lehet osztani az osztály attribútumait és függvényeit, akkor megfontolandó az osztály feldarabolása.
  • Ugyanakkor két osztály kapcsolata ideális esetben igen korlátozott. Ezt hívják kapcsolódásnak (coupling). Ha két osztály között túl sok a kapcsolat, akkor megfontolandó vagy az osztályok átszervezése, vagy összevonása.
  • Az osztály önmagában egy absztrakt fogalom. Ahhoz, hogy használni tudjuk, példányosítani (instantiation) kell. A példányokat objektumoknak (object) nevezzük, és innen az objektumorientált programozás elnevezés is.
  • Az osztályok, egészen pontosan a belőlük példányosított objektumok között különböző kapcsolatok lehetnek:
    • Asszociáció (associtation): az egyik objektum hivatkozik a másikra.
    • Aggregáció (aggregation): az egyik osztály tartalmazza a másikat úgy, hogy a másik önmagában is létezhet.
    • Kompozíció (composition): az egyik osztály tartalmazhatja a másikat úgy, hogy a másik az első nélkül nem létezhet.
  • Az objektumorientált programozásban alapvető fogalom az öröklődés (inheritance): amikor egy osztály megörökli egy másik osztály jellemzőit, és kiterjeszti azt. Amiből öröklődünk, azt szülő vagy ősosztálynak hívhatjuk, a másikat pedig leszármazott vagy gyermek osztálynak. A leszármazott akár felül is definiálhat függvényeket az ősosztályban. Úgy is szokás fogalmazni, hogy a leszármazott osztály kiterjeszti (extends) az őszosztályt.2
  • Programozási nyelvtől függ, hogy lehetséges-e egyszerre több osztályból örökölni (multiple inheritance), vagy csak egyből. Ahol ez megengedett, ott felmerül a "gyémánt" (diamond) probléma: ha van egy A osztály, melyből öröklődik a B és a C, majd egy D osztály öröklődik a B-ből és a C-ből is, akkor a D-ben kétszer szerepelnek az A-ban definiált attribútumok és metódusok. (Ha ábrázoljuk ezt a hierarchiát, akkor az eredmény gyémántra hasonlít, innen az elnevezés.) Ez nehezen felderíthető hibákhoz vezethet, valamint magát a programozási nyelvet is elbonyolítja, mivel lehetőséget kell biztosítania arra, hogy a programozó jelezze, pontosan melyik ágra gondolt.
  • A példányosításnál általában megkülönböztetünk statikus és dinamikus típust. Ha példányosítunk egy leszármazott osztályt, akkor a változó típusa lehet az ősosztály, vagy a leszármazott is. Például ha van egy Gyümölcs ősosztályunk és egy Alma leszármazott osztályunk, akkor értékül adhatunk egy Alma objektumot egy Gyümölcs típusú változónak; ez esetben a statikus típus a Gyümölcs lesz, a dinamikus pedig az Alma. Viszont ez esetben csak a Gyümölcsre vonatkozó attribútumokat és metódusokat érjük el, az Almára vonatkozó speciálisakat nem. (A látszat ellenére ez egy nagyon hasznos és igen gyakori tulajdonság az objektumorientáltságnak.)
  • Öröklődésnél felüldefiniálhatunk (override) metódusokat; ez esetben ha a leszármazott osztályt példányosítjuk, akkor a felülírt metódus hajtódik végre. A felüldefiniálható metódusokat virtuális (virtual) metódusoknak hívjuk. Egyes programozási nyelvekben (pl. C++) külön jelölni kell a felüldefiniálható függvényeket, míg más programozási nyelvekben (pl. Java) mindegyik metódus alapból virtuális. A felüldefiniálás technikája az alábbi: a leszármazott osztályban létre kell hoznunk egy ugyanolyan szignatúrájú metódust, tehát melynek ugyanaz a neve, a paraméter listája és a visszatérési típusa is, mint amit felül szeretnénk definiálni. Előfordulhat, hogy valójában csak kiterjeszteni szeretnénk az eredeti metódust; ez esetben meghívhatjuk az eredeti megvalósítást, melyre az objektumorientált többnyire nyelvek lehetőséget biztosítanak.
  • Az attribútumok és metódusok láthatóságát (visibility) be lehet állítani. Vannak publikus (public), a külvilág számára látható részek, és privátok (private) is, melyek kifelé láthatatlanok. Programozási nyelvtől függően lehetnek egyéb láthatóságok is, pl. védett (protected), mely korlátozott láthatóságot biztosít. A láthatóságot érdemes korlátozni, mert ha bárki bármikor bármit módosíthat, akkor az jelentősen megnöveli a hibák valószínűségét, és csökkenti a hibakeresés hatékonyságát. Az attribútumokat célszerű teljesen elrejteni, és a metódusok közül is csak azokat publikussá tenni, amelyeket külső interfésznek szánunk. A láthatóságnak vannak átmeneti részei is, melyek csak bizonyos más osztályok számára láthatóak, például a leszármazott osztályok számára (ld. lejjebb).3
  • Egyes osztályok lehetnek absztraktak (abstract), melyet nem lehet példányosítani. Az ilyen osztálynak tipikus jellemzője az absztrakt függvény, melyet csak deklarál, de nem definiál, így a megvalósítás a gyermek osztályokra hárul.
  • A csak függvény fejlécekből álló osztályokat interfészeknek (interface) hívjuk. Ennek a gyakorlati haszna a következő: az adott osztályt használó komponensek jóval áttekinthetőbb képet kapnak a lehetőségekről, a megvalósítás részletei viszont rejtve maradnak. Ugyanannak az interfésznek több megvalósításai is lehet. Bizonyos művelet típusoknál, mint pl. az adatbázisba írás, akkor is érdemes különválasztani az absztrakciót és a megvalósítást, ha annak tudottan egyféle "éles" megvalósulása lesz: tesztelés céllal ugyanis elképzelhető más megvalósulás is, ami ténylegesen nem ír az adatbázisba semmit.
  • Mindegyik objektumorientált nyelvben létező fogalom a konstruktor (constructor), melynek segítségével létrejön maga az objektum. Egyes nyelvek (köztük az Arduino C++ is) definiálják a destruktor (destructor) fogalmát is, mely megszünteti az objektumot.
  • Az osztályokban létezhetnek statikus static) és nem statikus attribútumok és metódusok. A statikus attribútumokból mindig pontosan egy van, mindegyik példány ugyanazt látja, és akkor is létezik, ha nincs példányosítva az osztály. A statikus metódusokat példányosítás nélkül is meg tudjuk hívni, de azok csak statikus attribútumokon dolgozhatnak.

Egy való életből vett példával illusztrálom:

  • Absztrakt fogalom (azaz interfész) lehet például az, hogy jármű, mely egy metódust tartalmazhat, pl. azt, hogy elindul.
  • Ennek lehetséges megvalósításai (melyek még absztrakt osztályok): kerékpár, autó, repülő. Az autónak lehetnek tulajdonságai, pl. színe, rendszáma, hivatkozhat a tulajdonosára (asszociáció egy másik osztályra), tartalmazhat 4 kereket (mely aggregáció, mert az autó kereke létezik az autótól függetlenül is, de annak része).
  • Az autó, mint absztrakt osztály lehetséges megvalósításai a konkrét autó típusok, pl. Ford Fiesta.
  • A Ford Fiesta egy példánya egy bizonyos autó, adott rendszámmal. Az hivatkozik 4 konkrét kerékre, ami rajta van.
  • A konstruktor hozzárendeli a kerekeit, megadja a színét stb. A destruktor definiálja a kivonását a forgalomból.
  • Az, hogy mi módon indul el a jármű, magától a járműtől függ (az elindul mint absztrakt fogalom konkrét megvalósulása más és más különböző járműveken).
  • Adott jármű esetén érdekes lehet az, hogy abból a típusból hányat gyártottak. Ez az adat lehet egy statikus attribútum, hiszen adott típuson belül ugyanannyi mindegyik példány esetén.
  • Ha létrehozunk egy autószerelő műhelyt, akkor az ott végrehajtandó műveletek lehetnek statikusok.

Példa, mellyel a C++ szintaxis egy részét illusztrálom:

class Jarmu {
  public:
    void halad(float tav);  
};
 
class Auto : Jarmu {
  private:
    float fogyasztas;
    float benzin;
 
  public:
    Auto(float fogyasztas) {
      this->fogyasztas = fogyasztas;
    }
 
    void tankol(float mennyiseg) {
      benzin += mennyiseg;
    }
 
    void halad(float tav) {
      benzin -= (fogyasztas * tav);
    }
};

A C++-t mondhatjuk a C objektumorientált kiterjesztésének, viszont a felülről kompatibilitás kényszere megszorítja a tervezők kezét. A Java-ban viszont megvalósították azt, hogy nem lehetséges globális változókat és globális függvényeket létrehozni, muszáj mindegyiket valamilyen osztályba tenni.

Kivételkezelés

Amikor elkezdjük írni a programot, kezdetben általában nem törődünk a hibakezeléssel. Aztán fokozatosan jönnek a hibalehetőségek, és azt vesszük észre, hogy a hibakezeléssel kapcsolatos kód mennyisége jóval a fő sikeres forgatókönyv fölé nő. De mit lehet tenni, ha hiba történik? Az első kézenfekvő ötlet megfelelő hibakóddal való visszatérés. Sokáig nem volt ennél sokkal jobb megoldás, ezzel viszont volt pár probléma:

  • A programozási nyelvek többségében egy visszatérési értéke lehet egy függvénynek. Ezzel lefoglaljuk a hibakezelés számára ezt az értékes részt, és a valódi visszatérési érték valójában csak mellékhatásként jelentkezik, pl. egy globális változó beállításával, ami több programozási jó gyakorlat alapelvvel is szembe megy.
  • Elképzelhető, hogy sem az adott függvény, sem annak meghívója nem tud igazán mit kezdeni a hibával, azt a hívási láncban sokkal korábban lehet lekezelni.

E problémák kezelésére vezették be a kivételkezelés mechanizmusát. Ha olyan hiba keletkezik, mellyel az adott függvény nem tud mit kezdeni (pl. adatbázisból kellene kiolvasnia valamit, de nem elérhető az adatbázis), akkor "dob" egy kivételt. A kivételt "elkaphatja" tetszőleges függvény a hívási láncban. Ha valamelyik nem kapja el, akkor automatikusan tovább dobódik. Ennek következtében a visszatérési típus megmarad az üzleti logika számára, és nem kell különösebben bajlódni a hívási láncban történő továbbítással sem.

A szintaxisa a Java-ban igazán jól kiforrott, így azzal mutatom be.

try {
   ...
   f();
   ...
} catch (MyException1 e) {
   ...
} catch (MyException2 e) {
   ...
} finally {
   ...
}
...
void f() throws MyException1, MyException2 {
    ....
        throw new MyException1();
    ....
}

A Java-ban tehát a függvény fejlécben fel kell sorolni, hogy milyen kivételeket dobhat (egészen pontosan: csak az ún. ellenőrzött kivételeket kell felsorolni). A hívó oldalon a try … catch … finally struktúrával tudjuk kezelni; a finally mindenképpen lefut, akár történt kivétel, akár nem.

Az Arduino C++-ban is létezhet kivételkezelés, bár a valóságban elég ritka. A szintaxisa valamelyest eltér a Java-tól.

Generikus típusok

A tapasztalati igény hozta életre a generikus típusok megjelenését, mely a modern objektumorientált nyelvek jelentős részében megtalálható. Arról van szó, hogy egy adott osztály esetében nem adjuk meg a pontos típust, hanem megadunk egy általános (generikus) típusnevet, pl. T, és a használatnál adjuk meg (vagy bizonyos esetekben: próbálja meg kitalálni a fordító) a pontos típust.

Mire jó ez? Vegyük például a különböző adatszerkezeteket: halmazok, listák stb. Szinte típustól függetlenül ugyanazokat a műveleteket kell megvalósítani, és a megvalósítás is szinte ugyanaz: mindegy, hogy pl. 1, 2, 4 vagy 8 hosszú egészek halmazáról van-e szó, mondjuk egy beszúrás művelet ugyanúgy néz ki. Ez az állítás bizonyos általános algoritmusokra is igaz.

A C++-ban a generikus típusokat sablonok (template) segítségével használjuk, és innen ered a szabvány C++-ban található STL, azaz Standard Template Library elnevezés is. (Itt ugyanis a leggyakoribb adatszerkezetek és algoritmusok generikus megvalósítása található). Az Arduino-ban elvileg használhatjuk a C++ minden jellemzőjét, így a sablonokat is, bár a gyakorlatban erre szinte soha sincs szükség. A Java-ban is van generikus típus, igazán tökélyre viszont a Scala-ban vitték.

Egy C++ szintaxisú példával illusztrálom a használatát:

template <class T>
T getMax(T a, T b) {
  T result = a > b ? a : b;
  return result;
}
 
int max = getMax<int>(3, 5);

Nagyobb logikai egységek

Egy bizonyos méret felett a függvények és változók osztályokba szervezése sem bizonyul elég hatékonynak az áttekinthetőség javítására, még nagyobb logikai elhatárolásokra van szükség. A modern nyelvek többsége erre is lehetőséget biztosít. Ugyan nem valószínű, hogy olyan nagy Arduino kódot fogunk írni, hogy erre szükség legyen, de jó, ha tudjuk, hogy a C++-ban e célból ún. névtereket (angolul: namespace) hoztak létre, és alapból csak az adott névtéren belül látható osztályok, függvények és változók láthatóak, a többit prefixelni kell. Pl. a std::cout az std szabvány névterének a cout globális változóját használja. A Java-ban az osztályokat csomagokba (package) lehet szervezni, ráadásul a nyelv megköveteli azt, hogy a csomagnév pontosan megfeleljen a fizikai könyvtár szerkezetnek, ezáltal egyfajta rendet is kikényszerít.

Funkcionális programozás

A funkcionális programozási nyelvek esetén a függvény "első osztályú állampolgára" (first class citizen) a nyelvnek: értékül adhatjuk egy változónak, paraméterként átadhatjuk egy függvénynek, függvény visszatérési típusa is lehet. (Az olyan függvényeket, aelyek vagy paraméterül várnak másik fggvényt, vagy visszatérési típusuk függvény, magasabb szintű függvényeknek (higher order function). Lehet pl. olyan függvény paramétert megadni, melynek típusa egy olyan függvény, ami két egészet vár és egy egészet ad vissza. Ez a függvény meghívható pl. az összeadás vagy a szorzás függvénnyel is. És ezt tovább is gondolhatjuk: pl. a típusok lehetnek generikusok, tehát pl. nem követeljük meg, hogy egészek legyenek, hanem csak azt, hogy ugyanolyan típusúak, esetleg azt, hogy hol helyezkedjenek el az osztályhierarchiában.

A funkcionális programozás fogalmához tartozik a tiszte függvény fogalma, ami a következőt jelenti:

  • Nincs mellékhatása: nem állít be pl. külső változót.
  • A visszatérési értéke csak a paraméterül átadott értékektől függ.

A tiszta funkcionális programozás szabályai:

  • Állapotmentesség
  • Mellékhatás mentesség
  • Megváltoztathatatlan változók
  • Rekurzió használata ciklus helyett

Noha már a C is tartalmaz olyan nyelvi elemeket, amellyel megvalósítható a funkcionális programozás, túlzás lenne a C-t funkcionális nyelvnek nevezni, és rendkívül valószínűtlen az, hogy ez az Arduino-ban előforduljon. A funkcionális programozást igazán tökélyre a Scala nyelvben fejlesztették, mely aztán a Java 8-as verziójára hatott vissza, és a funkcionális programozás egyes alapelemeit ott is bevezették.

A keretrendszer által nyújtott környezetfüggetlen részek

Vannak olyan teljesen általános, a legtöbb helyen előforduló részek (ahol nem fordulnak elő, ott nem lenne elvi akadálya), melyek a látszat ellenére valójában nem nyelvi alapelemek

Szövegkezelés

A szövegkezelés szinte soha nem nyelvi elem, de olyan gyakran előfordul, hogy a legtöbb rendszer valamilyen szabványos könyvtárral fel van rá készítve. A szöveg típus szinte minden rendszeren String, és a leggyakoribb műveletek többnyire rendelkezésre állnak: stringek összevonása (concatenate vagy +), feldarabolása (split), hosszának megállapítása (length), nagybetűssé ill. kisbetűssé alakítása (toUpperCase, toLowerCase), az elején és a végén található szóköz jellegű karakterek törlése (trim), keresés benne (find) stb.

Az Arduino is tartalmaz egy beépített könyvtárat, mely a C++ String osztály függvényeinek egy részét tartalmazza. Specifikációja itt található: https://www.arduino.cc/reference/en/language/variables/data-types/stringobject/.

Algoritmusok és adatszerkezetek

Gyakran szükség van gyűjtemény jellegű adatszerkezetekre: halmazokra, listákra, sorokra, kulcs-érték párokat tárolókra, valamint a rajtuk végrehajtott algoritmusok is ismétlődnek, pl. rendezés. Az általában nyelvi elemként támogatott tömb már közepesen bonyolult programok esetén sem elegendő. Ezt az igényt felismerve először külső könyvtárként jelentek meg ezek az adatszerkezetek és algoritmusok, majd a legtöbb programozási nyelvben része lett a szabványos könyvtáraknak. A C++-ban, ahogy már szó volt róla, a Standard Template Library részeként szerepelnek.

Ezek az adatszerkezetek annyira beleivódtak a nyelvbe, hogy sok esetben agára a nyelvre is visszahatottak. Tipikusan a for ciklusnak új szintaxisa jött létre, az alábbi formában:

for (ElemType elem : collection) {
    ...
}

Az Arduino valójában túl kicsi ahhoz, hogy erre szükség legyen, így alapból ezek nem részei a szabvány könyvtárnak. Elvileg viszont - mivel szabvány C++-ról beszélünk - részévé tudjuk tenni.

Futtató környezettől függő általános részek

Vannak elemek a programozásban, melyeket annyira természetesnek gondolunk, hogy szinte a rendszer részeként tekintünk rájuk, valójában viszont nem azok.

Beolvasás, kiírás

Elsőre meglepő lehet, de már az olyan egyszerű műveletek is, mint mondjuk egy kiírás, nem számít nyelvi alapelemnek! De valójában miért is számítana annak? Gondoljunk csak az Arduino-ra: már ránézésre egyértelmű, hogy nincs hova kiírni az adatokat, mivel nincs képernyője. Persze nincs olyan rendszer, melyben ne lenne értelmezett valami formában legalább az output, de valami módon az input is szinte mindig értelmezett.

A kiíráshoz általában használnunk kell valamilyen rendszerkönyvtárat; a kiírás tehát nem nyelvi elem, de a legtöbb esetben automatikusan feltelepül a szükséges függőség. A függvény neve leggyakrabban print() (println(), printf(), …), write(), esetleg output(); tehát láthatjuk, hogy sajnos nincs egy egységesen elterjedt módszer. Az Arduino-nál talán a Serial.println() áll ehhez legközelebb, amely szöveg a soros monitoron jelenik meg.

A kiíráshoz hasonlóan a legtöbb rendszerben val valamiféle read() vagy input() függvény, pl. az Arduino-nál a Serial.readString() lehet ilyen, ami a soros monitorról olvas.

Memóriakezelés

Az ember erről is azt gondolná, hogy megkérdőjelezhetetlen alapelem, pedig nem az! Látszólag minden esetben ugyanazt kell csinálni: a lefordított program valahol elhelyezkedik a memóriában, valahol el kell helyezni a globális változókat, a lokális változókat (ezt hívjuk veremnek, angolul stack) és a külön lefoglalt nagyobb részeket (ezt hívjuk kupacnak, angolul heap), valamint gondoskodnunk kell a memória felszabadításról. Nagyjából ez igaz is, viszont ha már egyetlen lépéssel előrébb megyünk, akkor óriás eltéréseket tapasztalunk. Például a modern számítógépeken megkérdőjelezhetetlennek tűnő alapelv az ún. Neumann-elv: a kód és az adat ugyanoda kerül. Nos, az Arduino-n nem így van: ott létezik külön programmemória és adatmemória (SRAM), sőt, olyan adatmemória is, melynek tartalma újraindításkor nem veszik el (EEPROM). Az olyan alacsonyabb szintű programozási nyelvek esetén, mint pl. a C vagy a C++, nekünk kell gondoskodnunk a memória lefoglalásáról és felszabadításáról is, de pl. a Java esetén a memória lefoglalása alap nyelvi elem, a felszabadítása viszont a Java virtuális gépre van bízva.

Többszálúság

A többszálúságot szintén alap dolognak gondolnánk, hiszen a számítógépen, telefonunkon egyszerre oly sok minden fut, de valójában nem az! Én még emlékeszem a DOS ill. a Windows 3.1 világra, amikor tényleg nem volt még szimulált többszálúság sem, és az olyan egyszerű eszközökben, mint az Arduino, szintén nincs többszálúság. A többszálúság annyira nem nyilvánvaló, hogy pl. a C/C++ világban nem is létezik rá olyan szabványos megoldás, mint mondjuk a kiíratásra (tehát ami rendszer függő ugyan, de legalább a programozás szintjén ugyanazt kell írni): itt szinte minden operációs rendszerre külön utána kell nézni, hogy ott hogyan kell megvalósítani, és mire van szükség ahhoz, hogy használni tudjuk, vagy olyan külső könyvtárat kellett keresnünk, mely működött több operációs rendszer alatt is. A futtató környezetet megkívánó nyelvek (Java, Scala, C#, vagy akár a Python) esetén már beszélhetünk az alap keretrendszer által nyújtott többszálúságról, melyet akár még néhány nyelvi elem is támogathat, de a többszálúság ott sem nyelvi alapelem.

Grafikus felület

Grafikus felület sok esetben nem létezik (gondoljunk egy karakteres terminálra, vagy mondjuk az Arduino-ra, ahol még terminál sincs), és operációs rendszerről rendszerre változik (más Windows és más Linux alatt). Emiatt sem lehet a grafikus felület nyelvi alapelem, sőt alap beépített könyvtár sem. C++-ban még kísérletet sem láttam ennek áthidalására. A futtató rendszereket kívánó programozási nyelvek esetén (Java, C#, Python) viszont ezt is szabványosították.

Speciális komponensek

Vannak olyan gyakran ismétlődő komponensek, amelyek a legtöbb esetben még nem, vagy csak részben váltak a rendszer részévé. Ezekből ismerünk meg most néhányat.

Üzenet kezelés

Az egyes komponensek üzeneteket küldhetnek egymásnak. Alapvetően háromféle üzenet típust különböztetünk meg:

  • Adott címzettnek küldött üzenet (point-to-point)
  • Üzenet küldése azoknak, akik feliratkoztak adott témára (topic)
  • Üzenet behelyezése egy sorba, amit pontosan egy valaki fog feldolgozni (queue)

Webes szolgáltatások

Ide sorolom mindazt, amikor a kommunikáció weben keresztül történik. Néhány példa:

  • Egy szolgáltatás nyújtása a web felé, tehát amit egy külső rendszer (vagy akár a végfelhasználó) egy URL segítségével elér.
  • Egy másik rendszer által nyújtott webes szolgáltatás elérése.
  • Az e-mail küldést is felfoghatjuk egyfajta speciális, weben alapuló szolgáltatásként.

Szerializáció

A webes szolgáltatások megjelenésével alapvetővé vált a szerializáció. Ez azt jelenti, hogy a memóriában tárolt információt hogyan kódoljuk, hogy azt később megfelelően dekódolni tudjuk. A webes példában a kódolt üzenetet szeretnénk átadni egy másik rendszernek, amit az dekódol. Akár bináris, akár szöveges kódolás elfogadható. A probléma nem nyilvánvaló:

  • A szöveg kódolása látszólag nyilvánvaló, de érdemes megnézni a betű változók alfejezetnél felsorolt problémákat. Kezdetben szinte megkerülhetetlen probléma volt az e-mailben szereplő ékezetek. Az új sor kódolása szintén eltér különböző rendszerek között, ami gyakran eredményezett olyat, hogy a fogadó elég "szellősen", üres sorokat tartalmazva kapta meg az üzenetet.
  • A számok kódolása úgyszintén, a valóságban viszont ez sem nyilvánvaló. Már a több bájtos egészeknél is felmerül a kódolás kérdése: milyen sorrendben szerepelnek a bájtok, vagy előjelesként kell-e értelmezni. A lebegőpontos típusok kódolásánál több szabvány is létezik.
  • Vannak bekódolhatatlan részek, pl. egy hivatkozás egy helyi adatbázis kapcsolatra, ami a címzettnél nem szerepel. Ezeket is megfelelően kell kezelni.
  • A nem egyszerű típusok (pl. objektumokat tartalmazó listák) kódolása szintén nem nyilvánvaló.

A kódolás leggyakrabban karakteres és nem bináris, melynek több valószínű oka is van:

  • Így az ember is el tudja olvasni, ami megkönnyíti a hibakeresést.
  • Ezzel el lehet kerülni a kódolásokból adódó eltéréseket. Pl. az, hogy 12345, egyértelmű.
  • Nincs gazdasági kényszer a tömörítésre: a memória, a tárhely és a hálózati kapcsolat is elég gyors ahhoz, hogy ez ne jelentsen problémát.

Két jelentősebb protokoll terjedt el: az XML és a JSON. A szerializáció problémája általánosnak tűnik, vannak is szabvány megoldások, viszont - kissé meglepő módon - többnyire a programozási nyelvek nem nyújtanak szabványos saját megoldást, ahhoz külső könyvtárakra van szükség.

Adatbázis kezelés

Az adatok tartós lementésének az igénye egyidős az emberiséggel a számítástechnikával. Számos módszer lehetséges. Az egyszerűbb adatok mentéséhez egy szövegfájl is megteszi. A legelterjedtebb megoldás a relációs adatbázisba mentés, ahol adattáblákon belüli oszlopok és sorok vannak, az adattáblák között pedig kapcsolatok. A probléma általános volta ellenére megkerülhetetlenek bizonyos külső könyvtárak használata.

Üzenetkezelés

Az egymással együttműködő rendszerek kezdetben erősen kapcsolódtak egymáshoz: például egy web szolgáltatás függvényhívás során egyszerre kellett, hogy jelen legyen a kliens és a szerver is. Ez problémákhoz vezet az üzemeltetés, karbantartás során, így megjelent az igény arra, hogy a komponensek között minél kisebb legyen a kapcsolódás. Az üzenetorientált köztes réteg (Message Oriented Middleware, MOM) a mediátor szerepét tölti be az egyes komponensek között, amelyek ennek segítségével aszinkron módon tudnak egymással kommunikálni. Ebben a részben az üzenetkezeléssel kapcsolatos legfontosabb tudnivalókat foglaljuk össze. A gyakorlatban legtöbbször valamilyen köztes réteget (alkalmazás szerver, webszerver) használunk erre a célra; a lenti példák viszont önmagában futó alkalmazások (eltekintve a függetlenül elindítandó üzenetkezelő rendszertől).

Az üzenetek kezelésénél két fő fogalmat kell megismernünk:

  • Sor (queue): az ide küldött üzeneteket pontosan egy fogadó fogja feldolgozni. Ezt point-to-point módszernek is hívjuk.
  • Téma (topic): az ide küldött üzeneteket minden fogadó megkapja. Ennek a másik neve publish-subscribe. A feliratkozás kétféle lehet:
    • Tartós (durable): ez esetben a fogadó akkor is megkapja az üzenetet, ha annak küldésekor épp nem volt elérhető.
    • Nem tartós (non-durable): a fogadó csak akkor kapja meg az üzenetet, ha annak küldésekor elérhető volt.

Az üzenet küldés és fogadás is a tranzakció része, így a műveletet például jóvá kell hagyni (acknowledge). Az üzenet küldő rendszer általában bekonfigurálható, hogy probléma esetén próbálkozzon-e az újbóli küldéssel, ill. ezt hányszor tegye. Ha egy tranzakció nem sikerül, akkor az kihatással van az üzenetek kezelésére is, ill. egy tranzakció az üzenetkezelés miatt is elhasalhat. A példákban automatikus jóváhagyás mechanizmust alkalmazunk.

A küldés viszonylag egyértelmű, üzenetet fogadni viszont többféleképpen is lehet. Az egyik az explicit kiolvasás egy függvényhívással: ez megállítja a programot addig, amíg az üzenet meg nem érkezik, ill. egy bekonfigurált adott maximális ideig. A másik pedig a visszahívásos (callback) módszer: ez esetben egy interfész által meghatározott függvény hívódik, ha üzenet érkezik.

A brókert általában háromféleképpen tudjuk futtatni:

  • el tudjuk indítani programból,
  • önálló alkalmazásként,
  • olyan alkalmazásszervert használunk, amely alapból tartalmazza azt.

Egységtesztelés

Az egységtesztelés fejlesztői teszt, lényegében a fejlesztés része. Eléggé fontos ahhoz, hogy komoly elmélet épült köréje. Lássuk néhány fontosabbat közülük!

F.I.R.S.T.

Ez a betűszó 5 rövidítést takar. Nem egységes a szakirodalom abban, hogy melyik pontosan micsoda, nem talán nem is ez a lényeg, hanem a mondanivaló.

  • Fast, azaz gyors: az egységteszteknek gyorsaknak kell lenniük. A teljes lefedettséghez igen nagyszámú tesztesetre van szükség, és ennek akkor van értelme, ha bármikor gyorsan le tudjuk futtatni az összeset. Egy-egy tesztesetnek pillanatok alatt be kell fejeződnie.
  • Isolated vagy Independent: igazából mindkettő ugyanazt jelenti: a teszteseteknek függetleneknek kell lenniük egymástól. Nem szabad, hogy az egyik teszt futása függjön a másiktól. Az egységtesztet futtató rendszerek tipikusan véletlen sorrendben futtatják le az eseteket. Nehezen felderíthető hibát okoz az, ha van függőség két teszteset között, amely csak ritkán jön elő, ha a sorrend pont az, amelyik előhozza a hibát.
  • Repeatable, azaz megismételhető: a teszteseteknek determinisztikusnak kell lenniük. Nem szabad, hogy függjön semmi tőle független dologtól, és akárhányszor ismételjük meg, ugyanannak kell lennie az eredménynek.
  • Self-validating, azaz önellenőrző, ami azt jelenti, hogy a tesztesetnek egyértelműen sikeresnek vagy hibásnak kell lennie. Az nem megengedett, hogy az eredményeket kézzel ellenőrizzük, ill. azt sem, hogy a futtatás előtt kézi beállítást kelljen végrehajtanunk. (A magyarázatok alapján én ezt inkább self-containednek hívnám, ami azt jelenti, hogy mindent tartalmaz, amire szüksége van, nem kell kívülről hozzátenni semmit.)
  • Thorough vagy timely. Ez két különböző dolgot jelent. A thorough alapost jelent: ellenőrizzünk minden lehetséges lefutást, a szélsőséges értékeket, nagy adathalmazt, a lehetséges kivételeket, a hibás paramétereket, különböző jogosultságokat stb. A timely kb. azt jelenti, hogy naprakész, ami inkább filozófiai téma: mindig álljunk készen egységteszteket készíteni, az egységtesztek készüljenek el időben (a TDD fejlesztés során már a fejlesztés megkezdése előtt) stb.

TDD

A TDD a Test-Driven Development rövidítése, ami magyarul teszt-vezérelt fejlesztés jelent. A gyakorlatban azt azt jelenti, hogy a fejlesztő (aki esetleg nem is az üzleti logika fejlesztője) előre megírja teszteseteket, amelyek kezdetben értelemszerűen elhasalnak, és akkor tekintjük megvalósítottnak a függvényt, ha mindegyik teszteset sikeres. A nem TDD módszertannal előre készítjük el a kódot, és hozzáírjuk a teszteseteket.

Lefedettség mérése

A teszteknek egy fontos fokmérője az, hogy mennyire fedi le a lehetséges ágakat. Gondoljunk itt a sikertelen ágakra is, melyek normál üzemelési környezetben akár rendkívül ritkán futnak le: azt érdemes elkerülni, hogy legyen olyan programsor, ami először az életben az ügyfélnél hajtódik végre. Az egységteszteléskor tehát érdemes feledettség mérő eszközt (unit test coverage tool) használni. A jó lefedettség mérő eszköz nemcsak azt méri, hogy a programkód hány százaléka futott le, hanem azt is, hogy pl. egy "A vagy B" ill. "A ÉS B" jellegű feltétel esetén mind a 4 lehetséges kombinációt ellenőriztük-e. Tehát nem teljes a lefedettség, ha van egy feltételes ág, amely lefutott ugyan, de a feltétel összes létező kombinációja nincs letesztelve.

Mockolás

Mockolásnak nevezzük azt a folyamatot, amikor a teszteset környezetét felépítjük. Az egyszerű esetekben erre nincs szükség, a valóságban viszont a tesztelendő függvény meghív más függvényeket, amelyek esetleg más osztályokban vannak definiálva, és előfordulhat, hogy ott már nem az eredeti lefutást szeretnénk végrehajtani egységtesztkor. Ha például a kód adatbázisból olvas, akkor célszerű a megfelelő függvényeket átirányítani egy kamu megvalósításra, ami fix értékeket ad vissza, hogy ne kelljen fizikailag felépíteni az adatbázis kapcsolatot. Az ilyen osztályokat angolul test double-nek nevezzük. Nem tudom, van-e erre hivatalos magyar fordítás; most megalkotok egyet: teszt dublőr. A teszt dublőröket több csoportba oszthatjuk:

  • Fake (hamis): működő, de egyszerűsített megvalósítást jelent. Tipikus példa az adatbázisból olvasás, amikor adott lekérő kulcsokra előre beállított, fix értékeket ad vissza. Technikailag ez általában azt jelenti, hogy van egy interfészt, amit a működő kódban a valós implementációval használunk, a teszt kódban viszont az interfészt elére beállított
  • Stub (törzs): részbeni megvalósítást jelent. Csak azk a részek vannak megvalósítva, melyek szükségesek a teszthez.
  • Mock (utánzat): ezek megjegyzik a hívásokat, és arra valóak, hogy segítségével megszámoljuk, melyik hívás hányszor történt, és ez megfelel-e az elvártnak. Megkülönböztetünk nice (kb. szabatos) és strict (szigorú) mockolást: az első estben a hívás sorrendje nem számít (általában ez az alapértelmezett), a másodikban a sorrend is számít. A megvalósítás pedig olyan, hogy "ha ezzel a paraméterrel történik a hívás, akkor az legyen a válasz".
  • Dummy (kamu): olyan objektum, amit létrehozunk, de nem használjuk. Pl. kötelező, de abban az esetben nem hazsnált paraméter esetén lehet erre szüség.
  • Spy (kém): a mockolt megvalósítás a valós eljárást használja.

Ne rejtsünk el TUC-ban TUF-ot

Angolul never hide TUF in TUC. A rövidítések jelentése az alábbi:

  • TUF: Test Unfriendly Feature (barátságtalan programjellemző tesztelése): a nehezen tesztelhető részeket jelenti, pl. hálózati kapcsolat, külső könyvtárak használata stb.
  • TUC: Test Unfriendly Construct (barátságtalan konstrukció tesztelése): a nehezen hozzáférhető részeket jelenti, pl. privát vagy statikus változók, függvények.

Ez a mondás tehát azt jelent, hogy kerüljük el a következőt: mondjuk egy adatbázis elérést egy privát függvényben valósítunk meg, és azt a privát függvényt nem teszteljük.

Alkalmazás szerverek

Ez egy újabb absztrakciós szint, mondhatni olyan, mint az Eredet című filmben az álom az álomban ötlet. Ez elsősorban a Java világra jellemző, melynek futtatásához már önmagában szükség van egy keretrendszerre, itt viszont a Java futtató környezet köré szervezünk még egy réteget, az alkalmazás szervert. (Akik tudják, hogy miről van szó: alkalmazás szerver alatt most nem kizárólag azt értem, amit a specifikáció annak definiál, hanem minden olyat, melynek futtatásához nem elegendő a JVM, kell hozzá egy másik keretrendszer is.) Ezek alapszolgáltatásként nyújtanak olyan platformfüggő dolgokat (mint pl. az adatbáziskezelés), sőt, egy olyan absztrakciós réteget nyújtanak, melyek elfedik a fejlesztő elől a részleteket (az adatbázisos példánál maradva: például a tranzakciókezelést, vagy az adattáblákból beolvasott adatok Java objektumokká szervezését). Az alkalmazás szerverekre írt programok elterjedését nagyban segítette az annotáció nyelvi elem megjelenése: ezzel tud "üzenni" a programozó magának az alkalmazás szervernek (az adatbázisos példát folytatva: ezen keresztül tudja a fejlesztő jelezni, hogy azt a bizonyos osztályt próbálja meg úgy kezelni az alkalmazás szerver, melybe az adatbázisból kiolvasott adatok kerülnek). (Az Eredet hasonlatra visszatérve: ez olyan, mintha ezen keresztül menne át információ "álom az álomból" a valóságba, megkerülve az egy szintű álmot.)

Az Arduino programozás esetén erről a szintről szó sincs…

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License