Műszaki cikk

Egy THotPDF példány újrahasználata dokumentumok között Delphi-ben

A hiba így szól: Please load the document before using BeginDoc, és szinte mindig a második alkalommal jelenik meg. Az első dokumentum rendben íródik. Majd ugyanazt a THotPDF példányt megkérjük egy második elindítására, a BeginDoc kivételt (raises) dob, és az üzenet egy dokumentum betöltésére mutat, ami pont az ellenkezője annak, amit a kód próbál tenni. A tünet és az üzenet közötti eltérés (mismatch) az, ami miatt ez megragad. A valódi téma a komponens életciklusa (lifecycle), és amint ez a helyére kerül, a hiba többé nem lesz rejtélyes

THotPDF dokumentum életciklusa (lifecycle), amely a Create, BeginDoc, EndDoc és Free hívásokat mutatja kimeneti fájlonként
Egy THotPDF példány (instance) egy dokumentumra képeződik le: Create, BeginDoc, rajzolás (draw), EndDoc, Free.

Egy THotPDF példány (instance) egy dokumentum, nem pedig dokumentumgyár

A csábító mentális modell az, hogy a THotPDF egy szolgáltatás-objektum (service object), amelyet egyszer indít el (spin up), és dokumentumokkal táplál, ahogyan egy adatbázis-kapcsolatot (database connection) nyitva tarthat, és lekérdezést (query) lekérdezés után futtathat rajta keresztül. Ez nem így van. Egy példány egyetlen építés alatt álló dokumentumot modellez, és a belső állapotgépe (state machine) azt a feltételezést hordozza, hogy egyszer járja be az utat: az üresből, egy nyitott dokumentumon keresztül, egy mentett fájlig. A BeginDoc megnyitja ezt az utat, és megjelöli a példányt úgy, hogy folyamatban lévő dokumentummal (document in progress) rendelkezik. Az EndDoc mindent szerializál (serializes) a FileName-be, és lezárja azt. A BeginDoc ismételt meghívása ugyanazon a befejezett példányon arra kéri, hogy lépjen be újra egy olyan állapotba, amelyet soha nem hagyott el tisztán, és a védőfeltétel (guard), amely működésbe lép, az, amelynek üzenete véletlenül a betöltést említi, mivel belsőleg a "kész az indításra" (ready to begin) és a "van egy betöltött dokumentuma" (has a loaded document) feltételeket együtt ellenőrzik

Tehát az üzenet félrevezető, de a védőfeltétel (guard) teszi a dolgát. Megtagadja (refusing), hogy egy új dokumentumot indítson el egy olyan komponens tetején, amely még mindig azt hiszi, hogy egy dokumentum közepén (mid-document) tart. A javítás nem az, hogy legyőzi a védelmet (guard). Hanem az, hogy felhagy egy elhasznált (spent) példány újrahasználatával

Az életciklus abban a sorrendben, ahogyan annak történnie kell

Minden dokumentum, amelyet a HotPDF az alapoktól (from scratch) ír, ugyanazt a négy ütemet követi, és a sorrend nem képezheti alku tárgyát (not negotiable). A Create lefoglalja (allocates) a komponenst. A BeginDoc megnyitja a dokumentumot és rögzíti a szerkezeti választásokat, így bármit, ami a teljes fájlt érinti (oldalméret, tömörítés, titkosítás, kimeneti fájlnév), a Create és a BeginDoc között kell beállítani. Aztán rajzol (draw). Majd az EndDoc kiírja a bájtokat (bytes) a lemezre. A Free felszabadítja a példányt. A BeginDoc elé helyezett rajzolási hívásoknak nincs hol landolniuk (no page to land on); az utána hozzárendelt, az egész dokumentumra vonatkozó tulajdonságokat panasz nélkül figyelmen kívül hagyja

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // opens the document
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // writes invoice.pdf, closes it out
  finally
    Pdf.Free;                            // one instance, one document
  end;
end;

Olvassa ezt úgy, mint a munkaegységet (unit of work). Egy Create, egy BeginDoc, egy EndDoc, egy Free, egy fájl a lemezen. Abban a pillanatban, amikor egy második fájlt szeretne, egy új munkaegységet indít, ami egy új példányt (new instance) jelent

Mit is kellene jelentenie a "újrahasználatnak": egy új példány fájlonként

Az a verzió, amelyik eltörik (breaks), megpróbál takarékos lenni a foglalással (allocation): egyszer megépíti a komponenst, végigiterál egy kötegen (loop over a batch), meghívja a BeginDoc és az EndDoc metódusokat a cikluson (loop) belül. A második iteráció (iteration) kivételt dob (throws). Az a verzió, amelyik működik, minden kimenetet (output) saját, rövid élettartamú (short-lived) objektumként (object) kezel, és egy komponens létrehozásának foglalási költsége (allocation cost) elhanyagolható egy PDF elrendezésének (laying out) és szerializálásának (serializing) munkája mellett, így semmit nem spórol a példány (instance) felhalmozásával (hoarding)

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // new instance each pass
    try
      Pdf.FileName := Names[I] + '.pdf';
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('Arial', [], 12);
      Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
      Pdf.EndDoc;
    finally
      Pdf.Free;
    end;
  end;
end;

A cikluson belül elhelyezkedő try/finally az a rész, amelyet érdemes megvédeni a kódellenőrzés (review) során. Ha a BeginDoc vagy bármilyen rajzolási hívás (drawing call) kivételt dob (raises) egy dokumentum közepén, az adott iteráció példánya továbbra is felszabadul (freed), mielőtt a következő elkezdődne, így egyetlen hibás rekord nem hagy magára egy félig felépített (half-built) komponenst, és nem mérgezi meg (poison) a futás (run) hátralévő részét. Húzza ki a Create-et a ciklus (loop) fölé az "optimalizáláshoz", és máris visszatért az eredeti hibához (bug), amely most egy kötegelt ciklust (batch loop) visel

Egy meglévő fájl módosítása egy másik belépési pont

Létezik az "újrahasználatnak" egy második olvasata is, amely teljesen legitim: nem egy üres dokumentumot (blank document) akar, hanem egy már létező PDF-et akar megnyitni és megváltoztatni. Ez az út egyáltalán nem megy keresztül a BeginDoc-on, ami pontosan az oka annak, hogy a hibaüzenet a betöltést nevezi meg. Ön betölti a fájlt (load), szerkeszti (edit), és elmenti (save) azon a néven, amilyen néven csak akarja

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('contract.pdf');
    if PageCount > 0 then
    begin
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
      Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
      Pdf.SaveLoadedDocument('contract-reviewed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

A LoadFromFile visszaadja az oldalszámot (page count), és a nulla vagy az alatti érték azt jelenti, hogy a betöltés nem sikerült, ezért érdemes ellenőrizni, mielőtt a CurrentPage-hez ér. A párosítás (pairing) számít: a LoadFromFile segítségével megnyitott dokumentumot a SaveLoadedDocument segítségével menti el, nem pedig a BeginDoc/EndDoc párral, amely azokhoz a dokumentumokhoz tartozik, amelyeket a semmiből készít (author from nothing). A kettő keverése a leggyakoribb módja annak, hogy összezavarja ugyanazt az állapotgépet (state machine), amely az eredeti hibát (error) produkálta. Tartsa a két folyamatot (flows) mentálisan külön: a BeginDoc ... EndDoc létrehoz (creates), a LoadFromFile ... SaveLoadedDocument szerkeszt (edits)

A fájlzárolási probléma valós, és a válasz nem a megjelenítő ablakok kilövése

Az újrahasználati hiba (reuse error) gyakran együtt jár egy második panasszal, és a kettő összegabalyodik, mert ugyanabban a fájl újragenerálási (regenerate-the-file) munkafolyamatban merülnek fel. A felhasználó megnyitja az Ön által épp elkészített PDF-et, nyitva hagyja az Acrobatban vagy a Foxitban, majd elindít (triggers) egy újraépítést (rebuild). Az EndDoc megpróbálja megírni ugyanazt az elérési utat (path), az operációs rendszer (operating system) ezt megtagadja (refuses), mert a megjelenítő (viewer) egy olyan olvasási megosztással (read share) rendelkezik, amely blokkolja az írókat, és Ön egy hozzáférés-megtagadási (access-denied) hibát kap. Ez valóban egy Windows fájlzárolási (file-locking) probléma, nem pedig egy komponensállapot (component-state) probléma, és igazi választ (real answer) érdemel egy kerülőút (workaround) helyett

Az a kerülőút (workaround), amely a felső szintű ablakok (top-level windows) felsorolásáról és a WM_CLOSE küldéséről szól bárminek, aminek a címe PDF megjelenítőre hasonlít, egy rossz ösztön. A folyamathatárokon (process boundaries) átnyúlva zár be olyan ablakokat, amelyeket a programja (program) nem birtokol (not own), a megjelenítőket (viewers) a címszöveg (title text) alapján találgatja meg, és kérdés nélkül eldobhatja a felhasználó nem mentett annotációit (unsaved annotations). Kezelje ezt az egész megközelítést kódszagként (smell). A megbízható javítás az, hogy soha nem írunk olyan útvonalra (path), amelyet egy másik folyamat (process) birtokolhat. Szerializálja (Serialize) egy ideiglenes fájlba (temporary file) ugyanabban a könyvtárban, majd cserélje (swap) a helyére egy atomi átnevezéssel (atomic rename), miután az EndDoc sikerült. Ha egy megjelenítő (viewer) még mindig nyitva tartja a régi fájlt, az átnevezés (rename) vagy tisztán sikerül, vagy hangosan megbukik, és Ön egy egyértelmű üzenetet jelenít meg (surface) a zárolással (lock) való harc helyett

Egy nagy forgalmú szerver esetében (high-volume server), amely folyamatosan (constantly) újragenerálja a dokumentumokat, a tisztább fegyelem (discipline) az, hogy minden kimenetet egyedi néven (például egy időbélyeg (timestamp) vagy egy feladatazonosító (job id)) írunk, így két futás (runs) soha nem verseng (contend) egy útvonalért, és egy külön megőrzési házirend (retention policy) törli (clean up) a régi fájlokat. Bárhogy is legyen, az alapelv (principle) ugyanaz: tervezzen úgy, hogy a fájl, amelyet éppen ír, kizárólag az Öné legyen abban a pillanatban, amikor írja. A zárolás (lock) nem azért tűnik el, mert Ön kikényszerítette (forced) egy ablak bezárását, hanem azért, mert semmi más nem érinti a bájtokat

A javítás formája

Ha a két problémát a gyökereikig (roots) lecsupaszítjuk (strip), mindkettő a határok (boundaries) tiszteletben tartásáról szól. Az állapotgép-hiba (state-machine error) azt várja el, hogy tartsa tiszteletben a példány határát (instance boundary): egy THotPDF, egy dokumentum, majd engedje el, és készítsen egy másikat. A fájlzárolási (file-lock) hiba azt várja el, hogy tartsa tiszteletben a fájl határát (file boundary): írjon oda, ahol semmi más nem olvas, majd mozgassa (move) az eredményt a helyére. Egyik sem követeli meg (calls for) a könyvtár foltozását (patching the library) vagy az asztal szkriptelését (scripting the desktop). Mindkettő abból a megközelítésből fakad (fall out), hogy minden dokumentumot önálló munkaegységként (self-contained unit of work) kezelünk, frissen létrehozva (created fresh), tisztán megírva (written cleanly) és elengedve (released), ami ugyanaz a minta (pattern), amely a komponens többi részét is kiszámíthatóvá teszi

A BeginDoc, EndDoc, LoadFromFile és SaveLoadedDocument hívások, amelyek itt bemutatásra kerültek, a HotPDF Component részét képezik Delphi és C++Builder esetében

A frissített rész hangsúlyozza, hogy a THotPDF egy dokumentum életciklusát képviseli, minden fájlhoz új példány kell, a meglévő fájl módosítása pedig külön belépési pont