برنامه نویسی شی گرا اصلی است که تلاش بر این دارد تا مشکلات دنیای واقعی را به کمک کانسپتی انتزاعی به نام شیئ (Object) بعنوان ساختار داده ای قابل تغییر(imutable) و یا غیر قابل تغییر(mutable) و قابل ارجاع (referenced) شبیه سازی کند.
تمامی زبان های برنامه نویسی انتزاع (abstraction) را برای ما فراهم میکنند.
می توان اینطور گفت که پیچیدگی مسائلی که شما قادر به حل آن باشید ارتباط مستقیمی به نوع و کیفیت انتزاعی سازی دارد.
در اینجا منظور از "نوع" چیستی چیزیست که شما قصد انتزاع سازی آن را دارید. بطور مثال زبان assembly انتزاع کوچکی از ماشینی است که در لایه زیرین قرار دارد.
و بسیاری از زبان های بزرگ و گسترده ای که بعد از آن بوجود آمدند(مثل Basic,Fortran و C) انتزاعی از زبان assembly هستند.
این زبان ها پیشرفت بسیار بزرگی نسبت به اسمبلی بودند، اما انزاع و ساده سازی آن ها همچنان شما را مجاب می کند تا بر اساس ساختار کامپیوتری که در لایه های زیرین قرار دارد به جای ساختار مشکلی که در دنیای وقعی داریم فکر کنیم.
با این محدودیت برنامه نویس مجبور است تا بین مدل ماشین (محیطی که قرار است راه حل بر روی آن اجرا شود مثل یک کامپیوتر ) و مدل و ساختار مسئله ای که در تلاش برای حل آن است(محیطی که مشکل در آن وجود دارد مثل یک کسب و کار) پیدا کند.
رویکرد شی گرا با ابزاریست برای مجسم ساختن المان های محیط بیرون از ماشین درون ماشین انتزاع سازی و ساده سازی را یک قدم به پیش می برد.
با این رویکرد برنامه نویسان دیگر مجبور به در نظر گرفتن ساختار ماشینی که مسئله بر روی آن پیاده سازی می شود نباشند و تنها به خود مسئله و مشکلی که در دنیای واقی وجود دارد تمرکز کنند.
همانطور که گفته شد این رویکرد در بسیاری از پلتفرم ها که از برجسته ترین آنها میتوان به پلتفرم های اینترپرایز و سازمانی و حتی در مقیاس کوچکتر مثل پیاده سازی سمت سرور سایت های نه چندان بزرگ و برنامه های دسکتاپ های رومیزی اشاره کرد. با استفاده از برنامه نویسی شیئ گرا زمان پیاده سازی و کد نویسی بسیار کمتر میشود، از تکرار کد جلوگیری میشود، و درنهایت منجر به داشتن سیستمی پایدار با قابلیت نگهداری و ارتقا بالا میشود.
همانطور که گفته شد از این رویکرد تقریبا در تمامی پلتفرم ها استفاده میشود و اگر توسعه دهنده ای خود را با این رویکرد مجهز نسازد قطعا در آینده ای نه چندان دور قادر به توسعه نرم افزار نخواهد بود و توان رقابت با سایرین را نخواهد داشت.
برای درک بهتر مسائل OOP در این مقاله از کد های java استفاده خواهد شد.
هدف آموزش کامل و عمیق مباحث OOP نیست و تنها در حد آشنایی با کانسپت های ابتدایی و کلی آشنا خواهیم شد.
در سال 1968 برنامه نویسی ساختار یافته (structured) توسط Wybe Dijkstra کشف شد که دنیای برنامه نویسی را متحول کرد. به لطف برنامه نویسی ساختار یافته دیگر خبری از دستور gotoو معایبش نبود و برنامه های نوشته شده توسط بصورت خطی توسط شرط ها و حلقه ها اجرا میشد. Dijkstra فهمیده بود که استفاده از دستورات gotoمانع از کوچک سازی کد های نوشته شده به ماژول های کوچک تر بود زیرا می بایست کل کد ها در یک مکان قرار می داشت تا اجرا با دستور gotoبه خط مورد نظر منتقل شود. با بوجود آمدن زبان های ساخت یافته انقلابی در صنعت برنامه نویسی شکل گرفت و کد های نوشته شده به واحد های کوچکی تقسیم شد که نوشتن، خواندن و تست آنها صد ها برابر راحت تر بود. از زاویه ای دیگر این تغییر باعث شد که این واحد های کوچک شده کد امکان تعویض بدون آسیب به کل برنامه را داشته باشند. مرز دقیقی بین زبان های ساخت یافته (structured) و رویه ای (procedural) نمی توان قرار داد، زبان های روندی به دنباله برنامه نویسی ساخت یافته شکل گرفتند. با این وجود اینطور گفته می شود که کد های زبان های رویه ای با اشیا دنیای بیرون هیچ رابطه ای نداشته و تمرکز آن سیستمی است که در آن پیاده سازی می شود. اگر بخواهیم تعریف جامع و راحت تری داشته باشیم ، امروزه به تمام زبان های غیر شی گرای موجود میتوان رویه ای گفت زبان هایی مثل C، Pascal و Fortran.
همانطور که خواهیم دید بنیاد و پایه ساختار و یا معماری عالی برنامه فهمیدن و بکار بردن اصول OO است. اما Object oriented چیست؟
"ترکیب function و data" یکی از جواب ها است. اما این جواب دقیقی نمی تواند باشد زیرا اینطور می گوید که o.f() با f(o) تفاوت دارد. این نگرش کاملا غلط است زیرا برنامه نویسان مدت ها قبل از شکل گیری OO در سال 1966 زمانی که Dahl و Nygaard استک فانکشن را به حافظه heap منتقل کردند،ساختمان داده به تابع ها پاس می کردند.
بعضی دیگر OO را ترکیب این سه کلمه جادویی می دانند کپسوله سازی(encapsulation) ، ارث بری(inheritance) و polymorphism رویکر شی گرا را ترکیبی از این سه قابلیت می توان دانست و حداقل زبان های شی گرا می بایست این سه قابلیت را دارا باشند.
وارد کردن الگوریتم به یک کامپیوتر را می توان یکی از ساده ترین تعریف های برنامه نویسی دانست. الگوریتم را می توان مجموعه ای از دستورات منطقی دانست که صفر یا چند رودی و حداقل یک تا چند خروجی است.
تشبیهی که بطور معمول برای مثال در این باره استفاده می شود "پختن کبک| است.
ما در زبان فارسی میدانیم که پختن کیک انتزاعی از روند مشخص و معلومی است که میتوانیم به مراحل ساده تری تبدیلش کنیم. بطور مثال :
و چندین مرحله دیگر..
"پختن کیک" انتزاعی از انجام تمام این مراحل در یک جمله است.
کامپیوتر ها در اعماق وجودشان ماشین هایی "ساده" ای هستند که با کمک همین اصل قادر به انجام محاسبات دستورالعمل ها با سرعتی بسیار بالا هستند. میتوانیم این دستورات را بصورت procedural (خطی) واد کنیم و دستورات یکی پس از دیگری انجام خواهند شد.
دستورات در خام ترین ساده ترین حالتشان تنها مجموعه اعدادی دودویی هستند که توسط سیگنال های دیجیتال 0 و 1 و یا آنالوگ(بالا و پایین) تفسیر شده اند.
با تمام توضیحات بالا می توان گفت که انسان ها نمی توانند از طریق باینری با کامپیوتر ها تعامل کنند( یا حتی به ان ها برنامه دهند!) برای دادن دستورات ساده نیاز به مقدار عظیمی عدد باینری داریم.
با وجود زبان های برنامه نویسی دیگر کسی مجبور به نوشتن کد های باینری یا ماشین کد نیست، البته که اولین دانشمندان کامپیوتر این کار را کرده اند.
در نتیجه دستورات سطح پایین(نزدیک به ماشین) به زبان های سطح بالاتر تبدیل شدند(یا abstract شدند) ،زبان هایی که توسط انسان ها با اندکی تمرین و مطالعه قابل تفسیر و فهم باشند.
تکه کد زیر نمونه ای از یک کد procedural می باشد که مربع عددی را بدست می آورد که در این مثال (بدست آوردن مربع عدد) و ذخیره آن در یک متغیرprimitive به نام squareOf2 کامل بنظر میرسد.
اگرچه در مثال بالا تکه کد ما کاربردی و کامل است، اما سناریویی را در نظر گیرید که در آن قصد داریم بدون نوشتن دوباره این تکه کد، مربع اعداد مختلفی را بدست آوریم. در این سناریو میبینیم که نوشتن کد ها بصورت پشت سر هم و کپی کردن آن ها راه مناسبی برای نوشتن برنامه های کاربردی نیست.
تصور کنید میخواهیم مربع 100 عدد را بدست اوریم:
همانطور که مشاهده میکنید برای بدست آورن مربع 100 عدد نیاز به 100 خط کد مشابه داریم. یکی از منطقی ترین راه ها برای رفع این مشکل استفاده از حلقه و یک آرایه است.
کد بالا را این بار با استفاده از یک حلقه بازنویسی میکینیم:
int[] arrayOfsquares = new int[100];
for(int i=1;i<=100;i++){ arrayOfsquares[i]=i*i; }
دیدیم که با استفاده از حلقه و یک آرایه برای نگهداری مقادیر بدست آمده حجم کد را به 4 خط کاهش دادیم.
راه حل بالا راه حلی کامل برای این موضوع بود اما چه می شود اگر بخواهیم از این تکه کد در چند جای برنامه استفاده کنیم؟ اولین راخه حل کپی کردن حلقه و قرار دادن آن در مکان مورد نظر ماست که به نیاز ما پاسخ کامل میدهد پس مشکل کجاست؟
1. افزایش حجم کد: در پروژه های واقعی اما گاهی با هزاران خط کد سر و کار داریم پس کوچک ترین کپی کردن کد ها به حجم کدمان می افزاید.
2. مشکل در تغییر کد ها: تصور کنید تکه کد بالا را 5 بار در قسمت های مختلف برنامه کپی کرده ایم و زندگی عالیست. تصور کنید به علت های مختلفی برنامه ای که داریم باید تغییری یابد و بجای محاسبه مربع اعداد، مکعب آن ها را حساب کند، در این موقعیت برنامه نویس مجبور است تمام 5 تکه کد پیست شده را یکی یکی تغییر دهد. در سناریوی بالا تکه کد ما تنها در 5 جای برنامه کپی شده بود سناریویی را در نظر گیرید که تکه کدی در 20 قسمت از برنامه یا بیشتر کپی شده باشد، نیاز به تغییری کوچک در برنامه مان ممکن است ساعت ها زمان صرف کند. این موارد در مواقعی که باگی در کد duplicate پیدا شود و نیاز به دیباگینگ داشته باشد نیز صدق میکند.
3. کثیف کردن کد ها: پیش تر درباره مشکل در تغییر کد های کپی شده یا به اصطلاح ( Duplicate) شده و سختی هایی که به همراه می آورند گفته شد. حال فرض را بر این می گذاریم که کد های duplicate شده قرار نیست هیچ گاه تغییر پیدا کنند. با وجود این فرضیه نیز این کد ها با کاهش خوانایی برنامه باعث آسیب به برنامه کلی می شوند.
در بالا 3 مورد از آسیب هایی که کد های duplicate شده به برنامه و برنامه نویس وارد میکنند را بیان میکند.
در این قسمت از مقاله منظور از استفاده ز توابع برنامه نویسی تابعی نیست و تنها قابلیت هایی که توایع با خود به همراه می آورند توضیح داده خواهد شد.
در مثال های قبل مشکلات برنامه نویسی روندی یا خطی توضیح داده شد.
به منظور رهایی یافتن از تکرار دستورات می توانیم از توابع استفاده کنیم. توابع کد انجام کاری ساده و مشخص را در برمیگیرند و با استفاده از فراخوانی نام تابع قادر خواهیم بود آن را اجرا کنیم. یک تابع شامل سه بخش می باشد: دستوراتی که تابع موقع فراخوانی اجرا میکند، ورودی های تابع و نام تابع.
پس به عبارتی ساده تر برنامه نویسان با نوشتن کد ها در توابع قابلیت استفاده دوباره به آن ها می دهند و نیازی به دوبار ساهتن چیزی نیست.
در مثال بالا برای بدست آوردن مربع یک عدد، تابعی نوشته شده. تابع بالا پس از محسابه مربع عدد نتیجه را بصورت یک متغیر int باز میگرداند.
int squareOf1= getSquare(1); int squareOf2= getSquare(2);
کد بالا نحوه استفاده از تابع نوشته شده را نمایش می دهد.
در مثال بالا سناریویی را داشتیم که در آن مربع تعداد بالایی از اعداد را با استفاده از یک حلقه و آرایه بدست آوردیم. به تکه کد زیر دقت کنید:
public int getSquare(int[] inputArray){
int[] resultArray=new int[inputArray.length];
for(int i=0;i<inputArray.length;i++){
resultArray[i]= getSquare(inputArray[i]);
}
return resultArray;
}
در تکه کد بالا آرایه ای داریم هم نام با آرایه قبلی با ای تفاوت که data type ورودی آن باآرایه قبلی متفاوت است و همین امر باعث می شود که این دو آرایه همنام با یکدیگر متفاوت شوند که به این کار Overloading گفته می شود. تابع جدید آرایه ای از اعداد را گرفته ، مربع تک تک آنها را محاسبه کرده و در آرایه ای دیگر با نام resultArray ذخیره میکند و پس از اتمام محاسبه تمامی ورودی ها آرایه بدست آمده را باز میگرداند.
تکه کد زیر نحوه استفاده از آن را نشان می دهد:
int inputArrray={10,5,9,3,8,11};
int[] result= getSquare(inputArray);
در این بخش درباره توابع، نحوه استفاده از آن ها و مزیت های آن گفته شد. بطور خلاصه نتیجه نهایی استفاده از توابع، قطعه کدی است که تنها یک وظیفه مشخص دارد(مثل بدست آوردن مربع یک عدد). با توابع می توان الگوریتم های بزرگ را به بخش های کوچک تر تبدیل نمود.
شی (Object) ها فرم دیگری از یک ساختمان داده می باشند که در زبان های شی گرا استفاده می شوند.
ساختمان داده هایی هستند که اشیای دنیای واقعی را نمونه سازی میکنند. بطور ساده تر Object ها کمی از یک آرایه پیشرفته تر می باشند اما بسیار انعطاف پذیر و قابل تغییر اند.
ها با هدف اینکه به برنامه نویس انعطاف پذیری بیشتری در مدل سازی دنیای واقعی دهد پا را از آرایه ها فراتر میگذارد. طور معمول تقریبا هرچیزی که بتوان به آن فکر کرد را می توان در قالب یک object در برنامه داشت.
پس به زبان دیگر تا وقتی که شیئی که به آن می اندیشیم دارای منطق و خصوصیات مشخص باشد می توانیم آن را تبدیل به یک ساهختمان داده بسیار انعطاف پذیر به نام object کنیم.
در تکه کد زیر نحوه تعریف یک کلاس در زبان java را مشاهده می فرمایید.
//در انیجا کد قرار میگیرد
آبجکت ها را می توان به دو بخش تقسیم نمود:
1.صفات(Properties):
متغیر هایی که مقادیر مورد نیاز object را در خود ذخیره میکنند.
2.عملکر(Behaviour):
که به مجموعه متد های درون object گفته می شود.
بطور مثال برنامه ای در رابطه با یک مدرسه می خواهیم بنویسیم و object ای به نام Teacher داریم که نقش معلمان را در برنامه به عهده دارد. اطلاعاتی از قبیل سن، شماره شناسنامه و یا کلاس هایی که توسط ایشان تدریس می شود را properties یا خصوصیات این Object می نامیم.
نکته:در دنیای برنامه نویسی از کلمات properties ،fields، attributes بجای یکدیگر استفاده میکنند و تمام این ها به معنی اطلاعاتی است که درون object ذخیره می شود.
پس از آشنایی با Properties نوبت به Behavior می رسد که به متد های کلاس گفته می شود. برای مثال کلاس Teacher متد های teach() و takeTest() را داراست که Behavir این object را تشکیل می دهند. علاوه بر متد های مختص در هر کلاس ، متد های دیگری به نام های getter و setter وجود دارد. وظیفه متد getter همانطور که از اسمش پیداست بازگرداندن مقادیر field ها به کاربر است و setter وظیفه مقدار دهی به را دارد.
نکته: به object ای که دارای متد های setter برای مقادیرش باشد اصطلاحا mutable گفته می شود. بهتر است تا حد ممکن object ها را در حالت immutable نگه داشت زیرا تغییر مقدار field ها ممکن است امنیت پردازش را مخصوصا در برنامه های همروند به خطر اندازد.
نکته: متد دیگری به نام constructor یا سازنده باید در تمام کلاس ها وجود داشته باشد. این فانکشن در هنگام ساخت object اجرا می شود و در صورت نیاز مقادیر ائولیه ای را به field ها وارد میکند.
تکه کد زیر کلاس Teacher را تعریف میکند:
public class Teacher{
//Properties private String name; private int age; //methods public void teach(){ System.out.println("teaching"); }
}
با توضیحات بالا مشاهده کردید که object ها تنها نوع دیگری از ساختمان داده ها هستند که می توانند تغییر کنند و بیشتر از ارایه ها و متغیر ها انعطاف پذیری دارند. برنامه نویس می تواند با وارد کردن مقادیر مختلف عملکرد object را تحت تاثیر قرار دهد.
کلاس ها به عنوان طرح کلی(blueprint) برای ساختobject ها هستند. در مثال های قبل نحوه تعریف کلاس Teacher و فیلد ها و متد های آن را دیدیم. در اینجا کلاس Teacher یه عنوان طرحی برای ساخت object ای از جنس Teacher استفاده خواهد شد و متد teach درون آن صدا زده می شود.
Public class Main{
public static void main(String[] args) { Teacher teacherObject=new Teacher(); teacherObject.teach(); }
}
همانطور که در طبیعت ، فرزندان خصوصیات والدینشان را به ارث می برند در رویکرد برنامه نویسی شی گرا نیز چنین می باشد.
بطور مثال برنامه یک کارخانه ماشین سازی را در نظر بگیرید. فرض کنیم این کارخانه ماشین های مختلفی در مدل های اسپورت، خوانوادگی و وانت تولید میکند.
در این سناریو می توانیم با ساخت یک کلاس والد (super class) که تمام ویژگی های اصلی که هر ماشینی دارا می باشد ،طرحی کلی(blueprint) برای تمامی ماشین ها درست کنیم.
public class Car { private String name; protected int speed; public void move(){ System.out.println("Moving"); } public String getName() { return null; } }
حال دو فرزند با نام های sport و van تعریف میکنیم که از کلاس اصلی ارث بری میکنند.
public class Van extends Car{ public Van(){ super(); speed=200; } @Override public void getName() { return "van" } }
public class Sport extends Car{ public Sport(){ super(); speed=400; } @Override public void getName() { return "sport car" } }
در اینجا دو نمونه یکسان از طرح کلی ماشین را داریم. پس از ارث بری کردن فرزندان از والد برای هر ماشین ویژگی های مخصوص به خود را تعریف میکنیم.
نکته: هیچ محدودیتی بر تعداد کلاس های فرزند که از یک کلاس ارث بری میکنند وجود ندارد. در مثال بالا ما دو زیر کلاس تعریف کردیم اما این رقم می تواند تا بی نهایت ادامه پیدا کند.
بطور مثال کلاس sport دارای حداکثر سرعت بیشتری نسبت به نمونه van خواهد بود.
ارث بری با قدرت خود قابلیتی به نام polymorphism را در زبان های شی گرا ممکن می سازد.
با دانستن اینکه تمامی خودرو های کارخانه از این طرح پیروی میکنند و همگی فرزند کلاس car هستند، می توانیم در جا های مختلف برنامه فارق از نوع ماشین از کلاس والد car استفاده کنیم.
با همین قابلیت می توانیم برنامه هایی بنویسیم که کلاس های آن به یکدیگر وابستگی ضعیف (Loose coupled) داشته باشند. پس تا وقتی که کلاسی که از آن استفاده میکنیم از کلاس والد car ارث بری کرده باشد مشکلی برای برنامه پیش نخواهد آمد.
در تکه کد زیر متغیری از جنس کلاس Car تعریف کرده ایم و سپس با کلاس Van آن را مقداردهی کرده ایم. تکه کد زیر بدون خطا اجرا خواهد شد.
Car van=new Van(); van.getName(); //output => van
نکته: زمانی که متغیری از جنس کلاس والد به آبجکتی از جنس فرزنداشاره کند اصطلاحا upcasting گفته می شود(کد بالا) و زمانی که متغیری از جنس فرزند به آبجکتی از جنس والد اشاره کند downcasting رخ داده است(کد پایین).
Van van=new Car(); van.getName(); //output => null
دیگر مزیت polymorphism رهایی از مشکلات نوشتن تست هاست. با نوشتن تست هایی برای کلاس والد می توانیم تمامی کلاس های فرزند را نیز بدون مشکل تست کرده و از عملکردشان مطمعن شویم.
به قابلیت پنهان سازی کلاس ها، متد ها و field ها، کپسوله سازی یا Encapsulation گفته می شود.
با کپسوله سازی فیلد ها و متد ها در واقع آنان را از دید دیگران پنهان میکنیم. نتیجه این پنهان سازی API ای تمیز تر و پایدار(stable) تر می باشد.
با کبا کپسوله سازی متد ها قادر خواهیم بود هر زمان متد جدیدی (که خروجی یکسانی دارد) جایگزین متد قبلی کنیم بدون اینکه کد کاربران دچار مشکل شود.پسوله سازی متد ها قادر خواهیم بود هر زمان متد جدیدی (که خروجی یکسانی دارد) جایگزین متد قبلی کنیم بدون اینکه کد کاربران دچار مشکل شود.
گفته شد که برنامه نویسی OOP انعطاف پذیری بیشتری به برنامه نویس می دهد و به دنیای واقعی و مشکلاتی که در دنیای واقعی تلاش برای حلشان میکنیم نزدیک تر است،بنابراین برنامه نویس نگرانی بابت محیطی (System) که برنامه در آن پیاده سازی می شود نخواهد داشت.
در قسمت آشنایی با توابع دلیل استفاده از توابع و فلسفه آن توضیح داده شد. همچنین با ساخت چند تابع با پیاده سازی آن نیز اشنا شدیم.
در این قسمت درباره چیستی Object ها صحبت شد. گفتیم که objectها نوعی پیشرفته ای از ساختمان داده ها هستند که به برنامه نویس اجازه تعریف properties و behavior را می دهند.
گفتیم که بطور خلاصه کلاس ها نقشه ای برای ساخت objectها هستند. در این بخش درباره موضوعات ارث بری و کپسوله سازی نیز گفته شد.
امیدوارم این مقاله برای شما مفید واقع شده باشد.
منابع:
https://medium.com/swlh/what-is-object-oriented-programming-f5b42f3ac826
Robert Cecil Martin. (2017) Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall