حجت جعفری
خواندن ۱۱ دقیقه·۱ ماه پیش

مهندسی یعنی مبادله!

هیچ راه حل جادویی وجود ندارد

در طول این سالهایی که کار و مطالعه کردم بارها به این نتیجه رسیدم که در بیشتر مسایل فنی و مهندسی هیچ چیزی به عنوان بهترین راه حل وجود ندارد و همهٔ انتخاب‌های مهندسی به نوعی یک داد و ستد و مبادله بین مزایا و معایب راه حل‌های مختلف است، برای همین همواره باید آگاه باشیم که چه چیزی را می‌دهیم و در عوض چه چیزی به دست می‌آوریم.

البته این موضوعی نیست که من کشف کرده باشم، فرد بروکز (Fred Brooks) یکی از برندگان جایزه تورینگ در سال ۱۹۸۶ درست در سالی که من متولد شدم مقاله‌ای با عنوان "No Silver Bullet Essence and Accidents of Software Engineering" منتشر کرد و باعث فراگیر شدن عبارت "No Silver Bullet" در بین مهندسان نرم‌افزار شد، و معمولا زمانی استفاده می‌شود که می‌خواهیم تاکید کنیم هیچ راه حل واحد و ساده‌ای برای یک مساله پیچیده وجود ندارد.

  • اگر فکر می‌کنید فلان زبان برنامه‌نویسی بهترین زبان برنامه‌نویسی است، سخت در اشتباهید
  • اگر فکر می‌کنید فلان برسازه (Paradigm) برنامه نویسی مثلا برنامه‌نویسی ساخت یافته (Structured Programming) یا برنامه‌نویسی شی‌گرا (Object Oriented) بهترین راه ساخت نرم افزار است، سخت در اشتباهید
  • اگر فکر می‌کنید فلان موتور بازی سازی (Game Engine) بهترین است، سخت در اشتباهید
  • اگر فکر می‌کنید طراحی مبتنی بر داده (Data-Oriented Design) بهترین روش است یا طراحی مبتنی بر مولفه (Component Model) یا طراحی شی‌گرا (Object Oriented)، سخت در اشتباهید
  • اگر فکر می‌کنید فلان پلاگین (Plugin)، بسته نرم افزاری (Package)، کتابخانه (Library) بهترین است، سخت در اشتباهید

همه اینها در بین راه حل جایگزین متناظر خود مزایا و معایبی دارند که گاهی فقط گذر زمان آنها را آشکار خواهد کرد، برای همین همیشه باید تحلیل هزینه فایده (Cost-benefit analysis) انجام دهید، متغیرهایی که روی تصمیمتان موثر هستند را معین کنید و بعد از مقایسه ترجیحات انتخاب کنید.
برای یک پروژه فلان راه حل مناسب است، برای پروژه دیگر راه حل دیگر، برای یک تیم با ویژگی‌های مشخص فلان راه‌کار مناسب است برای تیم دیگر راه‌کار دیگر.

این مساله را می‌توان شبیه مسایل بهینه سازی (Optimization) با فضای حالت بزرگ دید و راه حل‌ها را یک کمینه محلی (Local Minimum) در فضای حالت، مثلا باید بردار ویژگی را مشخص کنیم و به صورت گرادیان کاهشی (Gradient descent) به سمت هدف حرکت کنیم.

گرادیان کاهشی، در فضای حالتی با ۳ ویژگی
گرادیان کاهشی، در فضای حالتی با ۳ ویژگی

مثالی از مبادله

یکی از ساده‌ترین مبادله‌ها که درک آن ساده‌ست و احتمالا برای خیلی‌ها پیش آمده، داد و ستد بین پرداش (CPU) و حافظه (Memory) است. گاهی ما با مصرف کردن بیشتر حافظه تلاش می‌کنیم میزان پردازش را کاهش دهیم و برعکس گاهی هم با مصرف بیشتر پردازشگر سعی می‌کنیم از میزان حافظه مصرفی بکاهیم.

مبادله بین پردازش و حافظه یکی از شایع ترین تصمیماتی است که در حوزه نرم افزار با آن مواجهیم، مثلا باید تصمیم بگیریم که داده‌ها را فشرده کنیم یا نکنیم؟ فشرده سازی مصرف حافظه و ذخیره سازی را بهبود می‌بخشد ولی برای دسترسی به داده‌ها نیاز به پردازش بیشتری خواهیم داشت. آیا داده‌ها را پیش پردازش کنیم یا نکنیم؟ پیش پردازش باعث مصرف بیشتر حافظه می‌شود ولی پردازش زمان اجرا را بهبود می‌بخشد.
یکی دیگر از موقعیت‌هایی که معمولا مجبور به مبادله بین CPU و RAM می‌شویم زمان انتخاب ساختمان داده مناسب برای مساله‌مان است. به طور مثال انتخاب بین آرایه (Arrays) و لیست پیوندی (Linked Lists) را در نظر بگیرید.

آرایه‌ها (Arrays)

کارایی CPU:

  • دسترسی تصادفی سریع (O(1)) به دلیل تخصیص حافظه پیوسته.
  • پیمایش (Iteration) بهینه برای کش (cache-friendly) است، که دسترسی ترتیبی را سریع‌تر می‌کند.

کارایی حافظه:

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

مبادله (Trade-off): آرایه‌ها برای دسترسی سریع و پیمایش عالی هستند، اما در اندازه انعطاف‌پذیر نیستند (تغییر اندازه هزینه‌بر است) و اگر از قبل بیش از حد بزرگ اختصاص داده شوند، ممکن است حافظه هدر رود.

لیست‌های پیوندی (Linked Lists)

کارایی CPU:

  • دسترسی تصادفی کند (O(n)) زیرا نیاز به پیمایش دارد.
  • درج و حذف سریع (O(1)) است اگر اشاره‌گری به گره مورد نظر داشته باشید.

کارایی حافظه:

  • اندازه پویا، بنابراین حافظه با نیاز رشد می‌کند.
  • هر گره به حافظه اضافی برای اشاره‌گرها نیاز دارد (سربار).

مبادله (Trade-off): لیست‌های پیوندی در اندازه انعطاف‌پذیر هستند و برای درج/حذف کارآمدند، اما برای دسترسی تصادفی کندتر هستند و به دلیل اشاره‌گرها حافظه بیشتری استفاده می‌کنند.

مثال‌ها بسیاری در مورد مبادله بین پردازش و حافظه وجود دارد که با جستجو در اینترنت می‌توانید آنها را پیدا کنید.

ویژگی‌های کیفی نرم افزار (Quality Attributes)

معمولا نیازمندی‌های یک نرم‌افزار را به دوسته نیازمندی‌های کارکردی (Functional) و نیازمندی‌های غیر کارکردی (Non-Functional) تقسیم می‌کنند، در برخی کتاب‌ها (مثل Object Oriented Software Construction) نیز آنها را به دو دسته درونی (Internal) و بیرونی (External) تقسیم می‌کنند.

ویژگی‌های عملکردی (Functional): این ویژگی‌ها به آنچه سیستم انجام می‌دهد مربوط می‌شوند و مستقیماً به نیازهای کاربر و عملکرد سیستم مرتبط هستند.

ویژگی‌های غیرعملکردی (Non-Functional): این ویژگی‌ها به چگونگی عملکرد سیستم مربوط می‌شوند و معمولاً به کیفیت اجرا، تجربه کاربری و قابلیت‌های فنی سیستم مرتبط هستند.

وقتی افراد در مورد نیازمندی‌های غیرعملکردی صحبت می‌کنند، معمولاً به ویژگی‌های کیفی اشاره دارند.

نیازمندی‌های غیرعملکردی به‌طور مستقیم در نرم‌افزار یا سخت‌افزار پیاده‌سازی نمی‌شوند. در عوض، آن‌ها به عنوان منشأیی برای عملکردهای مشتق‌شده، تصمیم‌های معماری یا رویکردهای طراحی و پیاده‌سازی عمل می‌کنند. برخی از نیازمندی‌های غیرعملکردی محدودیت‌هایی را اعمال می‌کنند که انتخاب‌های موجود برای طراح یا توسعه‌دهنده را محدود می‌سازند. برای مثال، یک نیازمندی قابلیت همکاری می‌تواند طراحی محصول را به استفاده از رابط‌های استاندارد خاص محدود کند.

برخی از ویژگی‌های کیفی مهم برای سیستم‌های نرم‌افزاری

  • قابلیت نگهداری (Maintainability): آیا توسعه‌دهندگان می‌توانند به راحتی سیستم را تغییر، اصلاح و بهبود بخشند؟
  • عملکرد (Performance): آیا سیستم به اندازه کافی سریع به اقدامات کاربر و رویدادهای خارجی پاسخ می‌دهد؟
  • کارایی (Efficiency): آیا سیستم از منابع کامپیوتری به صورت اقتصادی استفاده می‌کند؟
  • مقیاس‌پذیری (Scalability): آیا سیستم می‌تواند به راحتی برای پذیرش کاربران، داده‌ها یا تراکنش‌های بیشتر گسترش یابد؟
  • قابلیت اطمینان (Reliability): آیا سیستم زمانی که قرار است کار کند، بدون خطا اجرا می‌شود؟
  • مقاومت (Robustness): آیا سیستم به طور منطقی به ورودی‌های نادرست و شرایط عملیاتی غیرمنتظره پاسخ می‌دهد؟
  • قابلیت حمل (Portability): آیا می‌توان سیستم را به راحتی به پلتفرم‌های مختلف منتقل کرد؟
  • قابلیت استفاده مجدد (Reusability): آیا توسعه‌دهندگان می‌توانند بخش‌هایی از سیستم را در محصولات دیگر مورد استفاده مجدد قرار دهند؟
  • قابلیت دسترسی (Availability): آیا می‌توانم از سیستم در زمان و مکانی که نیاز دارم، استفاده کنم؟
  • قابلیت نصب (Installability): آیا می‌توانم به راحتی سیستم و ارتقاء آن را نصب، حذف و دوباره نصب کنم؟
  • یکپارچگی (Integrity): آیا سیستم در برابر نادرستی، خرابی و از دست رفتن داده‌ها محافظت می‌کند؟
  • قابلیت استفاده (Usability): آیا کاربران می‌توانند به راحتی نحوه استفاده از سیستم را برای انجام وظایف خود یاد بگیرند؟
  • قابلیت تأیید (Verifiability): آیا آزمایش کنندگان می‌توانند تعیین کنند که آیا نرم‌افزار به درستی پیاده‌سازی شده است یا خیر؟
  • ایمنی (Safety): آیا سیستم از کاربران در برابر آسیب و اموال در برابر خسارت محافظت می‌کند؟
  • امنیت (Security): آیا سیستم در برابر حملات بدافزار، مزاحمان، کاربران غیرمجاز و سرقت داده‌ها محافظت می‌کند؟
  • انطباق با استانداردها (Conformance to standards): آیا سیستم با تمام استانداردهای قابل اجرا برای عملکرد، ایمنی، ارتباطات، گواهینامه و رابط‌ها مطابقت دارد؟
  • قابلیت همکاری (Interoperability): آیا سیستم به خوبی با سایر سیستم‌ها برای تبادل داده و خدمات ارتباط برقرار می‌کند؟

در طراحی و توسعه سیستم‌های نرم‌افزاری، مبادله‌های کیفیتی (Quality Attributes Trade-offs) بسیار رایج هستند. امکان دستیابی به بهترین حالت ممکن برای تمامی جنبه‌های قابلیت‌ها و ویژگی‌های یک سیستم نرم‌افزاری وجود ندارد. همواره مبادله‌هایی اجتناب‌ناپذیر بین جنبه‌های مختلف کیفیت وجود دارد — بهبود یک جنبه اغلب باعث کاهش اجتناب‌ناپذیر در جنبه‌ای دیگر می‌شود. در نتیجه، بخش ضروری از تحلیل نیازمندی‌ها، درک این موضوع است که کدام ویژگی‌های کیفی از اهمیت بیشتری برخوردارند تا طراحان بتوانند به‌طور مناسب به آن‌ها بپردازند.

مثال‌هایی از مبادله بین ویژگی‌های کیفی

انعطاف‌پذیری (Flexibility) در مقابل عملکرد (Performance)

مبادله: افزایش انعطاف‌پذیری سیستم (مانند پشتیبانی از پلاگین‌ها یا ماژول‌های اضافی) ممکن است عملکرد سیستم را کاهش دهد.
مثال: استفاده از معماری‌های مبتنی بر پلاگین ممکن است باعث افزایش سربار و کاهش سرعت اجرا شود.

کارایی (Efficiency) در مقابل قابلیت نگهداری (Maintainability)

مبادله: کد بهینه‌شده برای کارایی ممکن است خوانایی و قابلیت نگهداری کم‌تری داشته باشد.
مثال: استفاده از بهینه‌سازی‌های سطح پایین (مانند کد اسمبلی) ممکن است کارایی را افزایش دهد، اما نگهداری و توسعه آن را دشوار کند.

عملکرد (Performance) در مقابل مقیاس‌پذیری (Scalability)

مبادله: بهبود عملکرد سیستم (مانند پاسخ‌دهی سریع‌تر) ممکن است نیاز به بهینه‌سازی‌های خاصی داشته باشد که مقیاس‌پذیری سیستم را محدود کند.
مثال: استفاده از کش‌های حافظه برای افزایش سرعت ممکن است باعث شود سیستم در صورت افزایش تعداد کاربران، به‌درستی مقیاس‌پذیر نباشد.

امنیت (Security) در مقابل قابلیت استفاده (Usability)

مبادله: افزایش امنیت (مانند احراز هویت چندمرحله‌ای یا رمزنگاری قوی) ممکن است تجربه کاربری را پیچیده‌تر کند.
مثال: اضافه کردن لایه‌های امنیتی بیشتر می‌تواند باعث شود کاربران برای دسترسی به سیستم، مراحل بیشتری را طی کنند.

قابلیت استفاده (Usability) در مقابل امنیت (Security)

مبادله: ساده‌سازی رابط کاربری برای بهبود قابلیت استفاده ممکن است امنیت سیستم را کاهش دهد.
مثال: اجازه دادن به کاربران برای استفاده از رمزهای عبور ساده ممکن است تجربه کاربری را بهبود بخشد، اما امنیت سیستم را کاهش می‌دهد.

قابلیت نگهداری (Maintainability) در مقابل کارایی (Efficiency)

مبادله: کدی که به‌راحتی قابل نگهداری و تغییر است (مانند استفاده از الگوهای طراحی ماژولار) ممکن است کارایی کم‌تری نسبت به کد بهینه‌شده داشته باشد.
مثال: استفاده از ماژول‌های مستقل ممکن است باعث افزایش سربار (Overhead) در اجرای سیستم شود.

قابلیت اطمینان (Reliability) در مقابل هزینه (Cost)

مبادله: افزایش قابلیت اطمینان (مانند افزونگی سخت‌افزاری یا استفاده از پایگاه‌داده‌های توزیع‌شده) ممکن است هزینه‌های توسعه و نگهداری را به‌طور قابل توجهی افزایش دهد.
مثال: پیاده‌سازی سیستم‌های توزیع‌شده برای اطمینان از دسترسی دائمی، هزینه‌های زیرساختی و عملیاتی را افزایش می‌دهد.

قابلیت انتقال (Portability) در مقابل عملکرد (Performance)

مبادله: طراحی سیستم برای اجرا روی پلتفرم‌های مختلف (مانند استفاده از زبان‌های چندسکویی) ممکن است عملکرد سیستم را کاهش دهد.
مثال: استفاده از فریم‌ورک‌های چندسکویی مانند جاوا یا .NET ممکن است باعث کاهش سرعت اجرا نسبت به کد نیتیو (Native) شود.

مقیاس‌پذیری (Scalability) در مقابل پیچیدگی (Complexity)

مبادله: افزایش مقیاس‌پذیری سیستم (مانند افزودن گره‌های بیشتر در یک سیستم توزیع‌شده) ممکن است پیچیدگی طراحی و مدیریت سیستم را افزایش دهد.
مثال: سیستم‌های توزیع‌شده مانند Apache Kafka مقیاس‌پذیری بالایی دارند، اما مدیریت و اشکال‌زدایی آن‌ها بسیار پیچیده است.

قابلیت اطمینان (Reliability) در مقابل زمان توسعه (Development Time)

مبادله: افزایش قابلیت اطمینان (مانند تست‌های گسترده و پیاده‌سازی مکانیزم‌های بازیابی خطا) ممکن است زمان توسعه را افزایش دهد.
مثال: افزودن تست‌های واحد و یکپارچه‌سازی مداوم (CI/CD) ممکن است زمان تحویل محصول را طولانی‌تر کند.

این مبادله‌ها نشان می‌دهند که در طراحی سیستم‌های نرم‌افزاری، باید اولویت‌های پروژه و نیازهای ذینفعان به دقت بررسی شوند تا تعادل مناسبی بین ویژگی‌های کیفی مختلف برقرار شود.

تجربه یک انتخاب

زمانی که در حال آماده شدن برای شروع پروژه Wizard Of Legend 2 بودیم، من و البرز غرایی به عنوان مهندسین پروژه باید تصمیم می‌گرفتیم که با توجه به نیازمندی‌های پروژه و تیم از کدام یک از موتورهای بازی‌سازی موجود استفاده کنیم.
در آن زمان یعنی اسفند ۱۴۰۱ ما تجربه سالها کار با موتور Unity را داشتیم و تقریبا تمام چم و خم کار با آن را می‌دانستیم، اما از طرفی مشکلات موجود در آن را هم می‌دانستیم.
از آنجایی که بازی یک حالت بازی چند نفره آنلاین داشت امکانات شبکه برای ما اهمیت داشت، در آن زمان Unity راه‌کار پایداری برای شبکه نداشت و در حالت تغییر راه‌کار شبکه خود بود، در حالی که آنریل امکانات شبکه خوب و پایداری داشت.
این که ما چه قدر با این موتور جدید آشنا بودیم و چه مقدار زمان صرف یادگیری آن می‌شد قطعا جزو معیارهای ما بود، تجربه قبلی کارکردن با یونیتی نکته مثبتی برای آن محسوب می‌شد.
بودن Blueprint و یکپارچگی آن با همه زیر سیستم‌ها برای آنریل مزیت خیلی خوبی محسوب می‌شد و در لایه‌هایی سرعت تکرار (Iteration) را بسیار افزایش می‌داد و باعث می‌شد افرادی از تیم که برنامه‌نویس نبودند هم بتوانند برخی کارها را انجام دهند.
استفاده از زبان ++C برای آنریل مزیت بزرگی بود، هم به لحاظ بهینه بودن و سرعت اجرا هم به خاطر این که در جزییات پیاده‌سازی درگیر جنگ با Garbage Collection نمی‌شدیم. در پروژه‌های قبلی مجبور بودیم با زبان #C طور خاصی برخورد کنیم تا کمترین تلفات حافظه را داشته باشیم.
بزرگی و پیچیدگی آنریل باعث ایجاد نوعی لختی می‌شود و نیاز به افراد در حوزه‌های تخصصی را بالا می‌برد، برای همین به تیم بزرگ تری نیاز دارد، که آن را برای پروژه‌های کوچک و متوسط نامناسب می‌کند.

مقایسه موتور Unity و Unreal
مقایسه موتور Unity و Unreal

ما بدون هیچ تعصبی مزایا و معایب هر دو را پای تخته نوشتیم و با بالا و پایین کردن معیارها و ترجیحات تیمی تصمیم گرفتیم برای این پروژه از موتور Unreal استفاده کنیم.
با این که این موتور برای ما جدید بود بخشی از زمان را باید صرف یادگیری آن می‌کردیم ولی بودن امکانات شبکه در دسترس و یکپارچگی آن با کد و Blueprint همچنین پلاگین Gameplay Ability System باعث صرفه جویی زیادی در زمان و راحتی پیاده سازی مکانیک‌های پیچیده شد.

نکته پایانی

در پایان باید بگویم که در انتخاب‌های مهندسی خود نباید تعصب داشت، باید از تجربیات دیگران درس بگیریم، نیازمندی‌های پروژه و تیم را بشناسید، ترجیحات خود را در نظر بگیرید و آگاهانه انتخاب کنید که چه چیزی می‌دهید و چه چیزی به دست می‌آورید، تا یک مبادله درست برای پروژه انجام دهیم.
اگر کسی بعد از خواندن این نوشته همچنان بر این باور است که فلان راه‌حل بهترین است و در همه حالت‌ها باید از آن استفاده کرد، باید به کارکرد سالم ذهن خود شک کند! البته اگر کارکرد سالم ذهنی کسی مشکل داشته باشد توانایی شک کردن به کارکرد ذهنی خود را نیز نخواهد داشت! D:

دانش آموخته هوش مصنوعی، مهندس نر‌م‌افزار و برنامه‌نویس، بازی ساز سابق
شاید از این پست‌ها خوشتان بیاید