c++ - هل تستخدم لغة pImpl بالفعل في الممارسة؟




oop pimpl-idiom (10)

أقرأ كتاب "C ++ استثنائية" بواسطة Herb Sutter ، وفي ذلك الكتاب تعلمت عن لغة pImpl. في الأساس ، فإن الفكرة هي إنشاء هيكل للأجسام الخاصة class وتخصيصها ديناميكيًا لتقليل وقت التجميع (وكذلك إخفاء التطبيقات الخاصة بطريقة أفضل).

فمثلا:

class X
{
private:
  C c;
  D d;  
} ;

يمكن تغييرها إلى:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

و ، في CPP ، التعريف:

struct X::XImpl
{
  C c;
  D d;
};

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

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


Answers

اتفق مع جميع الآخرين بشأن البضائع ، لكن دعوني أضع حدا للأدلة: لا يعمل بشكل جيد مع القوالب .

السبب هو أن إنشاء قالب القالب يتطلب الإعلان الكامل المتوفر في المكان الذي حدث فيه التثبيت. (وهذا هو السبب الرئيسي في عدم رؤية طرق القالب محددة في ملفات CPP)

لا يزال بإمكانك الرجوع إلى الفئات الفرعية المعيارية ، ولكن بما أنه يجب عليك تضمينها جميعًا ، يتم فقد كل ميزة "فصل التنفيذ" في التجميع (تجنب تضمين جميع التعليمات البرمجية المحددة في كل مكان ، تقصير التجميع).

هو نموذج جيد ل OOP الكلاسيكي (تستند الميراث) ولكن ليس للبرمجة العامة (على أساس التخصص).


اعتدت على استخدام هذا الأسلوب كثيرًا في الماضي ، لكنني وجدت نفسي أبتعد عنه.

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

مزايا pImpl هي:

  1. بافتراض وجود تنفيذ واحد فقط لهذه الواجهة ، فمن الواضح من خلال عدم استخدام تطبيق الطبقة المجردة / ملموسة

  2. إذا كان لديك مجموعة من الفصول الدراسية (وحدة نمطية) بحيث يصل العديد من الطبقات إلى نفس "impl" ، إلا أن مستخدمي الوحدة الدراسية سيستخدمون الفئات "المكشوفة" فقط.

  3. لا يوجد جدول- v إذا كان هذا يفترض أن يكون أمراً سيئاً.

عيوب وجدت من pImpl (حيث تعمل واجهة مجردة بشكل أفضل)

  1. في حين قد يكون لديك تنفيذ "إنتاج" واحد فقط ، باستخدام واجهة مجردة ، يمكنك أيضًا إنشاء عملية "زائفة" تعمل في اختبار الوحدة.

  2. (أكبر قضية). قبل أيام unique_ptr والانتقال كان لديك خيارات محدودة لكيفية تخزين pImpl. مؤشر خام وكان لديك مشكلات في أن تكون صفك غير قابل للنسخ. لن يعمل auto_ptr القديم مع الطبقة المعلنة (ليس على جميع المترجمين على أي حال). لذلك بدأ الناس في استخدام shared_ptr الذي كان لطيفًا في جعل صفك قابلاً للنسخ ولكن بالطبع كانت للنسختين نفس المشاركة الأساسية التي قد لا تتوقعها (تعديل واحد وكلاهما تم تعديلهما). لذا كان الحل في كثير من الأحيان يستخدم المؤشر الخام للداخلية وجعل الصف غير قابل للنسخ وإرجاع common_ptr إلى ذلك بدلاً من ذلك. لذلك يدعو إلى الجديد. (في الواقع 3 أعطيت shared_ptr القديمة لك ثانية واحدة).

  3. من الناحية الفنية لا يتم تصحيحها فعلاً حيث لا يتم نشر constness عبر مؤشر عضو.

عموما لقد انتقلت بعيدا في السنوات من pImpl وإلى استخدام واجهة مجردة بدلا من ذلك (وأساليب المصنع لإنشاء حالات).


وأود أن أعتبر PIMPL أساسا لفصول تتعرض لاستخدامها باعتبارها API من وحدات أخرى. هذا له العديد من الفوائد ، لأنه يجعل recompilation من التغييرات التي أجريت في تنفيذ PIMPL لا يؤثر على بقية المشروع. بالإضافة إلى ذلك ، بالنسبة إلى فئات واجهة برمجة التطبيقات ، فإنها تعمل على تعزيز التوافق الثنائي (لا تؤثر التغييرات في تطبيق الوحدة النمطية على عملاء تلك الوحدات ، فلا يلزم إعادة تحويلها لأن التطبيق الجديد له الواجهة الثنائية نفسها - الواجهة التي تتعرض لها PIMPL).

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


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

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: آمل أن لا أكون سوء الفهم تحريك دلالات.


أعتقد أن هذا هو واحد من أهم الأدوات الأساسية لفك الارتباط.

كنت تستخدم pimpl (والعديد من التعابير الأخرى من C ++ استثنائية) على مشروع مضمن (SetTopBox).

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


لقد قدم أشخاص آخرون بالفعل الدعم الفني / السلبي ، ولكني أعتقد أن ما يلي جدير بالملاحظة:

أولا وقبل كل شيء ، لا تكون عقائديا. إذا كان pImpl يعمل على موقفك ، فاستخدمه - لا تستخدمه فقط لأنه "من الأفضل لـ OO لأنه يخفي التنفيذ بالفعل " إلخ. نقلا عن الأسئلة المتداولة في C ++:

التغليف هو رمز ، وليس الناس ( source )

فقط لإعطائك مثالاً على برنامج مفتوح المصدر حيث يتم استخدامه ولماذا: OpenThreads ، مكتبة مؤشر الترابط المستخدمة من قبل OpenSceneGraph . والفكرة الرئيسية هي إزالة <Thread.h> (على سبيل المثال <Thread.h> ) من جميع التعليمات البرمجية الخاصة <Thread.h> الأساسي ، لأن متغيرات الحالة الداخلية (مثل مقابض الخيوط) تختلف من منصة إلى أخرى. وبهذه الطريقة يمكن للمرء أن يقوم بتجميع الكود على مكتبتك دون أي معرفة بالخصائص الأساسية للمنصات الأخرى ، لأن كل شيء مخفي.


يتم استخدامه في الممارسة في الكثير من المشاريع. انها usefullness يعتمد بشكل كبير على نوع المشروع. واحد من أبرز المشاريع التي تستخدم هذا هو Qt ، حيث الفكرة الأساسية هي إخفاء التنفيذ أو رمز النظام الأساسي من المستخدم (مطورين آخرين يستخدمون كيو تي).

هذه فكرة نبيلة ولكن هناك عيب حقيقي لهذا: تصحيح الأخطاء طالما أن الشفرة المخبأة في التقديرات الخاصة هي ذات جودة عالية ، فهذا أمر جيد ، ولكن إذا كانت هناك أخطاء ، فإن المستخدم / المطور يواجه مشكلة ، لأنه مجرد مؤشر غبي للتنفيذ المخفي ، حتى لو كان لديه شفرة المصدر للتطبيقات.

لذلك كما هو الحال في جميع قرارات التصميم تقريبا هناك إيجابيات وسلبيات.


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

بالطبع يتم استخدامه ، وفي مشروعي ، في كل صف تقريبًا ، لعدة أسباب ذكرتها:

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

هل هذه التقنية موصى باستخدامها في الأنظمة المدمجة (حيث يكون الأداء مهمًا جدًا)؟

هذا يعتمد على مدى قوة هدفك. لكن الجواب الوحيد على هذا السؤال هو: قياس وتقييم ما تكسبه وتخسره.


هنا سيناريو حقيقي صادفته ، حيث ساعد هذا المصطلح الكثير. لقد قررت مؤخرًا دعم DirectX 11 ، بالإضافة إلى دعم DirectX 9 الحالي ، في محرك ألعاب. المحرك بالفعل ملفوفة معظم ميزات DX ، لذلك تم استخدام أي من واجهات DX مباشرة ؛ تم تعريفهم فقط في الرؤوس كأعضاء خاصين. يستخدم المحرك DLLs كملحقات ، مضيفًا لوحة المفاتيح ، والماوس ، وعصا التحكم ، ودعم البرمجة ، في الأسبوع مثل العديد من الإضافات الأخرى. في حين أن معظم هذه DLLs لم تستخدم DX مباشرة ، فإنها تطلبت المعرفة والربط إلى DX لمجرد أنها سحبت في الرؤوس التي كشفت DX. عند إضافة DX 11 ، كان هذا التعقيد يزداد بشكل كبير ، ولكن بلا داعٍ. نقل أعضاء DX إلى Pimpl المحددة فقط في المصدر ألغى هذا الفرض. علاوةً على هذا الانخفاض في اعتمادات المكتبات ، أصبحت واجهاتي المكشوفة أكثر نظافةً ، حيث تم نقل وظائف عضو خاص إلى Pimpl ، مما أدى إلى تعريض الواجهات الأمامية المواجهة فقط.


كود الوضوح

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

قابلية الصيانة

من أجل الحفاظ على فئة pimpl'd ، أجد شخصيًا مزيدًا من الاهتمام في كل وصول لأحد أعضاء البيانات المملة. لا يمكن أن يساعدك المساعدون إذا كانت البيانات خاصة بحتة لأنه يجب عليك حينئذٍ عدم الكشف عن أي ملحق أو محوّل لها على أي حال ، وأنت عالق باستمرار في إزالة البثور.

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

أداء

فقدان الأداء صغير في كثير من الحالات ومهم في حالات قليلة. في المدى الطويل ، يكون الأمر مرتبطًا بفقدان الأداء الوظيفي للوظائف الافتراضية. نحن نتحدث عن dereference إضافية لكل وصول لكل عضو في البيانات ، بالإضافة إلى تخصيص الذاكرة الديناميكية للعبة pimpl ، بالإضافة إلى تحرير الذاكرة عند التدمير. إذا لم تتمكن فئة pimpl'd من الوصول إلى أعضاء البيانات الخاصة بها غالبًا ، يتم إنشاء كائنات الفئة pimpl'd غالبًا وتكون قصيرة العمر ، ثم يمكن للتخصيص الديناميكي أن يفوق التوقعات الإضافية.

قرار

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

لجميع الحالات الأخرى ، إنها مسألة ذوق بحت. جربه وقم بقياس أداء وقت التشغيل وأداء وقت الترجمة قبل اتخاذ قرار.





c++ oop pimpl-idiom