ویژگیهای زبانهای شیگرا
در جامعه برنامهنویسی هیچ توافقی درباره اینکه یک زبان باید چه ویژگیهایی داشته باشد تا بهعنوان شیگرا در نظر گرفته شود، وجود ندارد. Rust تحت تأثیر بسیاری از پارادایمهای برنامهنویسی قرار گرفته است، از جمله OOP؛ برای مثال، ما ویژگیهایی که از برنامهنویسی تابعی آمده بودند را در فصل 13 بررسی کردیم. میتوان گفت که زبانهای شیگرا برخی ویژگیهای مشترک دارند، یعنی اشیاء، کپسولهسازی (encapsulation) و وراثت (inheritance). بیایید بررسی کنیم که هر یک از این ویژگیها چه معنایی دارند و آیا Rust از آنها پشتیبانی میکند یا خیر.
اشیاء شامل دادهها و رفتار هستند
کتاب Design Patterns: Elements of Reusable Object-Oriented Software نوشته Erich Gamma، Richard Helm، Ralph Johnson و John Vlissides (انتشارات Addison-Wesley Professional، 1994)، که به طور غیررسمی به عنوان کتاب Gang of Four شناخته میشود، یک فهرست از الگوهای طراحی شیگرا است. این کتاب OOP را به این صورت تعریف میکند:
برنامههای شیگرا از اشیاء تشکیل شدهاند. یک شیء شامل دادهها و روشهایی که بر روی آن دادهها عمل میکنند، است. این روشها معمولاً به نام متدها یا عملیات شناخته میشوند.
با استفاده از این تعریف، Rust یک زبان شیگرا است: structها و enumها داده دارند، و بلوکهای impl
متدهایی را برای structها و enumها ارائه میدهند. حتی اگر structها و enumها با متدهایی که دارند اشیاء نامیده نشوند، بر اساس تعریف Gang of Four، آنها همان عملکرد را ارائه میدهند.
کپسولهسازی برای مخفی کردن جزئیات پیادهسازی
یکی دیگر از جنبههایی که معمولاً با OOP مرتبط است، مفهوم کپسولهسازی است، که به این معناست که جزئیات پیادهسازی یک شیء برای کدی که از آن شیء استفاده میکند قابل دسترسی نیست. بنابراین تنها راه تعامل با یک شیء از طریق API عمومی آن است؛ کدی که از شیء استفاده میکند نباید بتواند به جزئیات داخلی شیء دسترسی پیدا کند و دادهها یا رفتار را به صورت مستقیم تغییر دهد. این امکان را به برنامهنویس میدهد که جزئیات داخلی شیء را تغییر داده و بازسازی کند بدون اینکه نیازی به تغییر کدی که از آن شیء استفاده میکند، داشته باشد.
ما در فصل 7 بحث کردیم که چگونه میتوان کپسولهسازی را کنترل کرد: میتوانیم از کلمه کلیدی pub
استفاده کنیم تا تصمیم بگیریم کدام ماژولها، انواع، توابع و متدها در کد ما عمومی باشند، و بهطور پیشفرض همه چیز دیگر خصوصی است. برای مثال، میتوانیم یک struct به نام AveragedCollection
تعریف کنیم که یک فیلد شامل یک بردار از مقادیر i32
دارد. این struct همچنین میتواند یک فیلد داشته باشد که میانگین مقادیر موجود در بردار را نگه میدارد، به این معنا که نیازی به محاسبه میانگین به صورت لحظهای نیست هر زمان که کسی به آن نیاز داشت. به عبارت دیگر، AveragedCollection
میانگین محاسبهشده را برای ما کش میکند. لیستینگ 18-1 تعریف struct AveragedCollection
را نشان میدهد:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
AveragedCollection
struct that maintains a list of integers and the average of the items in the collectionساختار struct
با کلمه کلیدی pub
علامتگذاری شده است تا کدهای دیگر بتوانند از آن استفاده کنند، اما فیلدهای داخل struct همچنان خصوصی باقی میمانند. این نکته در این مثال مهم است، زیرا میخواهیم اطمینان حاصل کنیم که هر زمان مقداری به لیست اضافه یا از آن حذف میشود، میانگین نیز بهروزرسانی میشود. این کار را با پیادهسازی متدهای add
، remove
و average
روی struct انجام میدهیم، همانطور که در لیستینگ 18-2 نشان داده شده است:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
add
، remove
و average
در AveragedCollection
متدهای عمومی add
، remove
و average
تنها راههای دسترسی یا تغییر دادهها در یک نمونه از AveragedCollection
هستند. زمانی که یک آیتم با استفاده از متد add
به list
اضافه میشود یا با استفاده از متد remove
از آن حذف میشود، پیادهسازی هر یک از آنها متد خصوصی update_average
را فراخوانی میکند که بهروزرسانی فیلد average
را مدیریت میکند.
ما فیلدهای list
و average
را خصوصی نگه میداریم تا هیچ راهی برای کد خارجی وجود نداشته باشد که مستقیماً آیتمها را به list
اضافه یا از آن حذف کند. در غیر این صورت، فیلد average
ممکن است با تغییرات list
هماهنگ نباشد. متد average
مقدار موجود در فیلد average
را بازمیگرداند و به کد خارجی اجازه میدهد تا مقدار میانگین را بخواند اما آن را تغییر ندهد.
از آنجایی که جزئیات پیادهسازی ساختار AveragedCollection
را کپسوله کردهایم، میتوانیم به راحتی جنبههایی از آن را در آینده تغییر دهیم. برای مثال، میتوانیم به جای استفاده از Vec<i32>
برای فیلد list
، از یک HashSet<i32>
استفاده کنیم. تا زمانی که امضای متدهای عمومی add
، remove
و average
یکسان باقی بماند، کدی که از AveragedCollection
استفاده میکند نیازی به تغییر برای کامپایل شدن نخواهد داشت. اگر list
عمومی بود، این موضوع لزوماً صادق نبود: HashSet<i32>
و Vec<i32>
متدهای متفاوتی برای اضافه کردن و حذف آیتمها دارند، بنابراین کد خارجی احتمالاً باید تغییر کند اگر مستقیماً list
را تغییر میداد.
اگر کپسولهسازی یکی از جنبههای ضروری برای در نظر گرفتن یک زبان به عنوان شیگرا باشد، Rust این نیاز را برآورده میکند. امکان استفاده یا عدم استفاده از pub
برای بخشهای مختلف کد، کپسولهسازی جزئیات پیادهسازی را ممکن میسازد.
وراثت بهعنوان سیستم نوع و بهعنوان اشتراکگذاری کد
وراثت مکانیزمی است که به یک شیء اجازه میدهد عناصر را از تعریف یک شیء دیگر به ارث ببرد و در نتیجه دادهها و رفتار شیء والد را بدون نیاز به تعریف مجدد آنها به دست آورد.
اگر وراثت باید برای یک زبان وجود داشته باشد تا شیگرا در نظر گرفته شود، Rust یک زبان شیگرا نیست. در Rust، نمیتوانید یک struct تعریف کنید که فیلدها و پیادهسازی متدهای struct والد را بدون استفاده از یک ماکرو به ارث ببرد.
با این حال، اگر به استفاده از وراثت در ابزارهای برنامهنویسی خود عادت کردهاید، میتوانید بسته به دلیل خود برای استفاده از وراثت، از راهحلهای دیگری در Rust استفاده کنید.
دو دلیل اصلی برای انتخاب وراثت وجود دارد. یکی برای استفاده مجدد از کد: میتوانید یک رفتار خاص را برای یک نوع پیادهسازی کنید و وراثت این امکان را فراهم میکند که از آن پیادهسازی برای یک نوع دیگر استفاده مجدد کنید. در Rust، این کار را به صورت محدود با استفاده از پیادهسازیهای پیشفرض متدهای صفت (trait) انجام دهید، همانطور که در لیستینگ 10-14 دیدیم که یک پیادهسازی پیشفرض برای متد summarize
در صفت Summary
اضافه کردیم. هر نوعی که صفت Summary
را پیادهسازی کند، متد summarize
را بدون نیاز به کد اضافی خواهد داشت. این شبیه به این است که یک کلاس والد یک پیادهسازی از یک متد داشته باشد و یک کلاس فرزند ارثبرده نیز آن پیادهسازی متد را داشته باشد. همچنین میتوانیم پیادهسازی پیشفرض متد summarize
را زمانی که صفت Summary
را پیادهسازی میکنیم، بازنویسی کنیم که شبیه به بازنویسی پیادهسازی یک متد ارثبرده شده در کلاس فرزند است.
دلیل دیگر استفاده از وراثت مربوط به سیستم نوع است: برای این که یک نوع فرزند بتواند در همان مکانهایی که نوع والد استفاده میشود، مورد استفاده قرار گیرد. این مفهوم چندریختی (polymorphism) نیز نامیده میشود، که به این معناست که میتوانید چندین شیء را در زمان اجرا جایگزین یکدیگر کنید اگر آنها ویژگیهای خاصی را به اشتراک بگذارند.
چندریختی (Polymorphism)
برای بسیاری از افراد، چندریختی مترادف با وراثت است. اما در واقع یک مفهوم عمومیتر است که به کدی اشاره دارد که میتواند با دادههایی از انواع مختلف کار کند. در مورد وراثت، این انواع معمولاً زیرکلاسها هستند.
در مقابل، Rust از جنریکها برای انتزاع انواع ممکن مختلف استفاده میکند و محدودیتهای صفت (trait bounds) را برای تحمیل این که این انواع باید چه ویژگیهایی ارائه دهند، اعمال میکند. این رویکرد گاهی چندریختی پارامتریک محدودشده نامیده میشود.
وراثت اخیراً بهعنوان یک راهحل طراحی برنامهنویسی در بسیاری از زبانها محبوبیت خود را از دست داده است زیرا اغلب خطر اشتراکگذاری بیش از حد کد را به همراه دارد. زیرکلاسها نباید همیشه تمام ویژگیهای کلاس والد خود را به اشتراک بگذارند، اما با وراثت این اتفاق میافتد. این میتواند طراحی برنامه را کمتر انعطافپذیر کند. همچنین امکان فراخوانی متدهایی روی زیرکلاسها را فراهم میکند که معنا ندارند یا باعث خطا میشوند زیرا متدها برای زیرکلاس اعمال نمیشوند. علاوه بر این، برخی زبانها فقط اجازه وراثت تک (single inheritance) را میدهند (یعنی یک زیرکلاس فقط میتواند از یک کلاس ارث ببرد)، که انعطافپذیری طراحی برنامه را بیشتر محدود میکند.
به این دلایل، Rust رویکرد متفاوتی را با استفاده از اشیاء صفت (trait objects) به جای وراثت اتخاذ میکند. بیایید ببینیم که چگونه اشیاء صفت در Rust چندریختی را ممکن میسازند.