سؤال

إذا كنت أرغب في جعل الفصل قابلاً للتكيف، وجعل من الممكن تحديد خوارزميات مختلفة من الخارج - ما هو أفضل تطبيق في C++؟

أرى في الأساس احتمالين:

  • استخدم فئة أساسية مجردة وقم بتمرير كائن ملموس إليها
  • استخدم قالبًا

فيما يلي مثال صغير، تم تنفيذه في الإصدارات المختلفة:

النسخة 1: مجردة الطبقة الأساسية

class Brake {
public: virtual void stopCar() = 0;  
};

class BrakeWithABS : public Brake {
public: void stopCar() { ... }
};

class Car {
  Brake* _brake;
public:
  Car(Brake* brake) : _brake(brake) { brake->stopCar(); }
};

الإصدار 2أ: نموذج

template<class Brake>
class Car {
  Brake brake;
public:
  Car(){ brake.stopCar(); }
};

الإصدار 2ب: القالب والميراث الخاص

template<class Brake>
class Car : private Brake {
  using Brake::stopCar;
public:
  Car(){ stopCar(); }
};

كوني من Java، أميل بطبيعة الحال إلى استخدام الإصدار 1 دائمًا، ولكن يبدو أن إصدارات القوالب مفضلة كثيرًا، على سبيل المثال.في رمز المحكمة الخاصة بلبنان؟إذا كان هذا صحيحًا، فهل يرجع ذلك فقط إلى كفاءة الذاكرة وما إلى ذلك (لا يوجد وراثة ولا استدعاءات دالة افتراضية)؟

أدرك أنه لا يوجد فرق كبير بين الإصدار 2أ و2ب، انظر الأسئلة الشائعة حول لغة C++.

هل يمكنك التعليق على هذه الاحتمالات؟

هل كانت مفيدة؟

المحلول

هذا يعتمد على أهدافك.يمكنك استخدام الإصدار 1 إذا كنت

  • تنوي استبدال فرامل السيارة (في وقت التشغيل)
  • تنوي تمرير السيارة إلى وظائف غير القالب

أفضّل عمومًا الإصدار 1 باستخدام تعدد الأشكال في وقت التشغيل، لأنه لا يزال مرنًا ويسمح لك بالإبقاء على السيارة من نفس النوع: Car<Opel> هو نوع آخر من Car<Nissan>.إذا كانت أهدافك تتمثل في الأداء الرائع أثناء استخدام المكابح بشكل متكرر، فإنني أنصحك باستخدام المنهج النموذجي.بالمناسبة، هذا ما يسمى التصميم القائم على السياسة.أنت تقدم أ سياسة الفرامل.على سبيل المثال، لأنك قلت إنك قمت بالبرمجة بلغة Java، فربما لم تكن لديك خبرة كبيرة في لغة C++ بعد.طريقة واحدة للقيام بذلك:

template<typename Accelerator, typename Brakes>
class Car {
    Accelerator accelerator;
    Brakes brakes;

public:
    void brake() {
        brakes.brake();
    }
}

إذا كان لديك الكثير من السياسات، فيمكنك تجميعها معًا في بنيتها الخاصة، وتمرير تلك السياسة، على سبيل المثال كـ SpeedConfiguration جمع Accelerator, Brakes وبعض أكثر.أحاول في مشاريعي أن أبقي قدرًا كبيرًا من التعليمات البرمجية خالية من القوالب، مما يسمح بتجميعها مرة واحدة في ملفات الكائنات الخاصة بها، دون الحاجة إلى التعليمات البرمجية الخاصة بها في الرؤوس، مع السماح بتعدد الأشكال (عبر الوظائف الافتراضية).على سبيل المثال، قد ترغب في الاحتفاظ بالبيانات والوظائف الشائعة التي من المحتمل أن تستدعيها التعليمات البرمجية غير القالبية في مناسبات عديدة في فئة أساسية:

class VehicleBase {
protected:
    std::string model;
    std::string manufacturer;
    // ...

public:
    ~VehicleBase() { }
    virtual bool checkHealth() = 0;
};


template<typename Accelerator, typename Breaks>
class Car : public VehicleBase {
    Accelerator accelerator;
    Breaks breaks;
    // ...

    virtual bool checkHealth() { ... }
};

وبالمناسبة، هذا أيضًا هو النهج الذي تستخدمه تدفقات C++: std::ios_base يحتوي على أعلام وأشياء لا تعتمد على نوع الحرف أو السمات مثل الوضع المفتوح وأعلام التنسيق والأشياء، بينما std::basic_ios ثم هو قالب الفصل الذي يرثه.يؤدي هذا أيضًا إلى تقليل تضخم التعليمات البرمجية من خلال مشاركة التعليمات البرمجية المشتركة بين جميع عمليات إنشاء قالب الفصل الدراسي.

الميراث الخاص؟

وينبغي تجنب الميراث الخاص بشكل عام.إنه نادرًا ما يكون مفيدًا ويكون الاحتواء فكرة أفضل في معظم الحالات.حالة شائعة حيث يكون العكس صحيحًا عندما يكون الحجم أمرًا بالغ الأهمية حقًا (فئة السلسلة المستندة إلى السياسة، على سبيل المثال):يمكن تطبيق تحسين الفئة الأساسية الفارغة عند الاشتقاق من فئة سياسة فارغة (تحتوي فقط على وظائف).

يقرأ استخدامات وانتهاكات الميراث بواسطة هيرب سوتر.

نصائح أخرى

القاعدة الأساسية هي:

1) إذا تم اختيار نوع الخرسانة في وقت التجميع، يفضل القالب.سيكون أكثر أمانًا (أخطاء وقت التجميع مقابل أخطاء وقت التشغيل) وربما يكون أفضل.2) إذا تم الاختيار في وقت التشغيل (أينتيجة لإجراء المستخدم) لا يوجد خيار حقًا - استخدم الميراث والوظائف الافتراضية.

خيارات أخرى:

  1. استخدم ال نمط الزائر (دع الكود الخارجي يعمل في صفك).
  2. قم بإضفاء طابع خارجي على جزء من صفك، على سبيل المثال عبر التكرارات، حيث يمكن أن تعمل عليها التعليمات البرمجية العامة المستندة إلى التكرار.يعمل هذا بشكل أفضل إذا كان الكائن عبارة عن حاوية لكائنات أخرى.
  3. أنظر أيضاً نمط الاستراتيجية (توجد أمثلة لـ c++ بالداخل)

تعد القوالب طريقة للسماح للفصل الدراسي باستخدام متغير لا تهتم حقًا بنوعه.الميراث هو وسيلة لتحديد ما يعتمد عليه الفصل على سماته.انها ال "هو-أ" مقابل "لديه-أ" سؤال.

لقد تمت الإجابة على معظم أسئلتك بالفعل، لكني أردت توضيح هذه النقطة بالتفصيل:

قادمة من جافا ، أنا أميل بشكل طبيعي إلى استخدام الإصدار 1 دائمًا ، ولكن يبدو أن إصدارات القوالب مفضلة في كثير من الأحيان ، على سبيل المثالفي رمز المحكمة الخاصة بلبنان؟إذا كان هذا صحيحًا ، فهل هذا فقط بسبب كفاءة الذاكرة وما إلى ذلك (لا يوجد ميراث ، لا توجد مكالمات وظيفية افتراضية)؟

هذا جزء منه.ولكن هناك عامل آخر وهو نوع السلامة المضافة.عندما تعالج أ BrakeWithABS ك Brake, ، ستفقد معلومات النوع.لم تعد تعرف أن الكائن هو في الواقع أ BrakeWithABS.إذا كانت معلمة قالب، فلديك النوع الدقيق المتاح، والذي قد يمكّن المترجم في بعض الحالات من إجراء فحص أفضل للكتابة.أو قد يكون مفيدًا في التأكد من استدعاء التحميل الزائد الصحيح للوظيفة.(لو stopCar() يقوم بتمرير كائن الفرامل إلى وظيفة ثانية، والتي قد يكون لها حمل زائد منفصل BrakeWithABS, ، لن يتم استدعاء ذلك إذا كنت قد استخدمت الميراث، و BrakeWithABS تم الإدلاء به إلى أ Brake.

عامل آخر هو أنه يسمح بمزيد من المرونة.لماذا يجب أن ترث جميع تطبيقات الفرامل من نفس الفئة الأساسية؟هل لدى الفئة الأساسية بالفعل أي شيء لإحضاره إلى الطاولة؟إذا قمت بكتابة فصل دراسي يكشف وظائف الأعضاء المتوقعة، أليس هذا جيدًا بما يكفي ليكون بمثابة مكابح؟في كثير من الأحيان، يؤدي استخدام الواجهات أو الفئات الأساسية المجردة بشكل صريح إلى تقييد التعليمات البرمجية الخاصة بك أكثر من اللازم.

(ملاحظة، أنا لا أقول أن القوالب يجب أن تكون الحل المفضل دائمًا.هناك مخاوف أخرى قد تؤثر على هذا، بدءًا من سرعة الترجمة إلى "ما يعرفه المبرمجون في فريقي" أو فقط "ما أفضّله".وأحيانا أنت يحتاج تعدد الأشكال في وقت التشغيل، وفي هذه الحالة يكون حل القالب ببساطة غير ممكن)

هذه الإجابة هو أكثر أو أقل صحة.عندما تريد تحديد معلمات شيء ما في وقت الترجمة - يجب أن تفضل القوالب.عندما تريد تحديد معلمات لشيء ما في وقت التشغيل، يجب أن تفضل تجاوز الوظائف الافتراضية.

لكن, ، فإن استخدام القوالب لا يمنعك من القيام بالأمرين معًا (مما يجعل إصدار القالب أكثر مرونة):

struct Brake {
    virtual void stopCar() = 0;
};

struct BrakeChooser {
    BrakeChooser(Brake *brake) : brake(brake) {}
    void stopCar() { brake->stopCar(); }

    Brake *brake;
};

template<class Brake>
struct Car
{
    Car(Brake brake = Brake()) : brake(brake) {}
    void slamTheBrakePedal() { brake.stopCar(); }

    Brake brake;
};


// instantiation
Car<BrakeChooser> car(BrakeChooser(new AntiLockBrakes()));

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

تحتوي الفئة الأساسية المجردة على حمل إضافي للمكالمات الافتراضية ولكنها تتميز بميزة أن جميع الفئات المشتقة هي بالفعل فئات أساسية.ليس الأمر كذلك عند استخدام القوالب - Car<Brake> وCar<BrakeWithABS> غير مرتبطين ببعضهما البعض وسيتعين عليك إما Dynamic_cast والتحقق من وجود قيمة فارغة أو الحصول على قوالب لجميع التعليمات البرمجية التي تتعامل مع Car.

استخدم الواجهة إذا كنت تفترض دعم فئات Break المختلفة وتسلسلها الهرمي في وقت واحد.

Car( new Brake() )
Car( new BrakeABC() )
Car( new CoolBrake() )

وأنت لا تعرف هذه المعلومات في وقت التجميع.

إذا كنت تعرف أي استراحة ستستخدمها، فإن 2b هو الاختيار الصحيح بالنسبة لك لتحديد فئات السيارات المختلفة.الفرامل في هذه الحالة ستكون "استراتيجية" سيارتك ويمكنك ضبطها بشكل افتراضي.

لن أستخدم 2 أ.بدلاً من ذلك، يمكنك إضافة أساليب ثابتة إلى Break واستدعائها بدون مثيل.

أنا شخصياً أفضّل دائمًا استخدام الواجهات بدلاً من القوالب لعدة أسباب:

  1. تكون أخطاء تجميع القوالب وربطها غامضة في بعض الأحيان
  2. من الصعب تصحيح أخطاء التعليمات البرمجية التي تعتمد على القوالب (على الأقل في Visual Studio IDE)
  3. يمكن للقوالب أن تجعل الثنائيات الخاصة بك أكبر.
  4. تتطلب القوالب منك وضع كل التعليمات البرمجية الخاصة بها في ملف الرأس، مما يجعل فهم فئة القالب أصعب قليلاً.
  5. يصعب صيانة القوالب من قبل المبرمجين المبتدئين.

لا أستخدم القوالب إلا عندما تنشئ الجداول الافتراضية نوعًا من الحمل.

وبالطبع هذا رأيي الشخصي فقط.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top