مقدمة: لماذا تعتبر مبادئ SOLID مهمة في Rust؟
SOLID هي خمسة مبادئ أساسية للتصميم الكائني التوجه تساعد في إنشاء كود مرن وقابل للصيانة والتوسع. على الرغم من أن Rust ليست لغة OOP كلاسيكية (لا تحتوي على وراثة الفئات بالشكل المألوف)، إلا أن هذه المبادئ قابلة للتطبيق هنا بفضل الـ traits والـ structs والـ generics وإدارة الذاكرة. في هذه المقالة، سنشرح كل مبدأ من مبادئ SOLID مع أمثلة كود في Rust، ونوضح كيفية تنفيذها، ونشرح لماذا تجعل كودك أفضل.
1. مبدأ المسؤولية الواحدة (SRP)
الجوهر: يجب أن يكون لكل وحدة أو struct أو دالة سبب واحد فقط للتغيير.
في Rust، غالبًا ما يتم انتهاك هذا المبدأ عندما تكون struct واحدة مسؤولة عن تخزين البيانات وتسلسلها ومنطق الأعمال. النهج الصحيح هو فصل المسؤوليات.
مثال سيء (انتهاك SRP)
struct User { name: String, email: String,}
impl User { fn save_to_db(&self) { // منطق الحفظ في قاعدة البيانات }
fn send_email(&self, message: &str) { // منطق إرسال البريد الإلكتروني }}هنا User مسؤول عن التخزين والإرسال - سببان للتغيير.
مثال جيد (الالتزام بـ SRP)
struct User { name: String, email: String,}
struct UserRepository;impl UserRepository { fn save(&self, user: &User) { /* ... */ }}
struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { /* ... */ }}الآن كل struct لها مسؤولية واحدة: User - البيانات، UserRepository - الحفظ، EmailService - الإرسال.
2. مبدأ الافتتاح/الإغلاق (OCP)
الجوهر: يجب أن تكون الكيانات البرمجية مفتوحة للتمديد ولكن مغلقة للتعديل.
في Rust، يتم تحقيق ذلك باستخدام الـ traits والـ generics. يمكنك إضافة سلوك جديد دون تغيير الكود الموجود.
مثال: نظام الخصومات
لنفترض أن لدينا دالة لحساب السعر مع الخصم. بدلاً من إضافة أنواع خصم جديدة عبر if-else، نستخدم trait:
trait Discount { fn apply(&self, price: f64) -> f64;}
struct NoDiscount;impl Discount for NoDiscount { fn apply(&self, price: f64) -> f64 { price }}
struct SeasonalDiscount(f64);impl Discount for SeasonalDiscount { fn apply(&self, price: f64) -> f64 { price * (1.0 - self.0) }}
fn calculate_price(price: f64, discount: &dyn Discount) -> f64 { discount.apply(price)}الآن، لإضافة خصم جديد (مثل BlackFridayDiscount)، لا نحتاج لتغيير calculate_price - فقط ننفذ trait Discount.
3. مبدأ استبدال ليسكوف (LSP)
الجوهر: يجب أن تكون كائنات الأنواع الفرعية (في Rust - الأنواع التي تنفذ trait) قابلة للتبادل مع كائنات النوع الأساسي دون الإخلال بصحة البرنامج.
في Rust، هذا يعني أنه إذا كان لديك trait وعدة تطبيقات له، فإن الكود الذي يعمل مع dyn Trait أو الـ generics يجب ألا يتعطل عند استبدال أي منها.
مثال على انتهاك LSP
trait Bird { fn fly(&self) -> String;}
struct Sparrow;impl Bird for Sparrow { fn fly(&self) -> String { "Sparrow flies".to_string() }}
struct Penguin;impl Bird for Penguin { fn fly(&self) -> String { panic!("Penguins can't fly!") }}إذا استبدلنا Sparrow بـ Penguin، سيتعطل الكود. الحل هو عدم تضمين طرق لا يمكن تنفيذها بواسطة جميع الأنواع الفرعية. الأفضل فصل الـ traits:
trait CanFly { fn fly(&self) -> Stri