مبادئ SOLID في Rust: النظرية والتطبيق (مستوى متقدم)

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: SOLID في عالم Rust

\n\n

مبادئ SOLID هي أساس البنية المعمارية المرنة والقابلة للصيانة. ومع ذلك، فإن تطبيقها في Rust له خصوصيته، نظرًا لنظام الملكية الفريد، والسمات (traits)، وغياب البرمجة كائنية التوجه (OOP) التقليدية. في هذه المقالة، سنشرح كيفية تطبيق كل مبدأ من المبادئ الخمسة على مستوى متقدم، باستخدام Rust الاصطلاحي (idiomatic). لن نكتفي بسرد القواعد، بل سنوضح كيف تتحول هذه المبادئ في سياق نظام الأنواع (type system) والاقتراض (borrowing).

\n\n

هذه المقالة مخصصة للمطورين الذين لديهم معرفة أساسية بـ Rust ومبادئ SOLID. سنركز على التفاصيل الدقيقة: كيفية تجنب انتهاك LSP عند العمل مع الأنواع العامة (generics)، وكيف يؤثر ISP على تصميم السمات (traits)، ولماذا يتطلب DIP في Rust غالبًا استخدام كائنات السمات (trait objects) أو Box.

\n\n

1. مبدأ المسؤولية الواحدة (SRP) والملكية (Ownership)

\n\n

في Rust، يكتسب مبدأ SRP بُعدًا إضافيًا بسبب القواعد الصارمة للملكية. يجب أن يكون للفئة (أو الهيكل) سبب واحد فقط للتغيير. ولكن كيف يرتبط ذلك بنقل الملكية؟

\n\n

المشكلة: هيكل يدير الموارد والمنطق معًا

\n

لنأخذ مثالاً حيث يدير الهيكل واصف ملف (file descriptor) ويحلل البيانات في نفس الوقت. هذا ينتهك مبدأ SRP.

\n\n
use std::fs::File;\nuse std::io::Read;\n\n// ❌ انتهاك SRP: الهيكل يدير المورد ويعالج البيانات\nstruct DataProcessor {\n    file: File,\n}\n\nimpl DataProcessor {\n    fn new(path: &str) -> Self {\n        DataProcessor {\n            file: File::open(path).unwrap(),\n        }\n    }\n\n    fn process(&mut self) -> Vec<u8> {\n        let mut buffer = Vec::new();\n        self.file.read_to_end(&mut buffer).unwrap();\n        // ... منطق معالجة معقد\n        buffer\n    }\n}\n
\n\n

الحل: الفصل عبر نقل الملكية

\n

نفصل المسؤوليات: هيكل واحد مسؤول عن القراءة، وآخر عن المعالجة. يتم نقل الملكية بشكل صريح.

\n\n
use std::fs::File;\nuse std::io::Read;\n\n// ✅ المسؤولية الأولى: إدارة الملف\nstruct FileReader {\n    file: File,\n}\n\nimpl FileReader {\n    fn new(path: &str) -> Self {\n        FileReader {\n            file: File::open(path).unwrap(),\n        }\n    }\n\n    fn read(self) -> Vec<u8> {\n        let mut buffer = Vec::new();\n        // self.file يتم استهلاكه (نقله)، لأنه غير مطلوب بعد القراءة\n        self.file.read_to_end(&mut buffer).unwrap();\n        buffer\n    }\n}\n\n// ✅ المسؤولية الثانية: معالجة البيانات\nstruct DataProcessor {\n    data: Vec<u8>,\n}\n\nimpl DataProcessor {\n    fn new(data: Vec<u8>) -> Self {\n        DataProcessor { data }\n    }\n\n    fn process(&self) -> Vec<u8> {\n        // معالجة غير قابلة للتغيير\n        self.data.iter().map(|&b| b + 1).collect()\n    }\n}\n\nfn main() {\n    let reader = FileReader::new("data.bin");\n    let raw_data = reader.read(); // reader لم يعد متاحًا\n    let processor = DataProcessor::new(raw_data);\n    let result = processor.process();\n}\n
\n\n

النقطة الأساسية: استخدمنا self (الملكية) بدلاً من &self في read()، للإشارة صراحةً إلى أن FileReader لم يعد مطلوبًا بعد القراءة. هذا يجعل تدفق البيانات شفافًا ويمنع تسرب الموارد.

\n\n

2. مبدأ الانفتاح/الإغلاق (OCP) والسمات (Traits)

\n\n

ينص مبدأ OCP على: يجب أن تكون الكيانات مفتوحة للتمديد ولكن مغلقة للتعديل. في Rust، يتم تحقيق ذلك بشكل مثالي من خلال السمات (traits) والأنواع العامة (generics) دون استخدام الجداول الافتراضية (vtable) إذا لم يكن ذلك ضروريًا.

\n\n

مثال متقدم: استراتيجية بدون تكاليف إضافية (Zero-Cost Abstraction)

\n

لنفترض أن لدينا نظام تسجيل (logging). بدلاً من تعديل جوهر التطبيق لكل خلفية جديدة، نستخدم تعدد الأشكال (polymorphism) عبر السمات.

\n\n
/                

المدونات

توصيات الكتب