Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

خصائص اللغات كائنية التوجه (Object-Oriented Languages)

لا يوجد إجماع في مجتمع البرمجة حول الميزات التي يجب أن تتوفر في اللغة لكي تُعتبر كائنية التوجه. تتأثر لغة Rust بالعديد من النماذج البرمجية (Programming Paradigms)، بما في ذلك البرمجة كائنية التوجه (OOP)؛ على سبيل المثال، استكشفنا الميزات التي جاءت من البرمجة الوظيفية (Functional Programming) في الفصل 13. يمكن القول إن لغات OOP تشترك في خصائص معينة—وهي الكائنات، والتغليف، والوراثة. دعنا نلقي نظرة على معنى كل من هذه الخصائص وما إذا كانت Rust تدعمها.

الكائنات تحتوي على بيانات وسلوك (Objects Contain Data and Behavior)

كتاب أنماط التصميم: عناصر البرمجيات كائنية التوجه القابلة لإعادة الاستخدام (Design Patterns: Elements of Reusable Object-Oriented Software) لمؤلفيه إريك جاما، وريتشارد هيلم، ورالف جونسون، وجون فليسيدس (Addison-Wesley، 1994)، والمعروف عامياً بكتاب عصابة الأربعة (The Gang of Four)، هو فهرس لأنماط التصميم كائنية التوجه. يعرّف الكتاب OOP بهذه الطريقة:

تتكون البرامج كائنية التوجه من كائنات. يقوم الكائن (Object) بتغليف كل من البيانات والإجراءات التي تعمل على تلك البيانات. وعادة ما تسمى هذه الإجراءات توابع (Methods) أو عمليات (Operations).

باستخدام هذا التعريف، فإن Rust كائنية التوجه: فالهياكل (Structs) والتعدادات (Enums) تحتوي على بيانات، وتوفر كتل impl توابع (Methods) عليها. وبالرغم من أن Structs و Enums التي تحتوي على Methods لا تُسمى كائنات (Objects)، إلا أنها توفر نفس الوظائف، وفقاً لتعريف عصابة الأربعة للكائنات.

التغليف الذي يخفي تفاصيل التنفيذ (Encapsulation That Hides Implementation Details)

جانب آخر يرتبط عادة بـ OOP هو فكرة التغليف (Encapsulation)، والتي تعني أن تفاصيل تنفيذ (Implementation) الكائن لا يمكن الوصول إليها من قبل الشفرة البرمجية (Code) التي تستخدم ذلك الكائن. لذلك، فإن الطريقة الوحيدة للتفاعل مع الكائن هي من خلال واجهة برمجة التطبيقات (API) العامة الخاصة به؛ ولا ينبغي لـ Code الذي يستخدم الكائن أن يكون قادراً على الوصول إلى الأجزاء الداخلية للكائن وتغيير البيانات أو السلوك مباشرة. وهذا يمكن المبرمج من تغيير وإعادة هيكلة (Refactor) الأجزاء الداخلية للكائن دون الحاجة إلى تغيير Code الذي يستخدم الكائن.

ناقشنا كيفية التحكم في Encapsulation في الفصل 7: يمكننا استخدام الكلمة المفتاحية pub لتحديد أي الوحدات (Modules)، والأنواع (Types)، والدوال (Functions)، و Methods في Code الخاص بنا يجب أن تكون عامة (Public)، وبشكل افتراضي يكون كل شيء آخر خاصاً (Private). على سبيل المثال، يمكننا تعريف هيكل (Struct) باسم AveragedCollection يحتوي على حقل (Field) يضم متجهاً (Vector) من قيم i32. يمكن أن يحتوي Struct أيضاً على Field يضم متوسط القيم في Vector، مما يعني أنه لا يلزم حساب المتوسط عند الطلب في كل مرة يحتاجه فيها شخص ما. بمعنى آخر، سيقوم AveragedCollection بتخزين المتوسط المحسوب (Cache) لنا. تحتوي القائمة 18-1 على تعريف Struct ‏AveragedCollection.

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

تم وضع علامة pub على Struct لكي يتمكن Code الآخر من استخدامه، ولكن الحقول (Fields) داخل Struct تظل Private. هذا مهم في هذه الحالة لأننا نريد التأكد من أنه كلما تمت إضافة قيمة إلى القائمة أو إزالتها منها، يتم تحديث المتوسط أيضاً. نقوم بذلك من خلال تنفيذ Methods ‏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;
    }
}

تعتبر Methods العامة add و remove و average هي الطرق الوحيدة للوصول إلى البيانات أو تعديلها في مثيل (Instance) من AveragedCollection. عندما يتم إضافة عنصر إلى list باستخدام Method ‏add أو إزالته باستخدام Method ‏remove فإن تنفيذ كل منهما يستدعي Method الخاص update_average الذي يتولى تحديث Field ‏average أيضاً.

نترك Fields ‏list و average خاصة (Private) بحيث لا توجد طريقة لـ Code الخارجي لإضافة عناصر إلى Field ‏list أو إزالتها منه مباشرة؛ وإلا فقد يصبح Field ‏average غير متزامن عند تغير list. يعيد Method ‏average القيمة الموجودة في Field ‏average مما يسمح لـ Code الخارجي بقراءة المتوسط ولكن دون تعديله.

بما أننا قمنا بتغليف تفاصيل Implementation الخاصة بـ Struct ‏AveragedCollection فيمكننا بسهولة تغيير جوانب معينة، مثل هيكل البيانات (Data Structure)، في المستقبل. على سبيل المثال، يمكننا استخدام HashSet<i32> بدلاً من Vec<i32> لـ Field ‏list. وطالما ظلت تواقيع (Signatures) التوابع العامة add و remove و average كما هي، فلن يحتاج Code الذي يستخدم AveragedCollection إلى التغيير. إذا جعلنا list عاماً (Public) بدلاً من ذلك، فلن يكون هذا هو الحال بالضرورة: فـ HashSet<i32> و Vec<i32> لديهما Methods مختلفة لإضافة العناصر وإزالتها، لذا من المحتمل أن يضطر Code الخارجي للتغيير إذا كان يعدل list مباشرة.

إذا كان Encapsulation جانباً مطلوباً لكي تُعتبر اللغة كائنية التوجه، فإن Rust تلبي هذا المتطلب. فخيار استخدام pub أو عدم استخدامه لأجزاء مختلفة من Code يتيح تغليف تفاصيل Implementation.

الوراثة كنظام أنواع وكمشاركة للشفرة (Inheritance as a Type System and as Code Sharing)

الوراثة (Inheritance) هي آلية يمكن من خلالها للكائن أن يرث عناصر من تعريف كائن آخر، وبذلك يكتسب بيانات وسلوك الكائن الأب دون أن تضطر لتعريفها مرة أخرى.

إذا كان يجب أن تتوفر الوراثة في اللغة لكي تكون كائنية التوجه، فإن Rust ليست كذلك. فلا توجد طريقة لتعريف Struct يرث Fields وتنفيذ Methods لـ Struct الأب دون استخدام ماكرو (Macro).

ومع ذلك، إذا كنت معتاداً على وجود Inheritance في حقيبة أدواتك البرمجية، فيمكنك استخدام حلول أخرى في Rust، اعتماداً على سبب لجوئك إلى Inheritance في المقام الأول.

قد تختار Inheritance لسببين رئيسيين. أحدهما هو إعادة استخدام Code: يمكنك تنفيذ سلوك معين لنوع (Type) واحد، وتتيح لك Inheritance إعادة استخدام هذا Implementation لنوع مختلف. يمكنك القيام بذلك بطريقة محدودة في Code لغة Rust باستخدام تنفيذات سمة التابع الافتراضية (Default Trait Method Implementations)، والتي رأيتها في القائمة 10-14 عندما أضفنا تنفيذاً افتراضياً لـ Method ‏summarize على سمة (Trait) ‏Summary. أي Type ينفذ Trait ‏Summary سيكون لديه Method ‏summarize متاحاً عليه دون أي Code إضافي. وهذا يشبه وجود تنفيذ لـ Method في فئة أب (Parent Class) وامتلاك فئة ابن (Child Class) وارثة لنفس التنفيذ أيضاً. يمكننا أيضاً تجاوز (Override) التنفيذ الافتراضي لـ Method ‏summarize عندما ننفذ Trait ‏Summary وهو ما يشبه قيام Child Class بتجاوز تنفيذ Method موروث من Parent Class.

السبب الآخر لاستخدام Inheritance يتعلق بنظام الأنواع (Type System): للسماح باستخدام نوع ابن (Child Type) في نفس الأماكن التي يُستخدم فيها النوع الأب (Parent Type). يسمى هذا أيضاً تعدد الأشكال (Polymorphism)، وهو ما يعني أنه يمكنك استبدال كائنات متعددة ببعضها البعض في وقت التشغيل (Runtime) إذا كانت تشترك في خصائص معينة.

تعدد الأشكال (Polymorphism)

بالنسبة للكثيرين، يعتبر Polymorphism مرادفاً لـ Inheritance. لكنه في الواقع مفهوم أكثر عمومية يشير إلى Code يمكنه العمل مع بيانات من أنواع (Types) متعددة. وبالنسبة لـ Inheritance، تكون تلك Types عموماً فئات فرعية (Subclasses).

بدلاً من ذلك، تستخدم Rust الأنواع العامة (Generics) للتجريد (Abstract) عبر Types محتملة مختلفة، وحدود السمات (Trait Bounds) لفرض قيود على ما يجب أن توفره تلك Types. يسمى هذا أحياناً تعدد الأشكال البارامتري المحدود (Bounded Parametric Polymorphism).

اختارت Rust مجموعة مختلفة من المقايضات بعدم تقديمها لـ Inheritance. فغالباً ما تخاطر Inheritance بمشاركة Code أكثر من اللازم. لا ينبغي لـ Subclasses أن تشترك دائماً في جميع خصائص Parent Class الخاصة بها، ولكنها ستفعل ذلك مع Inheritance. وهذا يمكن أن يجعل تصميم البرنامج أقل مرونة. كما أنه يفتح المجال لاستدعاء Methods على Subclasses لا معنى لها أو تسبب أخطاء لأن Methods لا تنطبق على Subclass. بالإضافة إلى ذلك، تسمح بعض اللغات فقط بـ الوراثة الفردية (Single Inheritance) (مما يعني أن Subclass يمكنه الوراثة من فئة واحدة فقط)، مما يحد بشكل أكبر من مرونة تصميم البرنامج.

لهذه الأسباب، تتخذ Rust نهجاً مختلفاً باستخدام كائنات السمات (Trait Objects) بدلاً من Inheritance لتحقيق Polymorphism في Runtime. دعنا نلقي نظرة على كيفية عمل Trait Objects.