مقدمه

به فصل «شی‌گرایی در Rust» خوش آمدید. برنامه‌نویسی شیءگرا یا Object-Oriented Programming (OOP) یک پارادایم بسیار محبوب است که به ما اجازه می‌دهد تا برنامه‌ها را بر اساس مفهوم «اشیاء» (objects) مدل‌سازی کنیم. این اشیاء، داده‌ها (فیلدها) و رفتارهایی (متدها) که روی آن داده‌ها کار می‌کنند را در کنار هم کپسوله‌سازی می‌کنند.

پاسخ به سوال "آیا Rust یک زبان شیءگرا است؟" کمی پیچیده است. Rust بسیاری از ویژگی‌های مفید و اصلی زبان‌های شیءگرا را پشتیبانی می‌کند، اما رویکرد آن در برخی موارد متفاوت است. برای مثال، Rust مفهوم «وراثت» (inheritance) را به شکلی که در زبان‌هایی مانند Java یا C++ وجود دارد، پیاده‌سازی نمی‌کند. در این درس، ما به بررسی مشخصه‌های اصلی OOP و نحوه پیاده‌سازی آنها در Rust خواهیم پرداخت.

کپسوله‌سازی: پنهان کردن جزئیات پیاده‌سازی

«کپسوله‌سازی» یا Encapsulation به این ایده اشاره دارد که جزئیات پیاده‌سازی داخلی یک شیء باید از کدی که از آن شیء استفاده می‌کند، پنهان بماند. کدی که از شیء استفاده می‌کند (کد کلاینت) تنها باید با رابط عمومی (public API) آن تعامل داشته باشد و نباید نگران نحوه پیاده‌سازی داخلی آن باشد.

در Rust، ما این کار را با استفاده از کلمه کلیدی pub انجام می‌دهیم. به صورت پیش‌فرض، تمام فیلدها و متدهای یک struct خصوصی (private) هستند. ما می‌توانیم با استفاده از pub، آنها را عمومی کنیم و یک API پایدار برای کاربران struct خود فراهم کنیم.

Copy Icon src/lib.rs
pub struct AveragedCollection {
    list: Vec<i32>, // `list` is private
    average: f64, // `average` is private
}

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }
    pub fn remove(&mut self) -> Option<i32> {
        // ... implementation ...
    }
    pub fn average(&self) -> f64 {
        self.average
    }
    fn update_average(&mut self) {
        // ... implementation ...
    }
}

در این مثال، فیلدهای list و average خصوصی هستند. تنها راه برای تغییر آنها، استفاده از متدهای عمومی add و remove است. این کار تضمین می‌کند که فیلد average همیشه با محتوای list هماهنگ باقی می‌ماند و کاربران نمی‌توانند به صورت مستقیم آن را به یک مقدار نامعتبر تغییر دهند.

وراثت و جایگزین‌های آن در Rust

«وراثت» یا Inheritance مکانیزمی است که به یک نوع اجازه می‌دهد تا داده‌ها و رفتارهای یک نوع دیگر را به ارث ببرد. بسیاری از زبان‌های شیءگرا از این قابلیت پشتیبانی می‌کنند. هرچند وراثت می‌تواند برای اشتراک‌گذاری کد مفید باشد، اما گاهی اوقات منجر به مشکلاتی مانند ایجاد سلسله‌مراتب‌های پیچیده و شکننده می‌شود.

Rust به صورت آگاهانه تصمیم گرفته است تا وراثت پیاده‌سازی (implementation inheritance) را در زبان قرار ندهد. در عوض، Rust روش‌های دیگری را برای دستیابی به اشتراک‌گذاری کد و پلی‌مورفیسم ارائه می‌دهد که انعطاف‌پذیری بیشتری دارند.

اشتراک‌گذاری کد با پیاده‌سازی پیش‌فرض Trait

همانطور که در فصل ۱۰ دیدیم، می‌توانیم برای متدهای یک Trait پیاده‌سازی پیش‌فرض تعریف کنیم. هر نوعی که آن Trait را پیاده‌سازی می‌کند، این متد را به صورت رایگان به دست می‌آورد و می‌تواند در صورت نیاز آن را بازنویسی کند. این یک روش عالی برای اشتراک‌گذاری رفتار مشترک بین نوع‌های مختلف است.

پلی‌مورفیسم با Trait Boundها و Trait Objectها

وراثت اغلب برای دستیابی به «پلی‌مورفیسم» (polymorphism) استفاده می‌شود؛ یعنی توانایی استفاده از یک کد با نوع‌های مختلف. Rust این قابلیت را از طریق نوع‌های جنریک با Trait Boundها (برای پلی‌مورفیسم استاتیک) و «اشیاء تریت» یا Trait Objects (برای پلی‌مورفیسم دینامیک) فراهم می‌کند که در درس‌های بعدی این فصل به آنها خواهیم پرداخت.

نتیجه‌گیری

Rust بسیاری از ویژگی‌های مطلوب شیءگرایی مانند کپسوله‌سازی و پلی‌مورفیسم را پشتیبانی می‌کند، اما این کار را به روش منحصر به فرد خود و با تکیه بر مفاهیمی مانند structها و traitها انجام می‌دهد. عدم وجود وراثت در Rust یک انتخاب طراحی آگاهانه است که به نفع انعطاف‌پذیری بیشتر و جلوگیری از مشکلات رایج وراثت در زبان‌های دیگر تمام می‌شود.

در این درس به بررسی نحوه تطابق Rust با پارادایم شیءگرایی پرداختیم. در درس بعدی، به سراغ یکی از قدرتمندترین ابزارهای Rust برای پیاده‌سازی پلی‌مورفیسم دینامیک، یعنی «استفاده از Trait Objects» خواهیم رفت.