اصول SOLID به مجموعه ی 5 قاعده ی توسعه ی نرم افزار گفته می شود که توسط عمو باب (Robert C. Martin) ابداع شد. این اصول، مجموعه ای از دستورالعمل ها برای طراحی شئ گرا (OOD) هستند که در طراحی کلاس ها بسیار پرکاربرد و مفید هستند. برنامه نویسان وب، اپلیکیشن یا هر نوع بیزنسی که بر پایه توسعه ی نرم افزاری اجایل کار میکنند، به طور گسترده ای از این شیوه بهره می برند، اما این اصول در بین توسعه دهندگان بازی خیلی شناخته شده نیست. در این مقاله قصد داریم به تشریح و توضیح این قواعد، در چارچوب توسعه ی بازی بپردازیم.
این قاعده، سنگ بنای این مجموعه است و اگر آن را به درستی رعایت کنید، به نتیجه ی مطلوب خواهید رسید. طبق این اصل، هر کلاس باید فقط یک وظیفه به عهده داشته باشد و در نتیجه، تنها یک دلیل وجود خواهد داشت که کلاس را تغییر دهید. اگر کلاس ها کوچک باشند و فقط روی یک هدف تمرکز داشته باشند، به راحتی می توانید تشخیص دهید برای پیدا کردن یا اضافه کردن یک ویژگی در بازی به کدام قسمت باید مراجعه کنید.
چرا داشتن چند وظیفه برای یک کلاس، بد است؟ وظایف متعدد در یک کلاس باعث بروز وابستگی (Coupling) بین کد ها می شود و ایجاد تغییر در یک وظیفه، ممکن است باعث اختلال در مسئولیت های دیگر شود. این نوع طراحی بسیار شکننده است و سبب می شود در قسمت هایی که انتظار ندارید، برنامه تان دچار مشکل شود و گاها با این نوع سوالات روبرو شوید: "چرا بعد از تغییر سیستم رندرینگ، پرش و راه رفتن از کار افتاد؟ "
اگر کدهای بازی تان این قانون را نقض می کنند، باید فورا هر ویژگی و مسئولیتی را در کلاسِ مخصوصِ خودش قرار دهید. به عنوان اولین قدم می توانید برای هر مسئولیتی یک اینترفیس در نظر بگیرید. کلاس های دیگر می توانند به جای خودِ کلاس، به اینترفیسِ آنها وابسته باشند. سپس بدون هیچ مشکلی میتوانید کلاس را به چندین کلاس تقسیم کنید که هرکدام یک مسئولیت را به عهده دارند و هر کدام یک اینترفیس را پیاده سازی میکنند.
چه زمانی این مشکل را دارید ؟ معمولا متهم اصلی در نقضِ این اصل، کلاس یا کلاس هایی هستند که شامل صدها یا هزاران خط کد می باشند. می دانید درباره ی چه حرف میزنم، معمولا یک کلاس Manager یا به همین مفهوم دارید که تمام افراد تیم توسعه، کدهایشان را در آن قرار میدهند. این کلاس معمولا 500 دلیل برای تغییر دارد و 500 وظیفه متفاوت را به عهده دارد. بی رحم ترین باگ ها که ساعت ها یا روزها شما را درگیر میکند از دل این کلاس ها بیرون می آید.
هدف این اصل این است که ایجادِ تغییرات در هر کلاس را به حداقل برساند و در عین حال، این امکان را فراهم کند تا بتوان در سناریو های متفاوت از همان کلاس استفاده کرد. شاید چنین به نظر برسد که این دو مورد با هم تناقض دارند اما خواهید دید که چگونه همدیگر را کامل میکنند و یک طراحی قوی و یکپارچه شکل میدهند. اینکه یک کلاس برای توسعه باز باشد به این معنی است که با توجه به نیازمندی های جدید، بتوان رفتار یک کلاس را به شکل متفاوتی تغییر داد. اگر برای به وقوع پیوستنِ این تغییرات، نیاز به اصلاح هیچ کدی نباشد به این مفهوم است که کلاس باید نسبت به اصلاح بسته باشد.
این قانون را به راحتی میتوان با طراحی داده محور (data driven design) اجرا کرد. با ارسال اطلاعات پیکربندی مورد نیاز به یک کلاس، به راحتی می توان آن را توسعه داد و نیاز به اصلاح کد را کاهش داد. هر نوع متغیری (در معنای ریاضی) باید به کلاس ارسال شود، در نتیجه مفهوم کلاس (کد ها)، به تنهایی و به خودی خود عملکرد آن کلاس را مشخص نمی کند. برای درک بهتر این قاعده، بهتر است این موضوع را در قالبِ قاعده ی داده ها و عملیات در طراحی شی گرا (OOD) مورد تحلیل قرار دهید. کلاس، عملیاتی را که قرار است انجام دهد (همان توابع اش) و داده هایی که روی آنها عملیات انجام میدهد را تعریف میکند. تا جایی که امکان دارد، این داده ها باید به کلاس یا تابع ارسال شوند. به این ترتیب پیکربندی داده ها در خارج از کلاس و توسط کدی که آن را فراخوانی می کند، صورت می پذیرد و در نتیجه، امکان ایجاد تغییرات در آن افزایش می یابد.
چه زمانی این مشکل را دارید ؟ این همان فایلی است که می ترسید آن را باز کنید و مورد بررسی قرار دهید، چون همه ی تیم در حال کار کردن روی آن کد هستند و مدام آن را تغییر میدهند. مهم نیست با چه سیستمی کار می کنید، این فایل ها همیشه باعث بروز پیچیدگی می شوند و شما را گرفتار خواهند کرد.
ارث بری و چندریختی (polymorphism) به عنوان مکانیزم های قدرتمندی برای حل مشکلات پیچیده بوسیله ی راه حل های ساده، تلقی می شوند. این دو همچنین مکانیزم های قدرتمندی برای ایجاد باگ و کد های مشکل ساز می باشند. هدف این اصل این است که اطمینان حاصل کند که سلسله مراتب وراثت به خوبی کار کنند و توسط کدها بطور ناصحیح مورد استفاده قرار نگیرند. در غیر این صورت باگ هایی ایجاد می شود که به سختی قابل شناسایی و ردیابی خواهند بود. شاید این اصل در ظاهر ساده به نظر برسد اما حل مشکلات بوسیله ی آن، تا حدی پیچیده است.
اولین قدم برای حل این مشکل پیدا کردن مواردی است از چک کردنِ نوع آبجکت - چه نوع همان آبجکت و چه آبجکت هایی که روی آنها کار میکند. پس از این مرحله ی ساده، اصلی به اسم "طراحی بر اساس قرارداد" (Design By Contract) باید اعمال شود. هر تابعی یک سری شرط دارد که باید پیش از فراخوانی برقرار باشند (پیش شرایط) و یک سری شرط دارند که تضمین می کنند پس از اتمام، برآورده شوند (پس شرایط). این ها معمولا شرایط ضمنی هستند که در ذهن برنامه نویسی که روی آن کار میکند وجود دارد.گام اول، ساختار بندی کردن این شرایط به شکل کد می باشد. هنگامی که این گام به سرانجام رسید، قانون بعدی میتواند به تحقق بپیوندد: "کلاس های مشتق شده، تنها می توانند پیش شرایط را ضعیف کنند و پس شرایط را تقویت کنند". به عبارتی دیگر، توابع یک کلاس مشتق شده نباید انتظاری بیش از کلاس پایه شان داشته باشند و همچنین نباید وعده ی کمتر از آن را نیز بدهند. این قانون بسیار پر اهمیت است و علت آن، این موضوع است که یک مدل که به تنهایی مورد بررسی قرار میگیرد را نمیتوان به شکل معنی داری مورد تایید قرار داد. مطمئنا نمیدانید که آیا کلاس جدیدتان با نام Tankاعتبار دارد یا نه، مگر زمانی که آن را در کنار والدش، خواهر و برادرش (sibiling) و سایر سیستم های بازی قرار دهید.
چه زمانی این مشکل را دارید ؟ کلاس هایی که این قانون را نقض میکنند به راحتی قابل شناسایی هستند. کافی است به دنبال کلاس پایه ای باشید که از اطلاعات نوع ران تایم (RTTI) (یا نوع آبجکتی که بر روی آن کار می کند) برای بررسی و شناسایی نوع خودش استفاده میکند. اگر در برنامه تان، كلاس GameEntity برای اجرای یک سری کد و عملیات، چک میکند که آیا خودش از نوع Tank است یا نه، به این مفهوم است که این قانون نقض شده است. کلاس باید بتواند به صورت چند ریختی و بدون اینکه اهمیت دهد که نوع اصلی این آبجکت چیست، توابعِ آن را فراخوانی کند.
اینترفیس ها باید برای برقراری ارتباط بین آبجکت های مختلف مورد استفاده قرار بگیرند تا کدهایی تمیز با ساختار ماژولار را شکل دهند. اما اصلِ "تجزیه ی اینترفیس" این مفهوم را به این شکل بسط می دهد که اینترفیس ها نیز باید خودشان تمیز و یکپارچه باشند. هر چه اینترفیس بزرگتر باشد، احتمالِ بیشتری خواهد بود که کلاینت، به عملکردِ آبجکت دیگری نیز وابسته شود. اما به کمک اینترفیس های کوچک و تجزیه شده، هر آبجکت فقط به کوچکترین واحدِ عملکردی که نیاز دارد وابسته خواهد بود. این امر منجر به کاهش پیچیدگی ارتباطات بین آبجکت ها می شود و از آن مهمتر اینکه اگر کسی کدهای شما را بررسی کند، به راحتی متوجه میشود که هر کلاسی به چه چیزی وابسته است. به جای داشتن یک اینترفیس "غول پیکر"، بهتر است که آن را به چندین اینترفیس کوچکتر با وظایف مشخص تقسیم کنید تا هر کدام برای یک کلاینتِ بخصوص، مناسب باشند.
این قاعده با اصلِ "تک وظیفه ای" بسیار مرتبط است. در این قاعده هر اینترفیس باید تنها یک وظیفه داشته باشد که به شما این امکان را میدهد که به سادگی عملکردِ هر آبجکتی را بر اساس اینترفیس هایی که استفاده می کند، بیان کنید.
چه زمانی این مشکل را دارید ؟ تمام اینترفیس ها (کلاس های ابسترکت) را بررسی کنید و مطمئن شوید که تمامی توابع شان همگن و حول یک موضوع هستند. یک راه ساده برای تشخیص اینکه این قاعده را شکستید، زمانی است که چند گروه از توابع، درون یک اینترفیس دارید. معمولا راه تشخیص آن هم دیدن فضای خالی در کدها است، هرچه بین بخش هایی از کدهای یک اینترفیس فاصله ی بیشتری وجود داشته باشد به این مفهوم است که احتمالا این گروه ها کارایی متفاوتی نسبت به هم دارند و اینترفیس یکپارچه نیست.
"انتزاع نباید به جزئیات بستگی داشته باشد. جزئیات باید بر اساس انتزاع باشد."
این یک نکته ی کلیدی است که اخیرا در زمینه ی بازی سازی نیز به شدت پرکاربرد شده است. معمولا اگر یک کلاس به کلاس دیگری وابسته باشد، کلاینت ابتدا آبجکتی از آن کلاس را نمونه سازی(instantiate)میکند و سپس روی آن کار میکند. وارونگی وابستگی (یا وارونگی کنترل) این موضوع را برعکس میکند. به جای اینکه کلاینت مسئول ساخت آبجکت باشد، آبجکتی که به آن نیاز دارد، در اختیارش قرار میگیرد که در نتیجه ی آن، کنترل از کلاینت گرفته می شود و به مالکِ کلاینت که معمولا موتور بازی سازی است داده می شود.
مثال خوبی برای این موضوع، سیستم رندرینگ است. به جای نمونه سازی از آبجکت رندرینگ یا فراخوانی مستقیمِ کلاس های API رندرینگ، سیستم رندرینگ باید یک اینترفیس برای عملکردِ سطح پایین رندرینگ دریافت کند. با وابسته شدن به اینترفیسی که به سیستم رندرینگ داده شده، میتوان بدون ایجاد مشکلی برای کلاینت هایِ این سیستم ،API سطح پایین رندرینگ را تغییر داد.
چه زمانی این مشکل را دارید ؟ هنگامی که دو سیستم در حال ارتباط و صحبت با یکدیگر هستند، به چه شکلی این کار را انجام می دهند؟ اگر از کلاس های واقعی استفاده میکنند، بهتر است دنبال راهی باشید تا آنها را به اینترفیس وابسته کنید. بهترین راه این است که کلاسِ مورد نظر، رفرنسِ اینترفیسی که نیاز دارد را به عنوان پارامتر متد سازنده اش دریافت کند.
منبع: Gamasutra
ترجمه شده در شماره 27 بازینامه