برنامهنویسی شیگرا، یک شیوهی برنامهنویسی است که اجزای اصلی آن را اشیا تشکیل میدهند. زبانهای اولیهی برنامهنویسی بهصورت رویهای بودند به این صورت که رویهها روی کارتها پانچ میشدند و رایانهها با گرفتن دادهها، اقداماتی را به ترتیب روی آنها انجام داده و خروجی را چاپ میکردند. زبانهای رویهای کاربرد زیادی داشتند اما زمانیکه قرار بود برنامهنویس کاری را خارج از ترتیب مقدماتی مراحل انجام دهد، زبانهای برنامهنویسی رویهای پاسخگوی این نیاز نبودند. به همین دلیل زبانهای برنامهنویسی شیگرا مانند #C++ ،C، پایتون، پیاچپی، جاوا اسکریپت و... عرضه شدند.
برای درک بهتر مفهوم برنامهنویسی شیگرا کنترل تلویزیون را در نظر بگیرید. شما با استفاده از این کنترل که میتوانید تلویزیون را از همان جایی که نشستهاید خاموش، روشن یا کانال آن را عوض کنید. کنترل یک شی است که مجموعهای از ویژگیها و رفتارها در آن پنهان شده است اما میتوان بدون نیاز به درک مفهوم میکروچیپها یا سیمکشی داخلی و تنها با فشردن یک دکمه، تلویزیون را کنترل کرد. در برنامهنویسی شیگرا نیز روی عملکرد اشیا تمرکز میشود نه روی قطعه کد مورد نیاز برای تعریف نحوهی رفتار آنها.
وراثت، کپسوله سازی و چندریختی سه اصل اساسی برنامهنویسی شیگرا هستند که مزایای زیادی دارند و باعث شدهاند جهت برنامهنویسی از رویهای به شیگرا تغییر پیدا کند. اما از طرفی برخی برنامهنویسها معتقدند این سه اصل درکنار مزایای خود، معایبی نیز دارند که شاید باعث میشوند پایان برنامهنویسی شیگرا نزدیک باشد. در ادامه به بررسی بیشتر معایب هر یک از سه اصل برنامهنویسی شیگرا میپردازیم.
اصل اول، وراثت
وراثت یا ارثبری در نگاه اول بزرگترین مزیت برنامهنویسی شیگرا به شمار میرود. در برنامهنویسی شیگرا هر شی یک نمونه از کلاس است که میتواند ویژگیهای خود را از کلاسهای دیگر به ارث ببرد و در عین حال ویژگیهای خاص خودش را داشته باشد. بهعنوان مثال در شکل بالا «نقطه»، زیرکلاس «دایرهی توپُر» است. «دایرهی توپر» زیرکلاس «دایره» است و «دایره» زیرکلاس «شکل» است درواقع «شکل» کلاس والد است و سایر کلاسها فرزند هستند. وراثت باعث صرفهجویی در نوشتن کد میشود اما معایبی نیز دارد.
۱- مسئلهی موز میمون جنگل
بهعنوان مثال تصور کنید مشغول کار روی پروژهی جدیدی هستید و میخواهید از کلاسی که در پروژهی قبلی استفاده کردهاید در این پروژه نیز استفاده کنید. شاید با خودتان فکر کنید بهراحتی میتوان کلاس قدیمی را برداشت و در پروژهی جدید استفاده کرد اما درواقع شما به والد آن کلاس نیز نیاز خواهید داشت.
درواقع به والد آن کلاس والد و تمام والدهای بعدی نیز احتیاج خواهید داشت. اگر فکر میکنید مسئله به همینجا ختم میشود در اشتباه هستید؛ زیرا اگر شی شما شامل شی دیگری باشد، به آن شی نیز نیاز خواهید داشت. همچنین به والد آن شی، به والد والد آن شی و تمام والدهای آن شی نیز نیاز خواهد بود.
جو آرمسترانگ، خالق زبان برنامهنویسی ارلنگ (Erlang) میگوید:
مشکل زبانهای برنامهنویسی شیگرا این است که همهی آنها یک محیط ضمنی دارند که با خود حمل میکنند. شما یک موز میخواهید اما به گوریلی برمیخورید که آن موز و کل جنگل را در اختیار دارد.
راهحل مسئلهی موز میمون جنگل
برای حل این مشکل میتوان از ساختن سلسله مراتب عمیق خودداری کرد. اما اگر هدف از ارثبری، استفادهی مجدد باشد، آنگاه محدودکردن این مکانیزم، مزایای استفادهی مجدد را نیز محدود خواهد کرد.
۲- مسئلهی الماس
دیر یا زود این مشکل بزرگتر شده و به یک مسئلهی غیرقابل حل تبدیل میشود. تصویر بالا اگرچه منطقی به نظر میرسد اما اغلب زبانهای برنامهنویسی از مفهوم این شکل حمایت نمیکنند. برای درک بهتر این مسئله قطعه کد زیر را در نظر بگیرید:
Class PoweredDevice {}Class Scanner inherits from PoweredDevice { function start() { }}Class Printer inherits from PoweredDevice { function start() { }}Class Copier inherits from Scanner, Printer {}
هر دو کلاس اسکنر (Scanner) و پرینتر (Printer) تابعی به نام استارت (start) را پیادهسازی میکنند. اگر در تصویر دقت کنید کلاس کُپیر (Copier) از هر دو کلاس اسکنر و پرینتر ارث میبرد بنابراین این سؤال پیش میآید که کلاس کُپیر (Copier) کدام تابع استارت را به ارث میبرد: تابعی که در کلاس اسکنر نوشته شده یا تابعی که در کلاس پرینتر نوشته شده؟ یا هردو؟
راهحل مسئلهی الماس
راهحل ساده این است که این کار را نکنیم. بسیاری از زبانهای برنامهنویسی شیگرا اجازهی چنین کاری را نمیدهند. اما برای مدل کردن و استفادهی مجدد باید چه کار کرد؟ پاسخ استفاده از Contain و Delegate است. قطعه کد زیر را در نظر بگیرد.
Class PoweredDevice {}Class Scanner inherits from PoweredDevice { function start() { }}Class Printer inherits from PoweredDevice { function start() { }}Class Copier { Scanner scanner Printer printer function start() { printer.start() }}
در این قطعه کد، کلاس کپیر بهعنوان نمونهای از کلاس اسکنر و پرینتر تعریف شده و از تابع استارت کلاس پرینتر استفاده میکند. در خط آخر اگر بهجای ()printer.start بنویسم ()scanner.start در این صورت از تابع کلاس اسکنر استفاده خواهد کرد.
اما این راهحل نیز باعث بروز مشکل دیگری در اصل ارثبری خواهد شد.
۳- مسئلهی کلاس پایهی شکننده
یکی از بزرگترین مشکلات هر برنامهنویسی این است که برنامهای که نوشته یک روز به خوبی کار میکند و روز دیگر کار نمیکند. آنها هیچ تغییری در برنامهی خود ندادهاند اما در کمال شگفتی مشاهده میکنند برنامهای که تا دیروز به خوبی کار میکرده، دیگر امروز کار نمیکند.
دلیل این است که تغییراتی در کلاس والد اعمال شده و کل کدها را با مشکل مواجه کرده است. تغییر در کلاس پایه چگونه میتواند کل برنامه را با مشکل مواجه کند؟ قطعه کد زیر که به زبان جاوا نوشته شده را در نظر بگیرید:
import java.util.ArrayList;public class Array{ private ArrayList a = new ArrayList(); public void add(Object element) { a.add(element); } public void addAll(Object elements[]) { for (int i = 0; i < elements.length; ++i) a.add(elements[i]); // this line is going to be changed }}
به قسمتی از کد که با // جدا شده دقت کنید، این قسمت بعدا تغییر خواهد کرد. این کد دو تابع به نامهای ()add و ()addall دارد. تابع ()add یک تک عنصر را با مقدار قبلی جمع میکند و تابع ()addall تعدادی از عناصر را با فراخوانی تابع ()add با مقدار قبلی جمع میکند. حال کلاس مشتقشدهی زیر را در نظر بگیرید:
public class ArrayCount extends Array{ private int count = 0; @Override public void add(Object element) { super.add(element); ++count; } @Override public void addAll(Object elements[]) { super.addAll(elements); count += elements.length; }}
کلاس ArrayCount از کلاس Array مشتق شده با این تفاوت که تعداد عناصر را در متغیر Count ذخیره میکند. حال جزییات این دو کلاس را بررسی میکنیم.
()Array add (تابع add استفادهشده در کلاس Array) عنصری را به ArrayList اضافه میکند.
Array addAll() ،ArrayList را برای هر عنصر فراخوانی میکند.
()ArrayCount add (تابع add استفادهشده در کلاس ArrayCount) ابتدا تابع add والد خود را صدا میزند و سپس متغیر count را یک واحد افزایش میدهد.
()ArrayCount addAll ابتدا تابع ()addAll والد خود را صدا میزند و سپس متغیر count را به تعداد اعداد افزایش میدهد.
همهچیز به خوبی کار میکند تا اینکه قسمتی که در کد نوشته شده و با // جدا شده بود را تغییر میدهیم:
public void addAll(Object elements[]) { for (int i = 0; i < elements.length; ++i) add(elements[i]); // this line was changed }
با اعمال این تغییر همه چیز باز هم به خوبی کار میکند اما کلاسهای مشتقشده دچار تغییر میشوند.
()ArrayCount addAll تابع ()addAll والد خود را صدا میزند. آن هم تابع ()addرا فراخوانی میکند که توسط کلاس مشتقشده اورراید (فرزند متدهای ارثبریشده از والد را تغییر میدهد) شده است. این باعث میشود متغیر count هر بار با فراخوانی تابع add() کلاس اضافه شود و سپس یک بار دیگر با عناصر اضافهشده در تابع ()addAll کلاس جمع شود.
متغیر count دو بار جمع بسته میشود
شخصی که کلاس پایه را تعریف میکند باید به خوبی از عملکرد آن آگاهی داشته باشد زیرا هر تغییر کوچکی روی زیر کلاسها تأثیر میگذارد و عملکرد کل برنامه را مختل میکند.
راهحل مسئلهی کلاس پایهی شکننده
بار دیگر میتوان از از Contain و Delegate و برای جلوگیری از بروز چنین مشکلی استفاده کرد. با استفاده از Contain و Deligate برنامهنویسی جعبه سفید به برنامهنویسی جعبه سیاه تبدیل میشود. در برنامهنویسی جعبه سفید ما باید عملکرد کلاس پایه را با دقت تحت نظر داشته باشم، اما در برنامهنویسی جعبه سیاه تا زمانیکه نتوان تغییری در عملکرد کلاس پایه اعمال کرد، نیازی به درنظرگرفتن عملکرد کلاس پایه نیست.
اما بااینحال زبانهای برنامهنویسی شیگرا قرار بود ارثبری و استفادهی مجدد را برای کاربران راحتتر کنند و این تازه بخشی از مشکلات آنها است.
۴- مسئلهی سلسله مراتب
تصور کنید در شرکت جدیدی مشغول به کار شدهاید و میخواهید اسناد و کارهای مربوطبه شغل جدید را در پوشهای جداگانه در کامپیوتر دستهبندی کنید. برای این کار دو راه دارید، میتوانید فولدری به اسم «اسناد» بسازید و سپس داخل آن یک فولد دیگر به اسم «شرکت» بسازید؛ یا میتوانید فولدری به اسم «شرکت» بسازید و سپس فولدر دیگری به نام «اسناد» داخل آن ایجاد کنید. هر دو راه جواب میدهد اما کدامیک بهتر است؟
ایدهی سلسله مراتب طبقهبندیشده این بود کلاسهای پایه یا والد عمومی هستند و کلاسهای مشتقشده یا فرزند نسخهی تخصصیتر آنها هستند. همچنین هرقدر در سلسله مراتب بیشتر به سمت پایین حرکت میکنیم جزییات آن بیشتر میشود. اما اگر یک کلاس پایه و یک کلاس فرزند، بتوانند جای خود را بهآسانی تغییر دهند، پس این مدل با مشکل مواجه خواهد شد.
حل مسئلهی سلسله مراتب
مسئله این است که سلسله مراتب طبقهبندیشده کار نمیکند و استفاده از سلسله مراتب برای محدود کردن است. اگر دنیای واقعی را در نظر بگیرید مثالهای زیادی از محدود شدن توسط سلسله مراتب و همچنین سلسله مراتب طبقهبندیشده مشاهده میکنید. بهعنوان مثال جورابهای خود را در نظر بگیرید. جورابها در یکی از کشوهای کمد قرار دارند، کمد در اتاق است و اتاق بخشی از خانه است.
همان مثال فولدر فایلهای شرکت را به یاد بیاورید. هیچ اهمیتی ندارد که آنها را به چه ترتیبی ذخیره میکنید مهم این است که فولدرها تگ یا برچسب دارند. مسئلهی الماس نیز با همین روش قابل حل است اما به نظر میرسد با وجود این همه مشکل، دوران استفاده از وارثت رو به پایان باشد.
اصل دوم، کپسولهسازی
ویژگی دوم برنامهنویسی شیگرا کپسولهسازی است. به این معنا که متغیرهای درون شی مجزا باقی میمانند و دسترسی به آنها از خارج امکانپذیر نیست. درواقع این متغیرها داخل شی کپسوله میشوند و امین هستند. به نظر میرسد کپسولهسازی بسیار کاربردی است اما این ویژگی نیز مانند وراثت مشکلاتی دارد.
۱- مسئلهی ارجاع
بهدلیل بازدهی بیشتر، اشیا توسط مقادیر خود به تابع منتقل نمیشوند بلکه توسط ارجاع منتقل میشوند. درواقع توابع، اشیا را منتقل نمیکنند بلکه یک ارجاع یا اشارهگری به شی را منتقل میکنند.
اگر یک شی توسط ارجاع به سازندهی شی منتقل شود، سازنده میتواند ارجاع به شی را در یک متغیر خصوصی قرار دهد که توسط کپسولهسازی حمایت میشود. این یعنی شی منتقلشده دیگر در وضعیت امنی نیست.
راهحل مسئلهی ارجاع
سازنده باید شیای که منتقلشده را شبیهسازی (Clone) کند. نه فقط آن شی بلکه هر شی که داخل آن قرار دارد را نیز باید شبیهسازی کند. مسئلهی اصلی این است که همهی اشیا نمیتوانند شبیهسازی شوند. برخی از آنها منابع سیستمعامل را در اختیار دارند و شبیهسازی در چنین شرایط بیفایده یا غیرممکن است.
تمام زبانهای برنامهنویسی شیگرا چنین مشکلی را دارند و شاید دوران استفاده از کپسولهسازی نیز رو به پایان باشد.
اصل سوم، چندریختی
چندریختی به برنامهنویس امکان میدهد که متدهایی با نام یکسان را روی اشیای مختلف استفاده کند. بهعنوان مثال متد ()move متدی است که تمام مهرههای شطرنج را بهاندازهی یک واحد به همهی جهات حرکت میدهد اما همانطور که میدانید کاربردی نیست. درنتیجه برنامهنویس باید متد ()move جدیدی در زیرکلاس هر مهره تعریف کرده و نوع حرکت مهرهی شطرنج را روی آن اعمال کند. با این کار بعد از هر بار فراخوانی متد ()move باید نوع مهره را بهعنوان ورودی مشخص کند.
ویژگی چندریختی در برنامهنویسی شیگرا کاربردی است اما برای استفاده از آن نیازی به برنامهنویسی شیگرا نیست؛ چراکه اینترفیسها نیز همین کار را انجام میدهند و محدودیتی در ترکیب ویژگیها نیز ندارند.
پس به نظر میرسد چندریختی مبتنی بر اینترفیس جایگزین خوبی برای چندریختی شیگرا است.
کلام آخر
زبانهای برنامهنویسی شیگرا هنوز هم بهطور گسترده مورد استفاده هستند و بسیاری از افراد مبتدی هنوز به مشکلات گفتهشده در این مطلب برخورد نکردهاند. شاید سالها طول بکشد تا شخصی با کاستیهای برنامهنویسی شیگرا برخورد کند که در این صورت میتواند از برنامهنویسی تابعگرا (فانکشنال) استفاده کند.
بهعنوان مثال جاوا اسکریپت، اسکالا، ارلنگ، لیسپ، ریاکت، امال و... زبانهای برنامهنویسی هستند که امکان برنامهنویسی تابعگرا را فراهم میکنند. شاید در آیندهی نزدیک برنامهنویسی شیگرا کنار گذاشته شود و همهی برنامهنویسها به استفاده از زبانهای برنامهنویسی تابعگرا روی آورند.