پس از آن که در آموزشهای گذشته با مفاهیمی همچون کلاس و آبجکت آشنا شدیم، حال نوبت به آن رسیده تا ببینیم که بر اساس چه اصولی میتوان به صورت علمی اقدام به ساخت کلاسهای مختلف و بالتبع آبجکت هایی از روی آن کلاسها کنیم. به طور کلی، در برنامه نویسی شیء گرا چهار اصل پایهای برای ساخت کلاسها وجود دارد که عبارتند از:
- Abstraction
- Polymorphism
- Inheritance
- Encapsulation
برای به خاطر سپردن این چهار اصل، میتوان ابتدای هر یک از این مفاهیم را در کنار یکدیگر قرار داده و کلمه ی A PIE را به خاطر سپرد. اگرچه این مفاهیم در ابتدای امر کمی ترسناک و ناامید کننده به نظر می رسند، اما واقعیت این است که ما به صورت ناخودآگاه با این مفاهیم آشنا بوده و میشود گفت که هر روز با آنها سروکار داریم. در ادامه، با تک تک این مفاهیم در قالب مثالهایی کاربردی از دنیای واقعی آشنا خواهیم شد.
Abstraction
فرض کنیم در اتاقی نشستهایم و فردی از ما میپرسد که لپ تاپ کجاست؟ ما هم جواب فرد مد نظر را داده و میگوییم روی «میز» است. وقتی ما واژه ی میز را به زبان می آوریم، هرگز نمیگوییم که لپ تاپ روی میزی است که چوبی است، رنگ آن قهوه ای روشن است، روی آن یک شیشه قرار دارد، چهار پایه دارد، طول و عرض آن فلان اندازه است و … ما صرفاً واژه ی میز را به زبان آورده و طرف مقابل کاملاً متوجه میشود که منظور ما کدام میز است! هم ما و هم طرف مقابل مان که در اتاق حضور داریم تجربیاتی در برخورد با شیئ به نام «میز» داشته و داریم که به ما کمک کند تا منظور از میز را متوجه شویم. واژه ی Abstraction به معنی «انتزاعی بودن» است. یعنی چیزی که در حد یک ایده است و وجود خارجی ندارد یا این که نمیشود آن را لمس کرد.
در برنامه نویسی شیء گرا، Abstraction به این قضیه اطلاق میشود که ما به جای تمرکز روی یک نمونه ی عینی از چیزی، روی مفهوم چیزها متمرکز شویم و مفهومی کلی را در نظر بگیریم. Abstraction به مفهومی انتزاعی در ذهن برنامه نویس گفته میشود که کلی است و هیچ ارتباطی با یک آبجکت خاص ندارد.
همانطور که گفتیم، ما این چهار اصل در برنامه نویسی شیء گرایی را به طور ناخودآگاه در زندگی روزمره ی خود به کار میبریم و جالب است بدانیم که قلب برنامه نویسی شیء گرایی نیز همین Abstraction است. در واقع، وقتی که ما یک کلاس می سازیم، داریم به صورت انتزاعی و مفهومی، ایده ای را می پرورانیم که هیچ وجود خارجی ندارد تا این که یک شیء از روی آن کلاس ایجاد کنیم.
Abstraction حاکی از آن است که ما اصلاً نیازی نیست تا یک کلاس برای BehzadAccount و کلاس دیگری برای AliAccount بسازیم بلکه یک کلاس مفهومی تحت عنوان BankAccount خواهیم ساخت که از روی آن میتوان به تعداد بی نهایت آبجکت ایجاد کرد.
Encapsulation
این مفهوم از واژه ی Capsule که به همان کپسولی اطلاق میشود که وقتی مریض میشویم میل می کنیم گرفته شده است. کپسول چیزی است که غشایی دور چیزی یا چیزهای خاصی ایجاد میکند تا علاوه بر دور هم نگه داشتن آنها در کنار یکدیگر، از آن ها محافظت نیز کند.
در برنامه نویسی، Encapsulation به زمانی اطلاق میشود که ما Attribute ها وBehavior های یک آبجکت را در کپسولی فرضی قرار داده و آنها را کنار یکدیگر قرار دهیم. علاوه بر این، با به کارگیری مفهومی تحت عنوان Encapsulation ما خواهیم توانست دسترسی به بخش یا بخشهایی از یک کلاس یا آبجکتی که بر اساس آن کلاس نوشته شده را محدود کنیم. در مثال حساب بانکی، فرض کنیم ویژگیهایی داریم همچون شماره حساب و موجودی. از این دو صرفاً شماره حساب است که میتواند در اختیار سایر بخشهای نرمافزار قرار گیرد و این در حالی است که سطح دسترسی به ویژگی موجودی با مفهومی همچون Encapsulation محدود شده است.
حال ممکن است این سؤال برای برنامه نویسان مبتدی پیش بیاید که اگر من خودم اقدام به ساخت یک کلاس می کنم، چه لزومی دارد که بخش یا بخشهایی از کلاس ساخته شده را از خودم پنهان کنم؟ در پاسخ به چنین سؤالی بایستی گفت که در اینجا قضیه پنهان کاری و … نیست، بلکه در اینجا منظور به حداقل رساندن وابستگی میان بخشهای مختلف برنامه است. در چنین شرایطی، ایجاد یک تغییر کوچک در کلاس مد نظر -مثلا محدود کردن سطح دسترسی به برخی قابلیتهای کلاس- فوراً روی تمامی آبجکت های ساخته شده از روی این کلاس اعمال خواهد شد. در ادامه، ممکن است سؤال دیگری به ذهن خطور کند که تا چه حدی می بایست این خصوصیات را از دید سایر بخشهای برنامه پنهان ساخت؟ که در پاسخ به این سؤال هم بایستی گفت که هرچه بیشتر، بهتر!
زبانهای برنامه نویسی شیء گرای مختلف، تعریف مخصوص به خود را در ارتباط با مفهوم Encapsulation یا پنهان سازی بخشهایی از کلاس از دید سایر بخشهای نرمافزار دارند. آنچه در اینجا حائز اهمیت میباشد این است که مفاهیمی همچون Abstraction و Encapsulation در کنار یکدیگر، این فضا را برای ما -به عنوان برنامه نویس- ایجاد میکنند تا به هر میزانی که تمایل داشته باشیم مفاهیمی انتزاعی ایجاد کرده تا در آینده به هر میزان سطح دسترسی که تمایل داشته باشیم از روی آنها اشیائی خاص بسازیم.
Inheritance
در سادهترین شکل ممکن، Inheritance را میتوان به «استفاده ی مجدد از کدهای از قبل نوشته شده» ترجمه کرد. معنی لغوی Inheritance «وراثت» است. برای روشنتر شدن این مسأله مثالی عملی می زنیم. فرض کنیم که در نرمافزار خود، ما کلاسی ایجاد کردهایم تحت عنوان Person به معنی «انسان» که دارای یکسری خصوصیات است که از آن جمله میتوان به name, phoneNumber و email اشاره کرد که به ترتیب به معنی «نام»، «شماره تلفن»، «ایمیل» اند. این کلاس دارای یکسری عملکرد نیز هست که به طور مثال میتوان به متدی تحت عنوان ()changeEmail یا قابلیت «تغییر آدرس ایمیل» اشاره کرد. اکنون قصد داریم اقدام به ساخت کلاس جدیدی کنیم مثلاً تحت عنوان Customer به معنی «مشتری» که این کلاس دقیقاً شبیه به کلاس Person است. به عبارت دیگر دارای خصوصیاتی مثل name, phoneNumber و email است و همچنین عملکردی تحت عنوان ()changeEmail نیز دارا است. علاوه بر این موارد، کلاس مشتری دارای Attribute یی تحت عنوان customerNumber به معنی «شناسه ی مشتری» است که کلاس انسان فاقد آن است.
در چنین شرایطی ما دو راه کار پیش رو داریم: راه کار اول این که یک کلاس جدید همچون کلاس Person ساخته سپس قابلیت customerNumber را به آن اضافه کنیم یا این که کلاس جدیدی تحت عنوان Customer ساخته، صرفاً قابلیت customerNumber را در آن قرار داده سپس کاری کنیم که این کلاس جدید سایر خصوصیات و قابلیت هایش را از کلاس Person اصطلاحاً «به ارث ببرد» که این همان چیزی است که زبانهای برنامه نویسی شیء گرا برای آن ساخته شده اند.
از این پس، کلاس مشتری کلیه ی قابلیتهای کلاس انسان را دارد و جالب است بدانیم در صورتی که تغییری در کلاس انسان ایجاد کنیم، کلیه ی آن تغییرات در کلاس مشتری نیز اعمال خواهند شد. حال ممکن است این سؤال برای شما پیش بیاید که چرا از همان ابتدا چیزی مثل customerNumber را داخل کلاس انسان قرار ندادیم. پیش از این هم توضیح دادیم که ما در برنامه نویسی شیء گرا می بایست تا حد ممکن به اولین اصل یا همان Abstraction احترام بگذاریم و کلاسهایی که ایجاد میکنیم می بایست تا حد ممکن کلی باشند. اگر ما چیزی مثل شناسه ی مشتری را داخل کلاس انسان قرار می دادیم، این کلاس را تا حدودی محدود می کردیم. فرض کنیم که در آینده شرایطی ایجاد میشد که مجبور میشدیم که کلاسی تحت عنوان Seller به معنی «فروشنده» ایجاد می کردیم. در چنین شرایطی اگر کلاس Seller از کلاس Person ارث بری می کرد، دارای قابلیتی همچون customerNumber بود که این ویژگی به هیچ وجه به درد اشیاء ساخته شده از روی کلاس Seller نمی خوردند.
به خاطر داشته باشید |
در برنامه نویسی شیء گرایی، کلاس Person تحت عنوان Superclass یا «کلاس اصلی» و کلاس Customer تحت عنوان Subclass یا «کلاس زیرشاخه» شناخته می شود. از یک بعد دیگر، کلاس Person را میتوان کلاس Parent یا «والد» و کلاس Customer را میتوان کلاس Child یا «فرزند» تلقی کرد. |
در زبانهایی همچون ++C این امکان در اختیار برنامه نویس قرار گرفته تا بتواند از بیش از یک کلاس والد ارث بری کند که چنین قابلیتی تحت عنوان Multiple Inheritance یا «وراثت چندگانه» شناخته می شود. اگر چه چنین قابلیتی دست برنامه نویسان را خیلی باز می گذارد، اما در عین حال چنین قابلیتی منجر به پیچیدهتر شدن برنامه میگردد و به همین دلیل است که برخی زبانهای برنامه نویسی همچون جاوا، سی شارپ، آبجکتیو سی، روبی و … به هیچ وجه اجازه ی ارث بری از بیش از یک کلاس والد را به برنامه نویس نمی دهند.
Polymorphism
آخرین اصل از اصول برنامه نویسی شیء گرا Polymorphism نام دارد. Poly به معنی «چندین» است و Morph هم به معنی «شکل» است که روی هم رفته میتوان آن را به «چند شکلی» ترجمه کرد. این اصل در مقایسه با سه اصل اول اصول شیء گرایی کمی پیچیدهتر است اما سعی میکنیم با مثالهایی ملموس، مفهوم Polymorphism را برای برنامه نویسان مبتدی روشن کنیم.
در ابتدا مثالی از این اصل میزنیم که شاید تا به حال خیلی در مورد آن فکر نکرده باشیم. علامت + را در نظر بگیرید. علامت + به چه معنا است؟ در بسیاری از زبانهای برنامه نویسی این علامت دارای کاربردهای متنوعی است. اگر ما بخواهیم دو متغیر همچون a و b را با علامت + به صورت a + b در کنار یکدیگر قرار دهیم، بسته به این که این متغیرها چه مقداری داشته باشند، از علامت + می بایست انتظار عملکردی متفاوت داشته باشیم. به طور مثال، اگر مقدار متغیر a برابر با 7 و مقدار متغیر b برابر با 2 باشد، علامت + حاصل جمع جبری این دو متغیر که برابر با عدد 9 است را محاسبه خواهد کرد. حال فرض کنیم مقادیر مرتبط با این دو متغیر استرینگ باشند. به عبارت دیگر، مقدار متغیر a برابر با 'Behzad' و مقدار متغیر b برابر با 'Moradi' باشد که در چنین شرایطی علامت + به هیچ وجه حاصل جمع متغیرها را در اختیار ما قرار نخواهد داد بلکه این دو استرینگ را در کنار یکدیگر قرار داده و نتیجه ی 'BehzadMoradi' را در اختیار ما قرار میدهد که به چنین چیزی در برنامه نویسی اصطلاحاً Concatenation به معنی «الحاق» گفته می شود.
نکته |
String (استرینگ به معنی رشته) به نوع خاصی از کلاس ها در برنامه نویسی گفته می شود که می توانند دربرگیرنده ی هر نوع داده ای از حروفی مثل a, F, GG گرفته تا علائمی همچون !, % و * باشند. |
در واقع، از مثال فوق این نتیجه را میتوان گرفت که علامت + بسته به این که چه نوع دیتایی در اختیارش قرار گیرد، عملکرد متفاوتی از خود نشان خواهد داد. در مورد اول، عملکردش به این شکل است که مقادیر متغیرها را جمع جبری میکند و در مورد دوم، مقادیر را در کنار یکدیگر قرار می دهد.
با در نظر گرفتن این مقدمه، حال قصد داریم ببینیم که اصل چند شکلی در شیء گرایی چگونه نمود عینی پیدا خواهد کرد. مجدد به مثال حساب بانکی باز می گردیم. فرض کنیم که یک Superclass داریم تحت عنوان BankAccount که دارای یکسری Attribute مثل accountNumber و accountType به ترتیب به معنی «شماره حساب» و «نوع حساب» است. این کلاس علاوه بر این دارای یکسری Behavior نیز هست که از آن جمله میتوان به متدهایی همچون ()open و ()close و یا ()withdraw به ترتیب به معنی «باز کردن حساب»، «بستن حساب» و «برداشتن موجودی» می باشد.
فرض کنیم که قصد داریم یکسری کلاس جدید ایجاد کنیم مثلاً با نام های InvestmentAccount و SavingAccount و CurrentAccount به معنی «حساب سرمایه گذاری»، «حساب پس انداز» و «حساب جاری». این حسابهای بانکی همگی قابلیتهایی همچون باز و بسته کردن حساب، برداشت موجودی و یا شماره ی حساب و نوع حساب را دارند پس به سادگی میتوانیم برنامه ی خود را طوری بنویسیم که این کلاسها از کلاس اصلی قابلیت هایشان را به ارث ببرند اما توجه داشته باشیم که هر کدام از این کلاسها دارای یکسری ویژگیهای مخصوص به خود هستند. به طور مثال، حساب پس انداز می بایست دارای چیزی همچون interestRate یا «بهره ی بانکی» باشد که حسابهای سرمایهگذاری و جاری فاقد آن هستند. اما قضیه به اینجا ختم نشده و شرایط تاحدودی پیچیدهتری از آن چیزی است که در ظاهر به نظر می رسد. فرض کنیم بانکی که سفارش این نرمافزار را به ما داده است، قانونی گذاشته مبنی بر این که اگر یک مشتری از حساب سرمایهگذاری خود تا قبل از یک سال پولی برداشت کند، می بایست 30 درصد جریمه دهد اما مشتریانی که حساب پس انداز یا جاری دارند، به سادگی و بدون هیچ جریمه ای هر وقت که تمایل داشته میتوانند Withdraw کنند یا «از حساب خود پول برداشت کنند».
متد ()withdraw در کلاس والد تعریف شده است اما این در حالی است که ما در کلاس حساب سرمایه گذاری، خواهیم توانست نوع خاصی از آن متد را ایجاد کنیم که چنین کاری اصطلاحاً در برنامه نویسی Override یا «رونویسی» گفته می شود. به عبارت دیگر، ما متد ()withdraw که در کلاس BankAccount قرار دارد را در کلاس InvestmentAccount اصطلاحاً Override کرده و انتظارات مد نظر خود را در آن می گنجانیم که صرفاً مخصوص کلاس InvestmentAccount اند. به طور خلاصه، زمانی که ما بخواهیم چیزی را از کلاس دیگری استفاده کنیم، به سادگی آن را به ارث میبریم اما اگر بخواهیم تغییری در این ارث بری ایجاد کنیم، Attribute یا Behavior مد نظر را Override میکنیم.