مقاله فنی

تشخیص‌های ساختاریافته به‌جای نتایج بولی در HotXLS

یک تبدیل دسته‌ای را روی ده‌هزار صفحه‌گسترده در طول شب اجرا کنید، و صبح سه‌تا از آن‌ها False برمی‌گردانند. این کل کالبدشکافی‌ای است که یک نتیجه‌ی بولی save به شما می‌دهد: یک شمارش از شکست‌ها، بدون هیچ چیزی درباره‌ی اینکه کدام فایل، کدام برگ، یا کدام‌یک از ده‌ها علت ممکن مسئول بوده. HotXLS، کامپوننت بومی losLab برای فایل‌های Excel در Delphi و C++Builder، آن یک بیت را با تشخیص‌های ساختاریافته جایگزین می‌کند. اینترفیس IXLSWorkbookProgress یک فهرست Diagnostics و یک رویداد OnDiagnostic را در معرض دید می‌گذارد که یک کد عددی پایدار، یک سطح شدت، عملیاتی که شکست خورده، و برگی که اتفاق افتاده را برای هر فراخوانی Open، SaveAs، و Recalculate گزارش می‌دهد

چرا یک نتیجه‌ی بولی save در مقیاس شکست می‌خورد؟

یک فایل شکست‌خورده‌ی تکی مسئله‌ای نیست که یک نتیجه‌ی بولی ایجاد می‌کند؛ هزار تای آن‌ها هست. وقتی SaveAs برای سه فایل از ده‌هزار تا چیزی غیر از موفقیت برمی‌گرداند، سؤال بعدی همیشه یکی است: آیا این سه‌تا قابل‌تلاش‌مجدد هستند، یا به یک انسان نیاز دارند؟ یک خطای مجوز روی یک اشتراک شبکه همان رخداد یک فرمولی که موتور محاسبه نمی‌تواند ارزیابی کند نیست، و هیچ‌کدام هم همان یک برگ‌کاری که بی‌سروصدا از یک محدودیت قالب فراتر رفته نیست. با فقط یک نتیجه‌ی موفق/شکست برای کار‌کردن، هر یک از این‌ها به یک تیکت پشتیبانی یکسان تبدیل می‌شود، و کسی باید هر فایل را دستی، در اکسل، باز کند و به آن خیره شود تا علت آشکار شود. آن triage دستی هزینه‌ی واقعی یک API بولی است، و به‌طور خطی با اندازه‌ی دسته مقیاس می‌گیرد، که دقیقاً همان ویژگی‌ای است که از مدیریت خطا نمی‌خواهید

درون IXLSWorkbookProgress: یک TXLSDiagnostic چه چیزی حمل می‌کند

IXLSWorkbookProgress اینترفیسی است که HotXLS برای گزارش هم پیشرفت یک عملیات و هم اینکه چه چیزی درون آن اشتباه رفته استفاده می‌کند، و این دو نیمه یک قرارداد را به یک دلیل به اشتراک می‌گذارند: هر دو چیزهایی هستند که یک فراخوانی طولانی Open، SaveAs، یا Recalculate باید بدون raise‌کردن یک استثنا در میانه‌ی عملیات ارتباط دهد. نیمه‌ی پیشرفت OnProgress و OnProgressEx است، که با یک فاز، یک وضعیت، و یک جفت فعلی/کل شلیک می‌شود. نیمه‌ی تشخیص همانی است که این مقاله درباره‌ی آن است: یک ویژگی Diagnostics که یک فهرست TXLSDiagnostics برمی‌گرداند، یک میان‌بر LastDiagnostic برای آخرین ورودی، و یک رویداد OnDiagnostic که همان لحظه‌ای که هر رکورد TXLSDiagnostic ساخته می‌شود شلیک می‌شود. هر رکورد یک Code عددی، یک TXLSDiagnosticSeverity، TXLSDiagnosticOperationای که آن را تولید کرده، یک Message قابل‌خواندن انسانی، یک SheetIndex و SheetName، و یک NativeCode که هر مقدار بازگشتی سطح‌پایین‌تری که آن ورودی را ماشه کشیده را حفظ می‌کند، حمل می‌کند

var
  Book: TXLSXWorkbook;
  Diag: TXLSDiagnostic;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.SaveAs('quarterly-report.xlsx') <> 1 then
      for I := 0 to Book.Diagnostics.Count - 1 do
      begin
        Diag := Book.Diagnostics[I];
        Writeln(Format('[%d] severity=%d sheet="%s": %s',
          [Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
      end;
  finally
    Book.Free;
  end;
end;

خواندن Diagnostics به این شکل به‌تنهایی از یک نتیجه‌ی بولی جلو می‌زند، چون Code و SheetName یک معما را به یک واقعیت مشخص و قابل‌فیلترکردن تبدیل می‌کنند. رکورد TXLSDiagnostic فراتر از آنچه این مثال چاپ می‌کند می‌رود: RecordId و StreamOffset برای پزشکی‌قانونی در سطح بایت درون یک جریان BIFF وجود دارند، و PartName ورودی zip از OOXML، مثل xl/worksheets/sheet3.xml، را که یک مسئله از آن آمده حمل می‌کند. ارزش دانستن پیش از اینکه ابزارسازی حول آن‌ها بسازید: در نسخه‌ی فعلی هیچ‌کدام از نقاط فراخوانی تشخیصی توکار RecordId یا StreamOffset را پر نمی‌کنند، پس هر دو در پیش‌فرض سازنده‌شان یعنی -1، به‌معنای «قابل‌اعمال نیست» نه «صفر»، باقی می‌مانند. نبودشان را به‌عنوان عادی درنظر بگیرید، نه یک باگ در handler خودتان

دو موتور، یک شکل، یک تفاوت خاموش

HotXLS دو موتور را پشت همین مدل گزارش‌دهی عرضه می‌کند، یک نمای BIFF8 برای فایل‌های قدیمی .xls و یک نمای OOXML برای .xlsx، و آن‌ها IXLSWorkbookProgress را یکسان در معرض دید نمی‌گذارند. TXLSWorkbook، موتور .xls، رسماً IXLSWorkbookProgress را پیاده‌سازی می‌کند، پس می‌تواند به هرجایی که آن نوع اینترفیس انتظار می‌رود پاس داده شود. TXLSXWorkbook، موتور .xlsx، همان اعضای Diagnostics، LastDiagnostic، OnDiagnostic، OnProgress، و OnProgressEx را با نام‌ها و انواع یکسان در معرض دید می‌گذارد، اما به‌عنوان یک کلاس ساده به‌جای یک پیاده‌سازی رسمی از آن اینترفیس، پس یک پارامتر IXLSWorkbookProgress را به‌تنهایی برآورده نمی‌کند. در عمل این به‌ندرت اهمیت دارد، چون اغلب کد در هر زمان با یک کلاس کاربرگ عینی کار می‌کند، اما به این معناست که نمی‌توانید یک کمکی تکی از نوع IXLSWorkbookProgress بنویسید و شیء کاربرگ هرکدام از موتورها را به‌جای هم به آن بسپارید. تنها تفاوت فیلدی که مستقیماً از تفکیک قالب پیروی می‌کند PartName است: فقط موتور XLSX آن را پر می‌کند، چون فقط OOXML بخش‌های zip برای نام‌بردن دارد

چه چیزی یک کد تشخیصی را چیزی می‌کند که بتوانید ایمن روی آن شاخه بزنید؟

فیلد Code تنها بخشی از یک تشخیص است که ارزش hardcode کردن یک مقایسه در برابرش را دارد؛ Message این‌طور نیست، چون نثر دقیقاً همان چیزی است که در یک نسخه‌ی بعدی بدون اینکه کسی آن را به‌عنوان یک تغییر شکننده در نظر بگیرد، دوباره‌عبارت‌بندی، دوباره‌ترجمه، یا با جزئیات بیشتر گسترش می‌یابد. کدهای تشخیصی توکار HotXLS از پیش طوری خوانده می‌شوند که انگار با در نظر گرفتن آن تمایز طراحی شده‌اند: کدهای مرتبط با save از ۱۰۰۰ تا ۱۰۰۵ اجرا می‌شوند، کدهای مرتبط با open در ۱۱۰۰ و ۱۱۰۱ می‌نشینند، کدهای مرتبط با calculate در ۱۲۰۰ و ۱۲۰۱، و یک کد قالب-پشتیبانی‌نشده در ۱۳۰۰، با شکاف‌هایی رها‌شده درون هر باند به‌جای اینکه کدها در سراسر همه‌ی آن‌ها متوالی اجرا شوند. آن فاصله‌گذاری چیزی است که اجازه می‌دهد یک فروشنده یک حالت شکست جدید در زمان save را، مثلاً در ۱۰۰۶، بدون دوباره‌شماره‌گذاری کدهایی که دستور switch شما از پیش به آن‌ها وابسته است، اضافه کند، و ارزش بررسی‌کردن در هر API تشخیصی‌ای دارد پیش از اینکه به تطبیق روی یک کد در تولید متعهد شوید، نه فقط این یکی. صرف‌نظر از اینکه شماره‌گذاری چقدر پایدار به‌نظر می‌رسد، یک شاخه‌ی پیش‌فرض را در منطق dispatch خودتان نگه دارید، چون حالت‌های شکست جدید دقیقاً همان چیزی هستند که یک تجزیه‌گر یا نویسنده‌ی در حال تکامل دائماً کشف می‌کند. NativeCode و ExceptionClass یک لایه زیر Code می‌نشینند برای زمانی که نیاز به تشدید دارید: NativeCode مقدار بازگشتی زیرین را حفظ می‌کند، یک HRESULT از یک فراخوانی Structured Storage از جمله آن‌ها، و ExceptionClass نوع استثنای Delphi را ثبت می‌کند وقتی یکی درگیر بوده، که معمولاً برای بازکردن یک درخواست پشتیبانی دقیق بدون پیوست‌کردن یک stack trace کامل کافی است

شدت و عملیات تصمیم می‌گیرند کد شما بعداً چه کاری انجام دهد

شدت و عملیات چیزهایی هستند که یک تشخیص را از یک خط لاگ به یک تصمیم مسیریابی تبدیل می‌کنند. TXLSDiagnosticSeverity شامل Info، Warning، Error، و Fatal اجرا می‌شود، و TXLSDiagnosticOperation هر ورودی را با فراخوانی‌ای که آن را تولید کرده تگ می‌زند: Open، Save، Calculate، یا Export. این دو محور از نظر طراحی مستقل هستند: xlsDiagnosticUnhandledException یک کد ثابت تکی است که با Operation تنظیم‌شده روی هر فراخوانی‌ای که واقعاً آن را raise کرده شلیک می‌شود، پس Code پاسخ می‌دهد چه چیزی اشتباه رفته درحالی‌که Operation جداگانه پاسخ می‌دهد کجا، به‌جای اینکه به یک کد متمایز برای یک استثنا در طول open در برابر یکی در طول save نیاز داشته باشد. آن ترکیب‌پذیری چیزی هم است که مسیریابی را مکانیکی می‌کند: یک هشدار را لاگ کنید و ادامه دهید، یک save لغوشده از طریق پرچم Aborted یک مثال معمولی است؛ یک خطا را بشمارید و دسته را در حال اجرا نگه دارید، یک برگ‌کاری که در serialize‌کردن شکست خورده یک مثال معمولی است؛ دسته را روی یک شدت fatal متوقف کنید، چون آن سطح یعنی یک استثنای مدیریت‌نشده از پیش فراخوانی را باز کرده و ادامه‌دادن ریسک کار‌کردن از یک وضعیت نیمه‌به‌روزشده را دارد. یک احتیاط صادقانه: Info در enum به‌عنوان پیش‌فرضی که یک TXLSDiagnostic تازه با آن شروع می‌شود وجود دارد، اما هر نقطه فراخوانی تشخیصی که در نسخه‌ی امروز HotXLS ساخته شده فقط تا به‌حال Warning، Error، یا Fatal raise می‌کند؛ Info برای استفاده‌ی آینده رزرو شده، نه چیزی که موتور امروز منتشر می‌کند

// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
  Diag := Book.Diagnostics[I];
  case Diag.Severity of
    xlsDiagnosticWarning:
      Writeln(Format('WARN  [%d] %s', [Diag.Code, Diag.Message]));
    xlsDiagnosticError:
      begin
        Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
          [Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
        Inc(FailedSheetCount);
      end;
    xlsDiagnosticFatal:
      raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
  end;
end;

سیم‌کشی OnDiagnostic به یک خط لوله‌ی دسته‌ای

Poll‌کردن Diagnostics پس از هر فراخوانی برای یک فایل تکی کار می‌کند؛ این کار می‌ایستد به‌محض اینکه به آن دسته‌ی شبانه‌ی ده‌هزار فایل برگردید، چون Diagnostics در ابتدای هر فراخوانی Open، SaveAs، و Recalculate پاک می‌شود. آن را پس از سومین فایل در یک حلقه بخوانید و فقط تشخیص‌های فایل سوم را می‌بینید؛ هر چیزی که دو فایل اول گزارش داده‌اند از پیش رفته. OnDiagnostic این را با تبدیل مجموعه به یک جریان حل می‌کند: یک‌بار پیش از شروع حلقه مشترک شوید، و همان handler برای هر فایل، به‌ترتیب، با نام فایل هنوز در دامنه از طریق یک فیلد نمونه، شلیک می‌شود

type
  TBatchConverter = class
  private
    FCurrentFile: string;
    FFailedFiles: TStringList;
    procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
  end;

procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
  if Diagnostic.Severity >= xlsDiagnosticError then
    FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
      [FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;

// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
  FCurrentFile := FileNames[I];
  if Book.Open(FCurrentFile) = 1 then
    Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;

callback واقعاً چقدر هزینه دارد

OnDiagnostic به دلیلی ساختاری ارزان است: فقط زمانی شلیک می‌شود که چیزی از پیش اشتباه است، و اشتباه در مقایسه با تعداد سلول‌ها، ردیف‌ها، یا برگ‌کاری‌هایی که یک کاربرگ در اختیار دارد نادر است. آن را با OnProgress و OnProgressEx مقایسه کنید، که پیشرفت معمول را گزارش می‌دهند و از ابتدا باید حول فرکانس فراخوانی طراحی می‌شدند. HotXLS پیشرفت در سطح برگ‌کاری را یک‌بار به‌ازای هر برگ در طول Open و SaveAs شلیک می‌کند، نه یک‌بار به‌ازای هر سلول یا ردیف، که همان چیزی است که سربار به‌ازای هر فراخوانی را حتی روی کاربرگ‌هایی با میلیون‌ها سلول کوچک نگه می‌دارد؛ Recalculate فراتر می‌رود و رویداد پیشرفت خودش را به‌طور تقریبی هر چهار درصد از گراف وابستگی محدود می‌کند، پس یک محاسبه‌ی مجدد کامل به شما یک ضربان قلب می‌دهد به‌جای اینکه رشته‌ی UI شما را با رویدادها سیل‌آسا کند. تشخیص‌ها هیچ‌کدام از آن محدودسازی را لازم نداشتند، چون شمار رویداد به تعداد مسائل واقعی محدود است، نه به اندازه‌ی فایل

تنها جایی که کارایی همچنان به شما بستگی دارد درون خودِ handler است. OnDiagnostic به‌طور همزمان شلیک می‌شود، روی رشته‌ای که Open، SaveAs، یا Recalculate را اجرا می‌کند، پس یک handler که مسدود می‌شود، مثلاً یک نوشتن همزمان به یک سرویس لاگ‌گیری از راه دور، بخشی از زمان دیواری آن فراخوانی می‌شود. برای یک فایل تکی این نامرئی است. ضرب‌شده در سراسر یک دسته‌ی ده‌هزار فایلی، این تفاوت بین یک job است که شبانه تمام می‌شود و یکی که هنوز موقع ناهار در حال اجراست، پس آنچه handler باید انجام دهد را بافر کنید و آن را به‌صورت ناهمزمان flush کنید به‌جای اینکه بخش کند را درون‌خط انجام دهید

تشخیص‌های ساختاریافته دقیقاً همان‌جایی باارزش‌ترین هستند که یک نتیجه‌ی بولی ضعیف‌ترین است، در گردش‌کارهایی که به‌جای یکی، چندین فایل را لمس می‌کنند. یک خط لوله‌ی ممیزی و تبدیل کاربرگ روشن‌ترین مثال است: به‌جای ثبت یک موفق/شکست ساده به‌ازای هر فایل، فهرست Diagnostics هر فایل را به رکورد ممیزی‌اش پیوست کنید، و گزارش نه‌تنها می‌گوید چه چیزی شکست خورده بلکه چرا، که بیشتر همان چیزی است که مقاله‌ی ما درباره‌ی ساخت یک کارگاه ممیزی و تبدیل کاربرگ اصلاً تلاش می‌کند درست انجام دهد. همان جفت‌کردن پیشرفت و تشخیص هم متعلق به هر گردش‌کاری است که از پیش به گزارش پیشرفت به‌خاطر خودش نیاز دارد، که دقیقاً همان قلمروی است که در راهنمای ما درباره‌ی کارایی کاربرگ بزرگ در HotXLS پوشش داده شده، جایی که یک فراخوانی طولانی Open یا SaveAs به‌اندازه‌ی کافی معمول است که OnProgress از پیش سیم‌کشی شده و OnDiagnostic یک افزوده‌ی طبیعی و تقریباً رایگان کنارش است

هیچ‌کدام از این‌ها به نصب اکسل جایی در خط لوله نیاز ندارد، و هیچ‌کدام به گرفتن یک استثنای عمومی و حدس‌زدن معنایش نیاز ندارد. IXLSWorkbookProgress و اعضای Diagnostics، LastDiagnostic، و OnDiagnosticاش بخشی از کامپوننت استاندارد HotXLS برای Delphi و C++Builder هستند، در کنار مرجع کامل کد تشخیصی و بقیه‌ی سطح Open، SaveAs، و Recalculateای که این مقاله در حال گذشتن از آن بوده