هیدریشن اشیاء تا زمانی که آن را اندازه‌گیری نکنید، مسئله‌ای حل‌شده به نظر می‌رسد. شما یک ردیف از پایگاه داده واکشی می‌کنید، آرایه را به یک شیء نگاشت (map) می‌کنید و به کار خود ادامه می‌دهید. اکثر ما با آن مثل لوله‌کشی برخورد می‌کنیم؛ چیزی نامرئی، بی‌جذابیت و با این فرض که به اندازه کافی سریع است. سپس یک روز، یک کار دسته‌ای (batch job) یا یک ورکر صف (queue worker) را پروفایل می‌کنید و متوجه می‌شوید که بخش قابل توجهی از زمان CPU در مپر (mapper) هدر می‌رود. این درک دقیقاً همان چیزی بود که منجر به ساخت HydraType شد؛ هیدراتوری که بر اساس یک قانون سرسختانه ساخته شده است: یک کلاس هرگز نباید هزینه ویژگی‌هایی را بپردازد که پراپرتی‌هایش از آن‌ها استفاده نمی‌کنند.

اگر یک فیلد، یک رشته ساده است که به هیچ تبدیلی نیاز ندارد، کد تولیدشده باید طوری به نظر برسد که انگار یک انسان آن را با دست نوشته است. انتساب مستقیم. بدون حلقه، بدون پایپ‌لاین، بدون ریفلکشن.

هیدریشن در واقع چه هزینه‌ای دارد

در نگاه اول، تبدیل ['name' => 'Alice', 'age' => 30] به یک شیء User ساده به نظر می‌رسد. مشکل زمانی شروع می‌شود که به دنبال انعطاف‌پذیری باشید. اکثر هیدراتورهای چندمنظوره برای بررسی کلاس‌ها در زمان اجرا (runtime) به ریفلکشن متکی هستند. آن‌ها نقشه‌های متادیتا می‌سازند، تبدیل نوع (type coercion) را مدیریت می‌کنند و گراف‌های شیء تو در تو را حل می‌کنند. آن‌ها همچنین عاشق پایپ‌لاین‌ها هستند. هر مقدار از میان زنجیره‌ای از تغییردهنده‌ها (mutators)، تأییدکننده‌ها (assertions) و تبدیل‌کننده‌ها (transformers) عبور می‌کند، حتی اگر آن زنجیره خالی باشد. ساختار حلقه به خودی خود باعث ایجاد سربار می‌شود.

اگر چند ده رکورد را واکشی کنید، هرگز متوجه نخواهید شد. اما اپلیکیشن‌های مدرن PHP به‌طور معمول هزاران پیام صف را پردازش می‌کنند، مجموعه‌های عظیم CSV را وارد می‌کنند یا مجموعه‌نتایج عمیق را از Elasticsearch هیدراته می‌کنند. در این سناریوها، مپری که حتی چند میکروثانیه برای هر فیلد مصرف کند، به یک گلوگاه واقعی تبدیل می‌شود. هزار شیء که هر کدام هشت فیلد دارند، به مالیاتی تبدیل می‌شود که اثر آن را به وضوح حس خواهید کرد.

فلسفه بدون سربار (Zero-Overhead)

ایده اصلی پشت HydraType، جداسازی ویژگی‌ها است. تبدیل نوع، هیدریشن اشیاء تو در تو و تأییدکننده‌ها همگی پشتیبانی می‌شوند، اما هیچ‌کدام از آن‌ها اجباری نیستند. اگر یک پراپرتی به چیزی جز یک کپی مستقیم از آرایه به شیء نیاز نداشته باشد، نویسنده (writer) تولیدشده دقیقاً همان را شامل می‌شود. هیچ پایپ‌لاین مشترکی وجود ندارد که یک فیلد اسکالر ساده را مجبور کند در صف پشت تأییدکننده‌هایی که به آن‌ها نیازی ندارد، منتظر بماند.

دستیابی به این هدف سخت‌تر از آن چیزی است که به نظر می‌رسد. بسیاری از کتابخانه‌ها یک مسیر اجرای واحد را برای هر فیلد به اشتراک می‌گذارند، زیرا این کار کد را کوچک و یکدست نگه می‌دارد. HydraType رویکرد معکوسی را در پیش می‌گیرد. این ابزار برای هر DTO هدف، یک کلاس PHP منحصربه‌فرد تولید می‌کند و دقیقاً عملیاتی را منتشر می‌کند که آن کلاس خاص به آن‌ها نیاز دارد. پیچیدگی به یک انتخاب اختیاری (opt-in) تبدیل می‌شود، نه مالیاتی همگانی بر تمام پراپرتی‌ها.

سرعت چگونه حاصل می‌شود

چهار انتخاب مشخص، HydraType را هم از ابزارهای عمومی مبتنی بر ریفلکشن و هم از سایر روش‌های تولیدشده متمایز می‌کند.

۱. کد را یک بار تولید کنید، برای همیشه اجرا کنید

HydraType کلاس هدف شما را تنها یک بار بررسی می‌کند و سپس یک کلاس PHP اختصاصی متناسب با ساختار دقیق آن می‌نویسد. اگر DTO شما شامل یک رشته عمومی $name، یک عدد صحیح $status و یک DateTime خصوصی $createdAt باشد، نویسنده تولیدشده از قبل همه این‌ها را می‌داند. هیچ ریفلکشن تکراری، هیچ تجزیه رشته‌ای و هیچ ساخت متادیتای تنبل (lazy) در زمان اجرا وجود ندارد. هنگامی که OPCache فایل تولیدشده را کامپایل کرد، هیدراتور از یک کد PHP که با دست نوشته شده، غیرقابل تشخیص خواهد بود.

۲. حذف پایپ‌لاین‌های خالی

اکثر هیدراتورها کار خود را حول یک پایپ‌لاین در زمان اجرا ساختاردهی می‌کنند. آن‌ها آرایه‌ای از فراخوان‌ها (callables) شامل تغییردهنده‌ها، تأییدکننده‌ها و تبدیل‌کننده‌ها را نگه می‌دارند و روی هر مقدار پیمایش می‌کنند. حتی وقتی آن آرایه‌ها خالی هستند، foreach یا array_reduce همچنان اجرا می‌شوند. HydraType این مورد را کاملاً حذف می‌کند. در طول تولید کد، این ابزار بررسی می‌کند که یک پراپرتی واقعاً به چه چیزی نیاز دارد. اگر لیست عملیات خالی باشد، یک انتساب ساده مانند $object->name = $data['name']; منتشر می‌کند. در زمان اجرا، هیچ چیزی پیمایش نمی‌شود، زیرا ژنراتور از قبل فرآیند فکری را انجام داده است.

۳. استفاده از محدوده اسکوپِ Closure برای حذف نوشتن‌های مبتنی بر ریفلکشن

پراپرتی‌های private یک دردسر کلاسیک برای مپرهای خارجی هستند. راه فرار معمول استفاده از ReflectionProperty::setValue() است، اما این متد در مقایسه با دسترسی مستقیم به پراپرتی، جریمه سنگینی دارد. HydraType با استفاده از Closure::bind از این مشکل عبور می‌کند. نویسنده تولیدشده شامل Closureهایی با اسکوپِ کلاس هدف است که به آن‌ها اجازه می‌دهد بدون استفاده از ریفلکشن، مستقیماً به پراپرتی‌های private و protected دسترسی داشته باشند. این اتصال (binding) تنها یک بار در طول ساخت انجام می‌شود؛ پس از آن، Closure با سرعتی نزدیک به کد بومی (native) اجرا می‌شود.

۴. استفاده مجدد از نویسنده (Writer)

برخی کتابخانه‌ها برای هر شیئی که هیدراته می‌کنند، یک استراتژی یا گراف ریفلکشن جدید ایجاد می‌کنند. HydraType نویسنده خود را یک بار ایجاد می‌کند و سپس از آن نمونه در هزاران شیء مجدداً استفاده می‌کند. هزینه هر شیء به حداقل ممکن، یعنی فقط مقداردهی به پراپرتی‌ها و بازگرداندن نتیجه، کاهش می‌یابد.

اعداد و ارقام

در PHP 8.2، تفاوت بسیار چشمگیر است:

  • HydraType: 267.9 ns
  • Ocramius GeneratedHydrator: 307.9 ns
  • Symfony PropertyNormalizer: 8,499.0 ns
  • Valinor: 10,755.2 ns

Symfony PropertyNormalizer و Valinor ابزارهای قدرتمندی هستند، اما ابزارهایی عمومی محسوب می‌شوند. PropertyNormalizer بخشی از یک کامپوننت سریالایزر (serializer) است که تشخیص فرمت، نرمالایزرها و گراف‌های عمیق اشیاء را مدیریت می‌کند. Valinor در مورد درخت‌های نوع (type trees) و تبدیل نوع (coercion) بسیار سخت‌گیر است. این قدرت با هزینه همراه است. در این تست، آن‌ها تقریباً سی تا چهل برابر کندتر از HydraType بودند. حتی Ocramius GeneratedHydrator که از قبل از تولید کد (code generation) استفاده می‌کند، به دلیل تفاوت‌های معماری در مورد خط‌لوله‌ها (pipelines) و دسترسی به ویژگی‌ها (property access)، کمی عقب‌تر قرار دارد.

زمینه (Context) اهمیت دارد. یک کوئری دیتابیس یا یک رفت و برگشت HTTP (roundtrip)، تمام این اعداد را ناچیز جلوه می‌دهد. اگر پس از یک کوئری در حال hydrate کردن سه شیء هستید، دنبال کردن نانوثانیه‌ها اتلاف انرژی است. این شکاف زمانی معنا پیدا می‌کند که مقیاس‌پذیری (scale) مد نظر باشد. یک فرآیند دسته‌ای (batch process) که پنجاه هزار رکورد را hydrate می‌کند، یا یک مصرف‌کننده صف (queue consumer) که اشیاء را از آرایه‌های کش بازسازی می‌کند، تفاوت بین ۲۶۷ نانوثانیه و ده میکروثانیه را حس خواهد کرد. با ضرب این تفاوت در فیلدها و ردیف‌ها، یک مپر (mapper) سریع‌تر می‌تواند بدون تغییر در منطق تجاری (business logic)، چندین ثانیه از زمان یک عملیات بکاهد.

نکته اصلی

درس اینجاست که هر پروژه‌ای به یک hydrator سفارشی نیاز ندارد؛ بلکه انتخاب‌های معماری اثرات تجمعی دارند. HydraType با تولید کدی که فقط شامل موارد درخواستی شماست، ویژگی‌های ساده (plain properties) را سریع نگه می‌دارد و در عین حال راه گریزی (escape hatch) برای موارد پیچیده فراهم می‌کند. تبدیل نوع و اشیاء تودرتو زمانی که به آن‌ها نیاز دارید وجود دارند، اما برای هر رشته اسکالر (scalar string) در شیء، باعث افت عملکرد نمی‌شوند.

قبل از تغییر ابزار، پروفایلینگ (profile) انجام دهید. اگر فرآیند hydration در ردپاهای (traces) دنیای واقعی شما ظاهر نمی‌شود، چیزی برای اصلاح وجود ندارد. اما اگر در حال انتقال مجموعه‌داده‌های بزرگ از طریق مپرهای عمومی هستید و شاهد مصرف بالای CPU هستید، به یاد داشته باشید که انتساب ساده در PHP (plain PHP assignment) هنوز هم وجود دارد. گاهی اوقات سریع‌ترین کد، همان کدی است که خودتان می‌نوشتید؛ کدی که یک بار تولید شده و سپس فراموش می‌شود.