Na még egy réteg

A blog kicsit feléled egyetlen vékonyka szösszenet erejéig, aztán ki tudja. A mai téma amaz ismert bon mot köré fog csoportosulni, miszerint minden szoftvertervezési hiba javítható egy extra absztrakciós szint bevezetésével, kivéve ha az a baj, hogy túl sok szint van. Méghozzá két olyan anti-patternt mutatunk be, melynek lényege a további absztrakciót egyáltalán nem biztosító absztrakciós réteg.

Az első neve legyen "Rename Layer". A Rename Layer jellemzője, hogy az alatta levő réteg minden eleméhez (eljárásához, típusához, stb) tartalmaz egy annak teljesen megfelelőt, csak mondjuk OpenFile helyett FileOpen a neve, és négy logikai paraméter helyett egy halmazt (set) vesz át.

A Rename Layer arra jó, hogy növelje a fejlesztés időköltségét, a dokumentáció elkészítésének forrásigényét és magának a dokumentációnak a mennyiségét, utóbbi miatt valamelyest növeli a megtanulási időt. Hozzáad némi extra hibalehetőséget, és kevéske erőforrásigényt. Utóbbiak nem nagy ár, csak hát a hozadék, az meg semmi.

A másik példa a mai napra a "Please Layer". A Please Layert onnan ismerni fel, hogy a dokumentációban némi dramatikus felvezetés után, mint például licence ellenőrzés, inicializálás, ilyesmik, hirtelen crescendoba csap át a zenemű, és valami efféle eljárást találunk:

function PerformAction(Action: TActionCode; Params: TParamStrct): integer;

 

 

Majd egy hosszú táblázat következik a különböző Action kódokkal. Gondolom, eddigre kitalálható volt, hogy több eljárás nincs is, esetleg még egy rövid tust húz a zenekar, de a réteg lényegében ebből az egy eljárásból áll.

Az ilyen rétegekre jellemző az Egyetlen Eljárás nagyzoló elnevezése. Nem is egyszerű a kérdés, mivel ez Az Eljárás, miért lenne egyetlen valaminek megkülönböztető neve? Ezért javaslom inkább az alábbi nevet, mivel pontosan fedi a lényeget:

Please(acOpenFile, ['c:\autoexec.bat'])

 


Watch!

Itt van mindjárt a Delphi watch ablaka. Nem bonyolult dolog, lényegében egy lista. Lehet hozzáadni, és törölni belőle. Lehet módosítani az elemeit. Hány idegesítő hiba férhet egy ilyen egyszerű valamibe? Vegyük sorra.

Az első hiba talán nem is konkrétan a watch ablakkal van, hanem általában a dokkolási mechanizmussal. Ha megnyitom a watch ablakot, majd bedokkolom az editor alá. Ezután az ember dülledő szemmel nyomkodja az enter gombot, de új elemet ugyan felvenni nem tud, ugyanis a fókusz visszament az editorra, oda írtunk be pár új sort nagy lendülettel. Próbálkozunk az alt+ctrl+W kombinációval, de az semmit sem csinál, ha az ablak már látszik, és egy fókuszált ablakba van bedokkolva. Egérrel kell odakattintani, más módszer nincs. Az F7 vagy F8 gombot megnyomva a fókusz ismét az editorra megy át, visszaút nincs, csak az egér.

Az elemeket nem lehet átrendezni. Emiatt az ember hamarosan egy ilyen elrendezésnél köt ki:

SomeList[i]
SomeList[0]
i
SomeList.Count
SourceText
SomeList[5]
SomeList[4]
j
Ebben az összevisszaságban semmit nem találni. Rendet rakni pedig úgy lehet, hogy az ember kitörli az egészet, és felveszi újra.

Nem mintha kitörölni olyan könnyű lenne. Ha a ctrl-A kombinációval szeretnénk kijelölni az összes elemet, rájövünk, hogy az nem arra való, hanem új elem felvitelére (Add). Mindent kijelölni egyszerűen nem lehet. Illetve lehet, egérrel.

De abban sincs sok köszönet, mert ha kijelöljük az összes lehetséges sort, azt tapasztaljuk, hogy a Delete gomb nem csinál semmit, és a jobbgombos menüben is szürke a törlés. Van ugyanis egy sor, ami üres, ám mégis kijelölhető. Ennek funkciója az, hogy ráállva és entert nyomva új elemet vihetünk fel. Ha ezt az üres sort is kijelöljük, törölni nem lehet.

Egyáltalán, vajon minek üres sor? Ha újat akarok felvenni, arra ott van az Ins gomb (vagy a némileg szokatlan ctrl-A, ami ugyanazt csinálja). Miért jó az nekem, hogy egy nemlétező elem módosításával hozok létre egy újat? Az lenne a logika, hogy ha egy nemlétező elemből létező lesz, az végülis módosítás? Ha a Borland (CodeGear? Embarcadero?) ilyen gondolkodásmódot vár el a programozóktól, annak nem lesz jó vége.


De a homokóra ...

Mondjuk az ember éppen csinál valami olyasmit, amire oda kell figyelni, emlékezni kell pár részletre, stb. Eközben ki kell menteni egy fájlt valamely programból. Két másodperc alatt megnyílik az Kimentés ablak, legördítjük a mappákat, ám az nem nyílik ki, és csak képzeljük hallani, ahogy a számítógépben zakatolnak a fogaskerekek. Tizenöt másodperc múlva megjelenik az áhított lista. De ha közben újra kattintottuk a legördítő gombot, gondolván, hogy az elsőt valahogy nem vette észre, rögtön el is tűnik. Ha végre elő tudtuk csalni, választunk, fájlnevet adunk, mentünk.A kimentés két másodperc. Mire az ember ennek a végére ér, tanakodhat, hogy hol tartott éppen a feladatával.

Fülesablakban átváltunk egy másik fülre, de eszünkbe jut, hogy még valamit nem töltöttünk ki. Ám a gép már dohogva dolgozik az ablak megjelenítésén, éppen az adatbázisból pakolja kifele az adatokat, és nem hajlandó félbehagyni. Ki kell várni elsőre is, majd a módosítás után megint, immár a helyes adatokkal. Az ember frusztrációszintje a piros vonal felé kúszik. A jövőben minden adatot háromszor átolvas, és akkor is szorongva nyomja meg a gombokat. Hogy ne csak panaszkodjunk, itt vannak a tanulságok mára:

  • Aki készíti, annak sokkal kevésbé fontos, hogy a program gördülékenyen használható legyen. Onnan kisebbnek látszanak a kényelmetlenségek. Ha választani kell a bonyolultabb megvalósítás és egy másodpercnyi várakoztatás között, azonnal az utóbbit választjuk. Adott esetben ez lehet jó választás, de ezt tudatosan kell végiggondolni, ergonómiai szempontokat figyelembevéve. Tudjunk a felhasználó fejével gondolkodni.
  • Ami pár másodpercnek tűnik, az nem biztos, hogy csak pár másodperc. A folyamatok gyakran egymás után történnek, így az idők könnyen összeadódhatnak. Előre nem látott körülmény lassíthatja feldolgozást. Erős hálózati forgalom mellett párszáz sor letöltése az adatbázisból pillanatszerűről kínosan lassúra változhat. Egy fájl beolvasása is eltarthat öt másodpercig is akár, ha a lemezt a rendszer energiatakarékosságból kikapcsolta.
  • A felhasználótól ellopott időt nem óradíjban kell számolni. Tény, hogy egy másodperc késedelem nem nagy veszteség. A veszteség abban rejlik, hogy felhasználó munkakedve, figyelme, lelkesedése milyen mértékben degradálódik. Az ilyen lelkesedéselszívó módszerek a produktivitást aláássák.

Tessék ezért figyelmet fordítani arra, hogy a program maradjon folyamatosan készenlétben, a felhasználója instrukcióit azonnal kövesse. Amit lehet, a háttérben csináljon, és ha már nincs rá szükség, hagyja abba. A felhasználó érezze úgy, hogy egy hangszeren játszik, amit tetszése szerint szólaltathat meg, és amely idomul a kezéhez. Mint minden szabály, ez is értő fülnek való. Nem muszáj minden tizedmásodpercet lefaragni. De kötelező végiggondolni, és tudatosan dönteni.


Csuka, róka

A TThread.Suspend eljárás igazából rosszul van elnevezve. A helyes név így hangzik: TThread.RandomlyHangTheProgram. Nem vicc, ezt a helyzet. A legjobb, amit tehetünk, ha egyáltalán nem használjuk, de talán még jobb, ha el is felejtjük.

A Suspend ugyanis megállítja a szálat, akármit is csinál az éppen. Fogva tart egy fájlt? Várólistára mindenkit. Belépett egy critical sectionbe? Nincs több belépés. Képzeljük csak el azt, ha a szál éppen memóriát foglalt. A memóriakezelő is osztott erőforrás, critical section védi a szálak versengése ellen. Ha a szál éppen fogva tartja a critical sectiont amikor a Suspend meghívódik, a memóriakezelő ettől kezdve zárva van minden arrajáró számára. Egy egyszerű szövegművelet is lehetlenné válik, ha ahhoz memóriafoglalás vagy felszabadítás kellene. Ha pedig éppen az a szál próbál memóriát foglalni, amelyik a Suspendet meghívta, és majd később a Resume-ot hívná, kialakul a csuka fogta róka esete, amit csak úgy hívunk: deadlock.

Az olvasóra bíznánk annak végiggondolását, hogy mi történik, ha meghívjuk a win32 API TerminateThread eljárását.


Élet with és pointerek nélkül

Legyen egy record típusú adatunk, ami legyen ráadásul egy tömbben. Akarjuk az adatot alaposan megdolgozni. Ez első megközelítésben így nézne ki:

type
  TThing = record
    ... 
  end;

...
  if Things[i].AreaAutoCalc then
    Things[i].Area := Things[i].Width * Things[i].Height;
...

Az indexelés négyszeri megismétlése nem túl szép. A Delphi erre kínálja fel a with szerkezetet:

...
  with Things[i] do
    if AreaAutoCalc then
      Area := Width * Height;
...

A with azonban sokak szerint (pl szerintünk) kerülendő, mert számos veszélyt és kényelmetlenséget rejteget. Ezek forrása az, hogy összekeverednek a nevek, nem tudni, hogy egy adat a with-ben szereplő objektumé, vagy az eljárás környezetéhez tartozik. Például ha később bevezetünk egy új adattagot a típusunkba, előfordulhat, hogy egy with-es helyen mostantól ezt az adattagot fogja jelenteni az, ami eddig valami mást jelentett. Erről csak akkor fogunk tudomást szerezni, amikor a program csúnyán összeomlik, vagy tönkreteszi a százmillió dolláros adatbázist/gépet.

Bevezethetünk változót is.

var
  Thing: TThing;
...
  Thing := Things[i];
  if Thing.AreaAutoCalc then
      Thing.Area := Thing.Width * Thing.Height;
  Things[i] := Thing;
...

Nem túl szép ez sem, továbbra is kétszer kellett indexelni, ráadásul egy másolatot készítünk az eredetiről, ami nem baj, ha nem tizenöt kilobájtos recordot használunk egy százmilliós ciklus közepén. De akkor baj.

Kéne valami olyan típus, hogy hivatkozás egy adatra.

var
  Thing: reference to TThing;
...
  Thing := Things[i];
  if Thing.AreaAutoCalc then
      Thing.Area := Thing.Width * Thing.Height;
...

Ez azt jelentené, hogy a Thing változó nem egy másolat, hanem mostantól így hivatkozunk az adatra magára. Ennek lenne a Delphi megvalósítása a típusos pointer. Kicsit sárgább, kicsit savanyúbb, de a mienk.

var
  Thing: ^TThing;
...
  Thing := @Things[i];
  if Thing.AreaAutoCalc then
    Thing.Area := Thing.Width * Thing.Height;
...

Ezzel meg az a baj, hogy egyrészt igen csúnya, másrészt elég veszélyes játék. Ha elfelejtjük inicializálni, nagy bajt is lehet okozni. Ez egy alacsonyszintű struktúra, amit szerencsésebb lenne elkerülni, főleg kezdő szinten. A pointer még a with-nél is rosszabb.

Az se volna rossz, ha a with szintaxisa kicsit erősebb lenne:

...
  with Thing := Things[i] do
    if Thing.AreaAutoCalc then
      Thing.Area := Thing.Width * Thing.Height;
...

Kis lépés a Delphi fejlesztőinek, nagy lépés a programozónak. A with időtartamára létrejönne egy szimbólum, ami hasonlatos a pointerhez, de mindig jó értéket tartalmaz, és nem él tovább, mint az adat, amire mutat. Ám az álmodozás nem vezet sehová.

Van szerencsém bejelenteni, hogy a Delphiben is van ilyen hivatkozási lehetőség, csak kis trükk kell hozzá. Ez pedig a var illetve a const paraméter.

...
  procedure CalcAreaAsNeeded(var Thing: TThing);
  begin
    if Thing.AreaAutoCalc then
      Thing.Area := Thing.Width * Thing.Height;
  end;
begin
  ...
    CalcAreaAsNeeded(Things[i]);
  ...
end;

Előnyök: korrekt megoldást kapunk, ami ráadásul önmagát dokumentálja, és még pont is jár érte.


Nem, nem és nem!

Nem, az ablak fejléce nem való üzenetek közlésére.

Nem, nem lehet üresen hagyni az üzenetrészt.

Nem, a Close nem helyes választás erre az esetre.

Ez így nem jó.

symantec üzenetablak, fejlécben az üzenet, amúgy üres.


Gombsaláta

A K9 egy jó kis spam szűrő, ingyenes, kicsi, egyszerű. Egyáltalán nem áll szándékunkban rosszat mondani róla. De most mégis arra fogjuk használni, hogy bemutassuk, hogy kell zavaró felületet készíteni.

Elöljáróban, hogy mire való a K9. Beépülve a levelezőprogram és a mailboxunk közé, figyeli a leveleket, amiket kapunk, és megpróbálja kitalálni, hogy melyik kéretlen reklámszemét, és melyik valódi. A program tanítható, tehát ha átengedett egy szemetet, vagy elfogott egy jó levelet, a megfelelő ablakban ez a döntés korrigálható. A korrekciót megjegyzi, és legközelebb pontosabban ítél.

A program kritizálásra váró ablaka pedig az alábbi.

a k9 ablaka email listával és gombsorral

Az ábrán az a felület látható, amivel az elmúlt időszakban kapott emailjeinket tekinthetjük át, illetve igény szerint átsorolhatjuk szemétté vagy jó levéllé. Az ablak tetején egy toolbar-szerű kialakításban gombok sorakoznak. Funkciójuk változatos. Haladjunk balról jobbra.

Az első két gomb együtt értelmes, közülük mindig az egyik van lenyomva. Ha az első van lenyomva, akkor a jó leveleket, ha a második, a szemeteket látjuk a listában. Ez tehát egy szűrő funkció. A programbeli megvalósítás feleslegesen szigorú, hiszen semmi akadálya nem lenne, hogy egyszerre lássuk az összes levelet. Ha viszont kizárólagosra készítették, talán szerencsésebb lett volna fülesablakot választani, mint arról korábban írtunk.

A következő két gomb egymáshoz nem kapcsolódó funkciókat takar. Az elsővel megnézhetjük a levél tartalmát, a másodikkal törölhetjük a raktárból. Érdemes megfigyelni, hogy a törlés gomb képe azonos a spam szűrőgomb képén levő áthúzással, csak ott egy mappa is van. Szerencsétlen dolog két eltérő funkciót azonos szimbólummal jelölni. A raktárból törlés és a szemétté nyilvánítás egymástól nagyon különböző dolgok.

A következő két gomb a levél átminősítésére szolgál. A jóvá minősítő gomb ikonja azonos a jó leveleket mutató szűrőgombéval, ez rendben van. A szemétté minősítő gomb ikonja egy szokvány tiltó jel. Ez szerencsésebb jelölés az áthúzásnál, mivel semmi esetre sem keverhető a törléssel. Ám mindenképpen helytelen, hogy a szűrőgombbal nem illeszkedik. További zavaró tényező, hogy a gombok felirata megegyezik az első két gombéval, holott funkciójuk igencsak eltér. Van tehát egy két Spam gombunk, az egyik mappával, áthúzással, a másik mappa nélkül, titlójellel. Ember legyen a talpán, aki itt kiigazodik.

A következő gomb a lista frissítésére szolgál. Bár ez a funkció nem tűnik túl hasznosnak, nincs vele baj.

A következő gombot most diszkréten átugorjuk, őszintén szólva fogalmam sincs, mire szolgál, sose próbáltam.

Az utolsó gomb funkciója az, hogy az átminősített leveleket szortírozza a helyükre. Átminősítés után ugyanis nem tűnik el a levél a szemünk elől (nagyon helyesen), hanem csupán a "Spam" oszlopban látszik a változás. Az ablak legközelebbi megnyitásakor kerül a helyére. Illetve ezen gomb megnyomására azonnal. Ami feltűnik, hogy az ikon teljesen azonos a frissítéssel, csak más a szine. Vajon milyen módon jelenti a zöld szín a szortírozást, szemben a kék színnel, ami az újraolvasást jelöli? Nem sikerül ebben értelmet látnunk.

A gombsor nagyon egysíkú. Az összes gomb hasonló képi elemekből táplálkozik, amelyek nem segítenek a feladatokról a képekre asszociálni, és viszont. Zavaró az áthúzás és a tiltójel hasonlósága. Ráadásul a jelek használata sem konzekvens.

Mindezt tetézi az, hogy a gombok eltérő hatókörrel bírnak, és ebben sincs semmilyen rendszer. Az első két gomb a lista láthatóságát szabályozza. A következő négy gomb a kiválasztott levélről szól. Ebből az első kettő a levélről magáról szól, a második kettő viszont azok besorolásáról, ami inkább a szűrőkkel függ össze logikailag. A többi gomb listával kapcsolatos funkciókat takar.

Célszerű lett volna a különböző hatókörű gombokat vizuálisan elkülöníteni. Ezt lehet csinálni mérettel és/vagy elhelyezéssel, csoportosítással. Ha a levélre ható gombok jobbra igazítva vannak, vagy a lista oldalán helyezkednek el, egyszerűbb lenne a felhasználó dolga.

Ugyancsak átgondolandó, hogy miért kell a szűrőt ilyen furcsán megvalósítani. Nem lett volna praktikusabb egy "Show" feliratú combo három választással?

Both spam and good
Spam only
Good only

A levelekről szóló gombok hátterébe egy boríték elfért volna.

Meggondolandó lett volna a szemetet jelző áthúzott kör helyett egy behajtani tilos tábla, vagy bármi más, szemléletesebb jelzés. Akár egy bűzölgő kutyagumi képe is jobban kifejezte volna a felhasználó gondolatvilágát.

A szortírozás gomb talán felesleges. Egy szűrőváltással a dolgok kerüljenek a helyükre.

A konkrét megvalósításon és az egyes gombok hasznosságán lehet elmélkedni, de mindenképpen olyan irányban keresgéljünk, ami csökkenti a felület egyhangúságát, és jobban reflektál a különböző funkciók természetes csoportosítására, viszonyára egymással és a többi felületelemmel.


Szál és szál-osztály.

Szál vagy TThread objektum. Kezdő programozónak gyakran nem megy a fejébe elsőre, hogy hogy is vannak ezek a szálak és objektumok egymással. Hajlamosak vagyunk azt hinni, hogy ami a TThread része, az szálon fut, ami meg nem a TThread része, ez nem fut szálon (azaz a fő VCL szálon fut).

Nagyon fontos, és mindenki vésse az eszébe: a kódnak és a szálnak semmi köze egymáshoz! Bármely kódrészlet futhat szálról is és nem szálról is. Attól függ csupán, hogy hívjuk-e onnan. Akármilyen eljárást hívunk meg szálról, az a szálon fog lefutni. Akkor is, ha a fő ablak metódusa. És amit nem szálról hívunk, az pedig nem szálról hívódik, még ha a TThread metódusa is.

Nézzük példaként időrendben, hogy mi történik, ha létrehozunk egy TThread objektumot.

constructor TThread.Create(CreateSuspended: Boolean);
begin
  inherited Create;
  AddThread;
  FSuspended := CreateSuspended;
  FCreateSuspended := CreateSuspended;
  FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), 
    CREATE_SUSPENDED, FThreadID);
  if FHandle = 0 then
    raise EThread.CreateResFmt(@SThreadCreateError, 
      [SysErrorMessage(GetLastError)]);
end;

Emlékeztető: mivel ezt a fő VCL szál hívta meg, így ez is azon fut. A lényeg a BeginThread, amely a delphiben a szálak indítására szolgál. Paraméterben kapja meg az eljárást, amit egy másik szálon akarunk végrehajtani. Kap még egy paramétert, ami ahhoz kell, hogy ama bizonyos eljárásnak üzenni tudjunk valamit. Erre azért van szükség, mert amikor egy szálat létrehozunk, valahogy el kell neki juttatni az adatokat, amivel dolgozni fog. Ha nem is magukat az adatokat, de legalább azt, hogy hol találhatni. Ez akkor válik igazán érdekessé, ha több teljesen egyforma szálat akarunk beindítani, azaz ugyanaz kód fog futni több példányban. Mondjuk ez matematikai alkalmazás, amiben több párhuzamos munkaszál egy egy számot analizál. A szál paramétere például lehet a szám, amit vizsgálnia kell. Jelen esetben maga a TThread objektum lesz ez a paraméter (Self). Mivel a ThreadProc egy sima eljárás, máskülönben nem tudná, hogy melyik TThread objektum indította őt be. Nézzük csak a BeginThreadet.

function BeginThread(SecurityAttributes: Pointer; 
  StackSize: LongWord; ThreadFunc: TThreadFunc; Parameter: Pointer; 
  CreationFlags: LongWord; var ThreadId: LongWord): Integer;
var
  P: PThreadRec;
begin
  New(P);
  P.Func := ThreadFunc;
  P.Parameter := Parameter;
  IsMultiThread := TRUE;
  Result := CreateThread(SecurityAttributes, StackSize, 
    @ThreadWrapper, P, CreationFlags, ThreadID);
end;

A BeginThread tulajdonképpen csak keret a CreateThread körül. A CreateThread az a win32 API hívás, amely paraméterben kapja meg azt az eljárást, amit aztán már egy másik szálról fog futtatni. Tulajdonképpen az eljárás maga lesz a szál élete. Ahogy a megadott eljárás kilép, a szál tevékenysége végetér, és a szál megszűnik.

Észreveendő, hogy a Delphi nem a mi általunk adott eljárást adja tovább, hanem egy saját eljárását (ThreadWrapper), aminek paraméterben adja meg, hogy mit kértünk, és majd az hívja meg. Ez egy csúnya assembly rutin, mindenféle inicializációkat végez, számunkra most nem fontos. Ami fontos, hogy közben meghívja az általunk kívánt eljárást is.

Amire még érdemes figyelni, az a matrjóska rendszerű paraméterátadás. Gyakran előforduló eset, amikor kereteljárásokat írunk valamely funkció köré, hogy az egyetlen csatolható paraméterre nekünk is szükségünk volna, de ugyanakkor kívülről is kapunk egyet, amit továbbítani kell. Jelen esetben a Delphi nem a mi paraméterünket adta át, hanem készített egy kis csomagocskát, amibe beletette a majd valóban hivandó eljárást, és a mi paraméterünket. Majd ezt a csomagocskát (illetve arra mutató pointert) adta át a szállétrehozó API hívásnak. Később majd a ThreadWrapper a csomagból kiszedi, hogy mit kell hívni, és milyen paramétert kell neki továbbítani. A csomagocska felszabadítását is ronda ThreadWrapper fogja csinálni. De most még nem tartunk itt.

A szál életéről a továbbiakban nem tudunk semmit. Valamikor be fog indulni, de hogy az jövő héten lesz, vagy most azonnal, azt nem tudni.

Ezekután gyors ütemben az összes eljárás, amerre jöttünk, visszatér. A hívó fél folytatja a dolgát.

Mindettől függetlenül, valahol a számítógép másik zugában, életre kel egy szál. Ezen belül a windows kialakítja a körülményeket, majd elindítja az általunk (azaz a Delphi által) megadott eljárást, ami a ThreadWrapper. A ThreadWrapper előszedi a tényleg általunk (akarom mondani, a TThread objektum által) adott eljárást, és meghívja a szintén TThreadtől kapott paraméterrel. Így jutunk a ThreadProchoz.

function ThreadProc(Thread: TThread): Integer;
var
  FreeThread: Boolean;
begin
  try
    if not Thread.Terminated then
    try
      Thread.Execute;
    except
      Thread.FFatalException := AcquireExceptionObject;
    end;
  finally
    FreeThread := Thread.FFreeOnTerminate;
    Result := Thread.FReturnValue;
    Thread.FFinished := True;
    Thread.DoTerminate;
    if FreeThread then Thread.Free;
    EndThread(Result);
  end;
end;

Az eljárás paraméterben kapja meg azt az objektumot, ami őt létrehozta. Ezt ugye a TThread.Create adta át a BeginThreadnek, az továbbadta a CreateThreadnek becsomagolva, majd a ThreadWrapper megkapta, kicsomagolta, és most itt van. A fontos számunkra az, hogy meghívódik a TThread.Execute, itt jutottunk tehát el a szál objektum lényegéig. Az Execute tehát szintén a szálon fut, de ezen nem lepődtünk meg, hiszen ez az értelme neki.

A másik említendő a DoTerminate hívás. A DoTerminate hívja meg az OnTerminate eseménykezelőt, na de nem mindegy, hogy hogyan:

procedure TThread.DoTerminate;
begin
  if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
end;

A synchronize-ról majd később. Vonjuk le az első tanulságot:

1. A create metódus a hívó szálon fut, azaz leggyakrabban a fő szálon. Hiába tartozik a TThread objektumhoz.

2. Minden, amit az Execute-ból meghívunk, akár ha az az MyEdit.OnChange is, szálról fut.

Most nézzük azt a Synchronize-t. Ha simán CallOnTerminate szerepelne, akkor az az előbbiek értelmében azt jelentené, hogy szálról fog hívódni az OnTerminate, mintegy hattyúdalként. Hiszen az őt hívó ThreadProc is a szálon fut. Ezen változtat a Synchronize. A Synchronize ugyanis levelet küld a fő szálnak, hogy futtasson már le egy eljárást a kedvünkért. Ezután a szál guru meditációba süllyed. A fő szál, ha akad egy kis ideje, lefuttatja a kedvünkért az eljárást, majd válaszlevelet küld. A válaszlevél megérkeztével a szál feléled, és folytatja a munkáját, jelen esetben a megsemmisülést.

Ismét vigyázzunk! Teljesen mindegy, hogy az eljárás, amit átadtunk, hol van, kié, mié. Akár a TThread objektum egyik metódusát is átadhatjuk, az is a fő szálon fog futni. Mivel a kód, az kód, CPU utasítások sorozata, teljesen mindegy, hogy mely objektumé, mely unitban van, vagy mi a neve, de főleg attól, hogy mit szerettünk volna elérni.

További tanulságok tehát:

3. Az OnTerminate a fő szálon fut.

4. A Synchronize-nak átadott eljárás a fő szálon fut, akármi is az.

Házi feladat.

Adott egy szál objektum, a TWorkerThread. Ennek Execute metódusában az alábbi részletet látjuk:

...
  Synchronize(UpdateForm)
...

Az UpdateForm az alábbi műveletet végzi:

procedure TWorkerThread.UpdateForm;
begin
  FDisplayPanel.Caption := GetWorkProgressDesc;
end;

procedure TWorkerThread.GetWorkProgressDesc;
begin
  Result := PhaseStrings[FPhase] + ' ' + 
    FCurrentObject.Description;
end;

Kérdés: GetWorkProgressDesc függvény szálon fut vagy nem? A helyes válasz egy pontot ér, a helytelen válasz meg négy mázsa virgácsot.

Szorgalmi házi feladat profiknak. Szükséges ismeretek: message loop, ablakok és szálak viszonya.

Legyen a következő TThread implementációnk:

type
  TSomeThread = class(TThread)
  protected
    FReceiver: THandle;
    FStuffs: TThreadList;

    procedure Execute; override;
    procedure MessageHandler(var Msg: TMessage);
    procedure ProcessStuff(Stuff: pointer);
  public
    procedure AddStuff(Stuff: pointer);
    constructor Create;
    destructor Destroy; override;
  end;

constructor TSomeThread.Create;
begin
  FStuffs := TThreadList.Create;
  FStuffs.Duplicates := dupAccept;
  FReceiver := Classes.AllocateHwnd(MessageHandler);
  inherited Create(false);
end;

destructor TSomeThread.Destroy;
begin
  Terminate;
  PostMessage(FReceiver, WM_USER, 0, 0);
  inherited;
  DeallocateHWnd(FReceiver);
end;

procedure TSomeThread.AddStuff(Stuff: pointer);
begin
  FStuffs.Add(Stuff);
  PostMessage(FReceiver, WM_USER, 0, 0);
end;

procedure TSomeThread.Execute;
var
  LockedStuffs: TList;
  Stuff: pointer;
  Msg: TMsg;
begin
  while not Terminated do begin
    GetMessage(Msg, 0, 0, 0);
    DispatchMessage(Msg);

    repeat // checkpoint 1 - we never get here
      LockedStuffs := FStuffs.LockList;
      try
        if LockedStuffs.Count > 0
          then Stuff := LockedStuffs[0]
          else Stuff := nil;
      finally
        FStuffs.UnlockList;
      end;

      if Stuff = nil then break;

      ProcessStuff(Stuff);

      FStuffs.Remove(Stuff);
    until Terminated;
  end;
end;

procedure TSomeThread.MessageHandler(var Msg: TMessage);
begin
  // null
end; // checkpoint 2 - this runs fine

procedure TSomeThread.ProcessStuff(Stuff: pointer);
begin
  sleep(1000); // imitate work
end;

A dolog nem bonyolult, a FStuffs egy lista, amiben tennivalók vannak. Az AddStuff metódussal adhatunk az objektumnak Stuffokat, amiket az majd feldolgoz. Az AddStuff hozzáadja a Stuffot a listához, majd üzenetet küld egy dedikált üzenetfogadó ablaknak. Ez azért lesz hasznos, mert az Execute guru meditációba zuhan mindjárt az elején (GetMessage), és csak akkor ébred fel, ha üzenet jön. Ekkor megnézi, hogy valóban van-e tennivaló, ha van, azokat megcsinálja, majd ismét meditációba zuhan. Így a szál nem fogyaszt semmilyen erőforrást amikor éppen nincs mit tennie. Érdekesség, hogy magával az üzenettel nem kezdünk semmit, elég nekünk az a tény, hogy a GetMessage visszatért.

Tesztelés céljából dobjunk össze egy kis programot, hozzunk létre egy TSomeThread példányt, és akár ott rögtön, akár egy kis idő múlva, vagy akár gombnyomásra adjunk neki AddStuffal valami munkát.

A program nem működik, soha nem jut túl a GetMessage soron. Az a furcsa, hogy ha a checkpoint 2-vel jelölt sorra töréspontot teszünk, akkor a program ott szépen megáll, vagyis az üzenet megérkezik az ablaknak. Ennek ellenére a checkpoint 1-et a program soha nem éri el. Mi lehet az oka?


Javítókulcs

Mostanában volt szerencsém néhány nem annyira professzionális programkódba bepillantást nyerni. Ennek kapcsán jutott eszembe, hogy néhány egyszerű mérőszámot lenne érdemes kitalálni, amik a kód jóságát valamelyest jeleznék. Legyen ez egy afféle ötletelés, szabad eszmefuttatás. Öt mérőszám, amire érdemes odafigyelni kódolás közben.

1. Azon unitok száma, amikhez nem tartozik form (+)

Kis programokra gyakran nem érvényes mérőszám, de az igen valószínű, hogy egy idő után felbukkannak olyan igények, amik külön unit után kiáltanak. Néha valamilyen logika, tárolandó adatstruktúra, interface, máskor csak általános rutinok gyűjteménye, de valami biztosan előkerül, ami nem kapcsolódik a program felületéhez.

Számoljunk minden unitra, ami egyedül áll, egy pontot, és minden datamodule-ra fél pontot.

2. Saját osztályok száma (+)

Egy ideig elnavigál az ember a változók tengerében. Az adatok is jól beleférnek egy StringListbe vagy tömbbe. De hamar megjelenik a rendszerezés igénye.

Minden osztály, ami nem a delphi által generált TForm származék, egy pont. Ha van benne property vagy private/protected rész, akkor két pont. Ha saját osztályból származik, akkor hozzá kell adni az ős pontszámát. Absztrakt osztály kétszeres pontszám.

3. Eljárások száma, amik nem eseménykezelők (+)

Kis egyszerű ablakoknál még megy az, hogy közös eseménykezelőket használunk, ha ugyanaz a tennivaló. Esetleg egyikből hívjuk a másikat, bár ez már kezd szagos lenni. De eljön az a pont (illetve van, akinél nem jön el), amikor a funkciókat elkezdjük dekomponálni, darabokra vágni, és a darabokat felhasználni ahol kell.

Minden olyan eljárás plusz egy pont, ami nem eseménykezelője semmilyen komponensnek. Mindegy, hogy közönséges eljárás vagy osztálymetódus.

4. Változók száma, amiből nem derül ki elemzés nélkül, hogy mit tartalmaz (-)

Példák: szn, a, h1, S, List.

Lehet persze a szokásos kibúvóval érvelni: a kódból egyértelmű, hogy mit jelent, tessék megérteni. Csakhogy neked két másodperc lett volna leírni egy rendes nevet, nekem meg esetleg percekig tart megérteni az algoritmust.

Minden ilyen változónév minusz egy pont. Osztálydefinícióban minusz kettő. Public szekcióban vagy globális változóként minusz három. Ha utána van egy megjegyzés, ami magyarázza a jelentését, akkor az érték kétszereződik! Mindenki minuszból indul a szokványos i, j, k ciklusváltozók miatt, ez benne van a pakliban. Eljárásparaméterek is számítanak.

5. Globális változók száma (-)

Kétségtelen, hogy vannak a program egészére vonatkozó értékek. Konfiguráció, napló, jogosultság-kezelő objektum, központi ikontár, adatbáziskapcsolat programonként jellemzően egy van, és kár lenne egymásnak passzolni őket a program egyes részei között. De ebből kevés van. Ha mégis sok lenne, akkor az csoportosítandó oszályokba, vagy DataModulokba.

Minusz egy pont mindegyik. Itt is valószínűleg minuszból indul mindenki, de ez nem baj. Az igazi kaszabolók beelőznek a 40-50 globális változójukkal.


OnClick

Fórumokon gyakori kérdés, hogy miért nem mondja meg az OnClick esemény, hogy melyik egérgombot nyomták le, illetve a control/shift gombok állapotát hogy lehet lekérdezni. Azért nem lehet ezeket lekérdezni, mert az OnClick nem egéresemény. Bizony nem! Na de akkor miért OnClick a neve? És mi az valójában?

Definíció helyett nézzük, hogy milyen esetekben hívódik az OnClick (a teljesség igénye nélkül):

TButton:

  • egérrel rákattintunk, és még a gomb felett fel is engedjük
  • fókuszált gombon szóközt nyomunk
  • fókuszált gombon entert nyomunk
  • a gomb az alapértelmezett (Default = true), és entert nyomunk
  • a gomb egy mégsem gomb (Cancel = true), és escape-et nyomunk
  • Alt+billentyű kombinációt nyomtunk
  • programból meghívtuk a gomb Click metódusát

TCheckBox:

  • egérrel rákattintunk, és még a rajta fel is engedjük
  • fókuszált állapotban szóközt nyomunk
  • Alt+billentyű kombinációt nyomtunk
  • programból átállítjuk az állapotot (State)

A fentiekből világos, hogy ez egy absztrakt esemény. A gomb esetén azt jelenti: "a felhasználó a gombhoz kapcsolt funkciót szeretné kérni". CheckBox esetén azt jelzi, hogy megváltozott az állapot (azonos az OnChange-dzsel). Semmi értelme annak a kérdésnek, hogy melyik egérgombbal nyomta meg a felhasználó az Alt-H kombinációt.

Az egyetlen kérdés már csak az, hogy miért OnClick a neve.

Csak.


Régebbiek | Végére »

balinto 2006