در طول این سالهایی که کار و مطالعه کردم بارها به این نتیجه رسیدم که در بیشتر مسایل فنی و مهندسی هیچ چیزی به عنوان بهترین راه حل وجود ندارد و همهٔ انتخابهای مهندسی به نوعی یک داد و ستد و مبادله بین مزایا و معایب راه حلهای مختلف است، برای همین همواره باید آگاه باشیم که چه چیزی را میدهیم و در عوض چه چیزی به دست میآوریم.
البته این موضوعی نیست که من کشف کرده باشم، فرد بروکز (Fred Brooks) یکی از برندگان جایزه تورینگ در سال ۱۹۸۶ درست در سالی که من متولد شدم مقالهای با عنوان "No Silver Bullet Essence and Accidents of Software Engineering" منتشر کرد و باعث فراگیر شدن عبارت "No Silver Bullet" در بین مهندسان نرمافزار شد، و معمولا زمانی استفاده میشود که میخواهیم تاکید کنیم هیچ راه حل واحد و سادهای برای یک مساله پیچیده وجود ندارد.
همه اینها در بین راه حل جایگزین متناظر خود مزایا و معایبی دارند که گاهی فقط گذر زمان آنها را آشکار خواهد کرد، برای همین همیشه باید تحلیل هزینه فایده (Cost-benefit analysis) انجام دهید، متغیرهایی که روی تصمیمتان موثر هستند را معین کنید و بعد از مقایسه ترجیحات انتخاب کنید.
برای یک پروژه فلان راه حل مناسب است، برای پروژه دیگر راه حل دیگر، برای یک تیم با ویژگیهای مشخص فلان راهکار مناسب است برای تیم دیگر راهکار دیگر.
این مساله را میتوان شبیه مسایل بهینه سازی (Optimization) با فضای حالت بزرگ دید و راه حلها را یک کمینه محلی (Local Minimum) در فضای حالت، مثلا باید بردار ویژگی را مشخص کنیم و به صورت گرادیان کاهشی (Gradient descent) به سمت هدف حرکت کنیم.
یکی از سادهترین مبادلهها که درک آن سادهست و احتمالا برای خیلیها پیش آمده، داد و ستد بین پرداش (CPU) و حافظه (Memory) است. گاهی ما با مصرف کردن بیشتر حافظه تلاش میکنیم میزان پردازش را کاهش دهیم و برعکس گاهی هم با مصرف بیشتر پردازشگر سعی میکنیم از میزان حافظه مصرفی بکاهیم.
مبادله بین پردازش و حافظه یکی از شایع ترین تصمیماتی است که در حوزه نرم افزار با آن مواجهیم، مثلا باید تصمیم بگیریم که دادهها را فشرده کنیم یا نکنیم؟ فشرده سازی مصرف حافظه و ذخیره سازی را بهبود میبخشد ولی برای دسترسی به دادهها نیاز به پردازش بیشتری خواهیم داشت. آیا دادهها را پیش پردازش کنیم یا نکنیم؟ پیش پردازش باعث مصرف بیشتر حافظه میشود ولی پردازش زمان اجرا را بهبود میبخشد.
یکی دیگر از موقعیتهایی که معمولا مجبور به مبادله بین CPU و RAM میشویم زمان انتخاب ساختمان داده مناسب برای مسالهمان است. به طور مثال انتخاب بین آرایه (Arrays) و لیست پیوندی (Linked Lists) را در نظر بگیرید.
آرایهها (Arrays)
کارایی CPU:
کارایی حافظه:
مبادله (Trade-off): آرایهها برای دسترسی سریع و پیمایش عالی هستند، اما در اندازه انعطافپذیر نیستند (تغییر اندازه هزینهبر است) و اگر از قبل بیش از حد بزرگ اختصاص داده شوند، ممکن است حافظه هدر رود.
لیستهای پیوندی (Linked Lists)
کارایی CPU:
کارایی حافظه:
مبادله (Trade-off): لیستهای پیوندی در اندازه انعطافپذیر هستند و برای درج/حذف کارآمدند، اما برای دسترسی تصادفی کندتر هستند و به دلیل اشارهگرها حافظه بیشتری استفاده میکنند.
مثالها بسیاری در مورد مبادله بین پردازش و حافظه وجود دارد که با جستجو در اینترنت میتوانید آنها را پیدا کنید.
معمولا نیازمندیهای یک نرمافزار را به دوسته نیازمندیهای کارکردی (Functional) و نیازمندیهای غیر کارکردی (Non-Functional) تقسیم میکنند، در برخی کتابها (مثل Object Oriented Software Construction) نیز آنها را به دو دسته درونی (Internal) و بیرونی (External) تقسیم میکنند.
ویژگیهای عملکردی (Functional): این ویژگیها به آنچه سیستم انجام میدهد مربوط میشوند و مستقیماً به نیازهای کاربر و عملکرد سیستم مرتبط هستند.
ویژگیهای غیرعملکردی (Non-Functional): این ویژگیها به چگونگی عملکرد سیستم مربوط میشوند و معمولاً به کیفیت اجرا، تجربه کاربری و قابلیتهای فنی سیستم مرتبط هستند.
وقتی افراد در مورد نیازمندیهای غیرعملکردی صحبت میکنند، معمولاً به ویژگیهای کیفی اشاره دارند.
نیازمندیهای غیرعملکردی بهطور مستقیم در نرمافزار یا سختافزار پیادهسازی نمیشوند. در عوض، آنها به عنوان منشأیی برای عملکردهای مشتقشده، تصمیمهای معماری یا رویکردهای طراحی و پیادهسازی عمل میکنند. برخی از نیازمندیهای غیرعملکردی محدودیتهایی را اعمال میکنند که انتخابهای موجود برای طراح یا توسعهدهنده را محدود میسازند. برای مثال، یک نیازمندی قابلیت همکاری میتواند طراحی محصول را به استفاده از رابطهای استاندارد خاص محدود کند.
برخی از ویژگیهای کیفی مهم برای سیستمهای نرمافزاری
در طراحی و توسعه سیستمهای نرمافزاری، مبادلههای کیفیتی (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 طور خاصی برخورد کنیم تا کمترین تلفات حافظه را داشته باشیم.
بزرگی و پیچیدگی آنریل باعث ایجاد نوعی لختی میشود و نیاز به افراد در حوزههای تخصصی را بالا میبرد، برای همین به تیم بزرگ تری نیاز دارد، که آن را برای پروژههای کوچک و متوسط نامناسب میکند.
ما بدون هیچ تعصبی مزایا و معایب هر دو را پای تخته نوشتیم و با بالا و پایین کردن معیارها و ترجیحات تیمی تصمیم گرفتیم برای این پروژه از موتور Unreal استفاده کنیم.
با این که این موتور برای ما جدید بود بخشی از زمان را باید صرف یادگیری آن میکردیم ولی بودن امکانات شبکه در دسترس و یکپارچگی آن با کد و Blueprint همچنین پلاگین Gameplay Ability System باعث صرفه جویی زیادی در زمان و راحتی پیاده سازی مکانیکهای پیچیده شد.
در پایان باید بگویم که در انتخابهای مهندسی خود نباید تعصب داشت، باید از تجربیات دیگران درس بگیریم، نیازمندیهای پروژه و تیم را بشناسید، ترجیحات خود را در نظر بگیرید و آگاهانه انتخاب کنید که چه چیزی میدهید و چه چیزی به دست میآورید، تا یک مبادله درست برای پروژه انجام دهیم.
اگر کسی بعد از خواندن این نوشته همچنان بر این باور است که فلان راهحل بهترین است و در همه حالتها باید از آن استفاده کرد، باید به کارکرد سالم ذهن خود شک کند! البته اگر کارکرد سالم ذهنی کسی مشکل داشته باشد توانایی شک کردن به کارکرد ذهنی خود را نیز نخواهد داشت! D: