در برنامه نویسی بسیار مهم است که در زمان طراحی کد، قوانین استانداردسازی آن رعایت شود. استانداردسازی کدها باعث تولید کدهای بهینه، تمیز و خوانا میشود. شاید عده ای از برنامه نویسان (تعدادشان اصلا کم نیست!) تصور کنند صرف تحویل دادن یک برنامه که کار میکند (Working Software) کافی است. اما واقعیت این است که کدهای کثیف و غیر استاندارد معمولا با بهینه نبودن خود باعث میشوند که حتی کاربر برنامه از آن رضایت نداشته باشد. بنابراین در اغلب موارد کدهای کثیف و بی کیفیت حتی از دید کاربران عادی برنامهها پنهان نمیماند!
ضمنا تغییر کدهای کثیف و بی قانون بسیار مشکل، پر هزینه و زمان بر است. علاوه بر اینکه تغییر کدهای یک برنامه نویس کثیف برای برنامه نویسان همکار او دشوار است، برای خود او میتواند به یک کابوس واقعی تبدیل شود!
یکی از قوانین مهمی که در برنامه نویسی باید رعایت شود قوانین معروف به SOLID (بخوانید سالید) است. SOLID شامل پنج اصل است که همگی برای داشتن یک کد تمیز و استاندارد و حتی عقلانی (!) ضروری هستند. این پنج اصل عبارت اند از:
Single Responsibility Principle یا اصل تک وظیفگی (SRP)
Open/Closed Principle یا اصل باز و بسته بودن (OCP)
Liskov Substitution Principle یا اصل جانشینی لیسکف (LSP)
Interface Segregation Principle یا اصل تفکیک Interface (ISP)
Dependency Inversion Principle یا اصل وارونه کردن وابستگی (DIP)
در این مطلب به آشنایی با اصل دوم یعنی اصل باز و بسته بودن میپردازیم.
اصل Open/Closed یا باز و بسته بودن چیست
اصل باز و بسته بودن یا اصل Open/Closed به نظر بسیاری، اساس برنامه نویسی شی گرا را تشکیل میدهد. رابرت مارتین (Robert C. Martin) که در بین برنامه نویسان به عمو باب (Uncle Bob) مشهور است با عبارت: "مهمترین اصل طراحی شی گرا" از این اصل یاد کرده است.
موجودیتهای برنامه که شامل کلاس ها، ماژول ها، توابع و... می را در نظر بگیرید. بر اساس اصل باز و بسته بودن هر کدام از این موجودیتها باید برای گسترده شدن (Extension) باز، اما برای تغییر کردن بسته باشند.
تصور کنید شخصی دست هایی شبیه قیچی داشته باشد! این دستها باید برای کارهای روزمره هر بار به طور کامل عوض شوند (که البته عوض کردن دست در دنیای واقعی کمی دور از ذهن به نظر میرسد!). درست مانند کلاسی که تمام کارایی را داخل خود پیاده کرده است و توسعه دهنده برای تغییر کارایی و حتی گسترش آن، محتویات کلاس را دستکاری میکند. اما روش درستتر این است که دستها به جای اینکه تبدیل به ابزار شوند، به ابزارهای مورد نیاز "مجهز شوند". در این حالت دستها با گسترش کارایی یا Extend شدن تجهیز میشوند.
رعایت این اصل به شما کمک میکند مجبور نباشید برای تغییر یک کلاس، تمام کدهایی که از آن کلاس استفاده میکنند را تغییر دهید. به عنوان مثال به کد زیر توجه کنید:
public function drawAllShapes($shapes) {
for ($i = 0; $i < count($shapes); $i++) {
$shape = $shapes[$i];
switch ($shape->type) {
case "square":
$this->drawSquare($shape);
break;
case "circle":
$this->drawCircle($shape);
break;
}
}
}
در این نمونه کد، بر اساس خاصیت type از اشیای نوع shape، در یک ساختار switch، شی مناسب بر اساس آن با تابع drawSquare() و تابع drawCircle() کشیده میشود. در این ساختار switch دو نوع مربع (Square) و دایره (Circle) بررسی شده است. حال اگر بخواهیم شکل دیگری غیر از مربع و دایره را بکشیم چه باید کرد؟
همانطور که حدس میزنید مجبوریم یک case دیگر برای شکل جدید در switch وارد کنیم. برای هر شکل جدیدی مجبوریم switch را بارها و بارها تغییر دهیم و به کد اصلی دست ببریم که این دقیقا بر خلاف اصل باز و بسته بودن است! بنابراین مشکل را به شکل زیر حل میکنیم:
interface Shape {
public function draw();
}
class Square implements Shape {
public function draw()
{
// Draw Square Here
echo "square";
}
}
class Circle implements Shape {
public function draw()
{
// Draw Circle Here
echo "circle";
}
}
class Draw {
public function drawAllShapes($shapes) {
for ($i = 0 ; $i < count($shapes) ; $i++)
$shapes[$i]->draw();
}
}
$draw = new Draw();
$square = new Square();
$circle = new Circle();
$shapes = array($square , $circle);
$draw->drawAllShapes($shapes);
اینبار یک Interfaceبا نام Shape تعریف کرده ایم. یک متد draw() درون این Interface تعریف شده است. هر کلاسی که بخواهد نوع جدیدی از شکل را معرفی کند مجبور است این Interface را Implement کرده و متد draw() را به سبک خود پیاده سازی کند.
سپس برای ساختن حلقه نمایش اشکال در متد drawAllShapes() مثل سابق لیست اشکال را از طریق پارامترهای متد دریافت میکنیم. در داخل حلقه کافی است متد draw() از هر شی shape موجود در لیست shapes را فراخوانی کنیم تا شکل به سبک خودش نمایش داده شود!
اصل باز و بسته بودن و Interface ها
در تعریف اول اصل باز و بسته بودن، برتراند میر (Bertrand Meyer) در کتاب با نام ساختار نرم افزار شی گرا (Object Oriented Software Construction) اینطور نوشته است:
"موجودیتهای نرم افزار (کلاس، ماژول، تابع و...) باید برای گسترش باز اما برای تغییر بسته باشند."
اما مشکل این تعریف در آن است که به طور تنگاتنگی با Inheritance یا وراثت در رابطه است. بنابراین کلاسهای فرزند با ارث بردن از کلاس والد تا حد زیادی به جزئیات ساختار والد وابسته میشدند. این روند باعث ایجاد اتصال تنگاتنگ (Tight Coupling) میشد.
برای آشنایی بیشتر با این مشکل بهتر است در مورد اصول طراحی کد بیشتر صحبت کنیم. به طور کلی اصول طراحی کد بر اساس این دو اصل استوار است:
High Cohesion یا انسجام بالا
Low Coupling یا اتصال کم (Loosely Coupling)
به طور خلاصه انسجام بالا یعنی این که متدهای یک کلاس همه در خدمت یک هدف باشند. اتصال کم یعنی ماژولهای یک برنامه نباید با یکدیگر در هم تنیده و بیش از حد متصل باشند. در صورت وجود چنین مشکلی در برنامه، اصطلاحا آن برنامه را (Tight Coupled) مینامند. در برنامههای در هم تنیده پس از ایجاد تغییر در یک کلاس مجبوریم کلاسهای وابسته و مربوط دیگر را هم تغییر دهیم. یعنی تغییر کوچکی در برنامه باعث بر زمین ریختن آوار برنامه و ارورهای مکرر میشود.
به اصل باز و بسته بودن بر میگردیم. گفتیم در صورتی که در این اصل از Inheritance استفاده کنیم، باعث ایجاد اتصال تنگاتنگ در برنامه میشویم. بنابراین عمو باب تعریف تازه ای از اصل باز و بسته بودن ارائه داد که به جای وراثت بر چندریختی (پلی مورفیسم) استوار است. حالا به جای سوپرکلاسها یا کلاسهای والد از Interface استفاده میکنیم. با این کار برای جایگزینی کد به جای درگیر شدن در جزئیات سوپرکلاس ها، تنها Interfaceها را پیاده سازی میکنیم و به گسترش برنامه میپردازیم. اما به هر حال انتخاب بین وراثت و چند ریختی برای رعایت اصل باز و بسته بودن انتخاب شماست!
با خواندن این مطلب مهمترین اصل برنامه نویسی شی گرا را یاد گرفتیم! اصل باز و بسته بودن یکی از اصول پنجگانه از SOLID است. با رعایت کردن این اصل، با تغییر یک کلاس یا ماژول، احتیاجی به تغییر دادن همه کدهایی که از آن استفاده کرده اند نداریم. آیا شما تجربه ای از رعایت کردن یا نکردن این اصل داشته اید؟ استفاده از این اصل چه کمکی به شما برای داشتن کدهای بهتر کرده است؟ از خواندن نظرات شما خوشحال میشویم!
۴ دیدگاه
۲۶ مهر ۱۴۰۱، ۱۴:۳۳
ممنون بابت مثالتون
فرض کنید ما یه متغییر ورودی میگیرم و بر اساس اون تشخصی میدیم کدوم shape رو بکشیم مثلا اگر ۱ بود square اگر ۲ بود circle اونوقت چطور میشه ؟
باز ما همون switch case یا if elseها رو خواهیم داشت
نازنین کریمی مقدم۲۷ مهر ۱۴۰۱، ۱۷:۴۷
درود
خب اینجا کلا مساله ای که مطرح کردید شرطی هست و ناچارید از switch یا if استفاده کنید. اما برای رعایت کردن این اصول بهتره که یه کلاس جدا به عنوان input detector برای تشخیص رشته ورودی و مپ کردنش به شکل بگذارید تا کدهای کمتری نیاز به تغییر دوباره داشته باشه.
محمد رحمتی۰۴ بهمن ۱۳۹۹، ۲۰:۳۲
سلام . چرا وقتی که داخل متد drawAllShapes متد draw از هر شی shape را فراخوانی میکنیم با اینکه متد draw داخل بدنه کلاس تعریف نشده خطا دریافت نمیکنیم.
نازنین کریمی مقدم۰۴ بهمن ۱۳۹۹، ۲۲:۳۵
درود.
به این دلیل که متد draw برای یک آرایه از اشیا فراخوانی میشود و در حقیقت نیاز هست تا این متد درون کلاسهای این اشیا تعریف شده باشد. اینجا هم این اتفاق افتاده و به همین دلیل خطا نمیگیریم.
برای فهم بهتر میتونم اینطور بهتون توضیح بدم که اون <- موقع فراخوانی، در اصل میگه که بیا از توابع موجود در کلاس اون شی که منو صدا زده، تابع رو صدا بزن. اگر به جای این خط کد: $shapes[$i]->draw(); تابع draw خالی رو مینوشتیم، چون نماینده ای صداش نکرده بود، پیش فرض میرفت داخل بدنه کلاس Draw رو نگاه میکرد و بعد خطا میداد.