دانستنی‌ها

ماهیت تکامل واسط‌‌ها در جاوا: وراثت چند‌گانه (2)

این مقاله، توسط مایکل کولینگ به رشتۀ تحریر درآمده و در اولین شماره Java Magazine سال 2018 منتشر شده است، نویسنده، سیر تاریخی توسعه و تکامل واسط‌ها را به دقت بررسی می‌کند. بخش دوم از این نوشتار سه قسمتی را در این‌جا مطالعه کنید.

در بخش اول، مایکل کولینگ، نویسندۀ مقاله معرفی شد. مقاله با بحثی در مورد وراثت آغاز شد. نویسنده، وراثت کد و وراثت نوع را از هم تفکیک کرده و جایگاه کلید‌واژۀ  extends و implements را شرح داد و در ادامه هم وراثت چندگانه و مشکلاتی که ممکن است از آن نتیجه شود، مرور شد. در این بخش، به دنبال پاسخ این سوال هستیم: طراحان جاوا برای پیشگیری و حل مشکلات ناشی از تجویز وراثت چندگانه چه تدابیری اندیشیده‌اند؟

گونه‌ای نجات‌بخش از وراثت نوع

وقتی با دقت به این مشکلات نگاه می‌کنید متوجه می‌شوید که تمام مشکلات حاصل از وراثت چندگانه، به وراثت کد بازمی‌گردد. وراثت چند‌گانۀ کد ایدۀ آشفته ایست؛ اما وراثت چند‌گانۀ نوع، مشکلی ایجاد‌ نمی‌کند. واقعیت مهم دیگری نیز وجود دارد، وراثت چند‌گانۀ کد، چندان مهم و حیاتی نیست، چرا که می‌توان از مفهوم واگذاری (delegation) استفاده کرده و با ایجاد ارجاعی به چند کلاس در داخل یک کلاس، فراخوانی توابع را به سمت آن‌ها هدایت کرده و تمام عملکرد موجود در چند کلاس را به این شکل در یک کلاس جمع کرد. اما وراثت چند‌گانۀ نوع بسیار کاربردی است و مزایای حاصل از آن به سادگی قابل دستیابی نیست.

از این رو طراحان جاوا یک تصمیم عمل‌گرایانه گرفتند، آن‌ها وراثت چندگانۀ نوع را مجاز و وراثت کد را تنها از یک سوپرکلاس امکان‌پذیر کردند.

واسط‌ها

جاوا، برای آن که قوانین مختلفی برای وراثت کد و نوع وضع کند، باید بتواند نوع یک شی را تعیین کند بدون این که کد‌های آن را پیاده‌سازی کند. این کاری است که واسط‌‌ها انجام می‌دهند.

واسط‌ها، تنها با مشخص کردن عنوان تابع (method signature) و بدون پیاده‌سازی آن‌ها، می‌توانند به اشیا نوع ببخشند. در نتیجه به محدودۀ وراثت کد نیز وارد نمی‌شوند. در واسط‌‌ها نیازی به افزودن فیلد و بدنۀ تابع نیست و می‌توان از کلید واژه‌‌هایی مثل public static final برای ثابت‌ها و public برای متد‌‌ها استفاده کرد. هر چند اگر نوشته نشوند نیز کامپایلر آن‌ها را به همین شکل در نظر می‌گیرد.

آرایشِ گفته‌شده از ساختار‌های زبان، دو نوع از وراثت را در اختیار ما قرار می‌دهد. می‌توان وراثت از یک کلاس را با استفاده از کلید واژۀ extends انجام داد که در آن هم کد و هم نوع به ارث برده می‌شود، یا وراثت از واسط را با استفاده از کلید وازۀ implements انجام داد که فقط نوع به ارث می‌رسد. حالا باید قوانینی برای سازمان‌دهی وراثت چند‌گانه داشته باشیم. جاوا وراثت چند‌گانه را با واسط‌‌ها محیا کرده و وراثت از کلاس‌‌ها را تنها محدود به یک سوپرکلاس می‌کند.

مزایای وراثت چند‌گانه برای انواع

مزایای مجاز شمردن وراثت چند‌گانه بسیار واضح است. به این وسیله می‌توان در مواقع مختلف به یک شی از زوایای متنوعی نگاه کرد. به عنوان مثال، یک برنامۀ شبیه‌ساز ترافیک را در نظر بگیرید. در این نرم‌افزار کلاسی به نام Car دارید. غیر از خودرو‌‌ها اشیاء کنشگر دیگری مانند پیاده‌ها، کامیون‌‌ها، چراغ‌‌های راهنمایی و رانندگی و … وجود دارد. حال تصور کنید می‌خواهید یک List از اشیای کنشگر را نگهدارید.

private List<Actor> actors;

در این حالت Actor می‌توانست یک واسط دارای متدی به نام act باشد:

public interface Actor {
    void act();
}

کلاس Car هم می‌تواند این واسط را به شکل زیر پیاده‌سازی کند:

class Car implements Actor {
    public void act() {
    ...
    }
}

دقت کنید که Car تنها نوع و تعریفِ act را از این واسط به ارث برده و هیچ کدی دریافت نکرده است. در نتیجه باید کدی به عنوان بدنۀ متد عرضه کند ( به اصطلاح: پیاده‌سازی واسط) تا بتواند قادر به ساخت شی شود.

آنچه تا این جا شرح داده شد وراثت یگانه بود که می‌توانستیم آن را به وسیلۀ کلاس‌‌ها انجام دهیم. اما تصور کنید حالا لیست دیگری از اشیایی بسازیم که قابل رسم روی نمایشگر هستند. این لیست متفاوت از لیست کنشگرها یا Actor هاست، چرا که تمام کنشگرها لزوما قابل نمایش نیستند و تمام قابل نمایش‌‌ها، در طول برنامه، لزوما عکس‌العملی از خود نشان‌ نمی‌دهند.

private List<Drawable> drawables;

هم‌چنین ممکن است بخواهید شرایط موجود در نرم‌افزار شبیه‌سازی را در یک حافظۀ قابل‌بازیابی ذخیره کنید. در نتیجه لازم است اشیا، واسطی به نام Serializable را نیز پیاده‌سازی کرده باشند:

private List<Serializable> objectsToSave;

در شرایطی که توصیف شد، اگر کلاس Car عضوی از هر سه لیست مذکور باشد – یعنی هم بتواند عکس العمل نشان دهد، هم قابل رسم باشد و هم قابل ذخیره‌سازی باشد- هنگام تعریف کلاس باید هر سه واسط را پیاده‌سازی کند:

class Car implements Actor, Drawable, Serializable ...

شرایط این‌چنینی چندان نامتداول نیست. معتبر دانستن سوپرنوع‌‌های چند‌گانه اجازه می‌دهد بتوانیم یک شی (مثلا Car در این مثال) را از نما‌‌های مختلف ببینیم، آن‌ها را با اشیای دیگرِ دارای اشتراک، در یک گروه قرار دهیم و بر اساس بخشی از رفتار اختصاصی‌ای که می‌توانند از خود نشان دهند با آن‌ها تعامل کنیم.

پردازش رویداد در کتابخانه‌‌های واسط گرافیکی جاوا با ایدۀ مشابهی طراحی شده است. مدیریت رویداد‌‌ها به وسیلۀ اشیایی انجام می‌شود که به اصطلاح listener‌‌های رویداد‌اند. عموما واسط‌‌هایی مانند ActionListener وجود دارد که تنها یک متد دارند و کلاس‌‌هایی که آن‌ها را پیاده‌سازی می‌کنند می‌توانند به عنوان Listener‌‌های رویداد‌‌هایی که از تعامل با المان‌‌های واسط گرافیکی به وجود می‌آید، در نظر گرفته شوند.

کلاس‌‌های Abstract

ربط کلاس‌‌های Abstract به واسط‌‌ها گاهی برای برخی مبهم است، بهتر است اینجا مقداری به موضوع بپردازیم. کلاس‌‌های Abstract چیزی مابین کلاس‌‌ها و واسط‌‌ها هستند. آن‌ها یک نوع تعریف می‌کنند و مانند کلاس‌‌ها می‌توانند حاوی کد هم باشند. در عین حال می‌توانند حاوی متد‌‌های Abstract هم باشند. این کلاس‌‌ها فقط تعریف می‌شوند اما پیاده‌سازی‌ نمی‌شوند. می‌توانید آن‌ها را به عنوان کلاس‌‌هایی که نصفه پیاده‌سازی شده‌اند در نظر گرفت. این کلاس‌‌ها بخش‌‌های تکمیل نشده یا متد‌‌های پیاده‌سازی نشده‌ای دارند که باید توسط زیرکلاس‌‌ها عرضه شوند.

در مثال‌‌های ما، Actor که تا این جا آن را به عنوان واسط تعریف کرده بودیم می‌توانست یک کلاس Abstract باشد. متد act می‌توانست Abstract باشد، چرا که این متد برای هر Actor متفاوت است و پیاده‌سازی پیش‌فرض معقول و مشترکی هم وجود ندارد. اما ممکن است حاوی کد‌‌های دیگری باشد که مورد استفادۀ تمام زیر‌کلاس‌‌ها قرار بگیرد.

در این صورت می‌توانیم Actor را یک کلاس Abstract تعریف کنیم، در نتیجه تعریف کلاس Car به این شکل خواهد بود:

class Car extends Actor implements Drawable, Serializable ...

اگر بخواهیم بیش از یکی از واسط‌هایمان حاوی کد باشد، ایدۀ تبدیل همۀ آن‌ها به کلاس‌‌های Abstract کارا نخواهد بود. همان طور که قبلا گفتیم، جاوا تنها وارثت یگانه از کد‌‌ها را مجاز کرده است و این بدین معنی است که تنها یک کلاس بعد از کلید واژۀ extends می‌تواند نوشته شود. وراثت چندگانه تنها برای واسط‌‌ها مجاز است.

اما یک راه دیگر هم وجود دارد: متد‌‌های پیش‌فرض (default) که در جاوا هشت معرفی شدند. در بخش بعد به آن‌ها خواهیم پرداخت.

واسط‌‌های خالی

گاهی با واسط‌‌هایی روبرو می‌شوید که خالی‌اند. تعریف آن‌ها از نام واسط تشکیل شده و هیچ متدی در آن‌ها نیست. به عنوان مثال واسط Serializable که پیش‌تر به آن اشاره کردیم، همین‌طور است. Clonable هم یکی از این واسط هاست. این ساختارها به واسط‌‌های Marker هم مشهور‌اند. از آن‌ها استفاده می‌شود تا اعلام شود کلاس دارای یک ویژگی خاص است و بیشتر به منظور ارائۀ اطلاعات در کد به کار می‌رود تا به منظور پیاده‌سازی یک نوع یا تعیین قرار‌داد بین بخش‌‌های مختلف نرم‌افزار. اما جاوا بعد از نسخۀ 5، annotation‌‌ها را معرفی کرد که روش بهتری برای انتقال فراداده (metadata) است. در حال حاضر دلیل خوبی برای استفاده از واسط‌‌های marker وجود ندارد. اگر جایی احساس نیاز به استفاده از آن‌ها می‌کنید، کاربرد‌‌های annotation‌‌ها را دوباره مرور کنید.

ادامه دارد…

عناوین اصلی بخش بعدی (آخر) مقاله:

  • پدیداری جدید در جاوا 8
  • واسط‌ها، در حال تحول
  • متدهای پیش‌فرض
  • متد‌‌های استاتیک
  • مشکل وراثت لوزی چه می‌شود؟
  • خلاصه و نتیجه‌گیری
  • منبع

مشاهده بخش اول مقاله
مشاهده بخش سوم مقاله

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا