التصميم الموجه نحو البيانات (أو لماذا ، باستخدام OOP ، ربما تطلق النار على نفسك)

صورة

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

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

كل شيء عن البيانات


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

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

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

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

بيانات مثالية


استدعاء التسلسل مع نهج وجوه المنحى

الشكل 1 أ. استدعاء تسلسل مع نهج وجوه المنحى

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

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

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

استدعاء التسلسل مع نهج البيانات المنحى

الشكل 1 ب. استدعاء التسلسل في تقنية موجهة البيانات

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

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

فوائد التصميم الموجه للبيانات


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

تواز


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

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

استخدام ذاكرة التخزين المؤقت


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

خيار التحسين


عندما نفكر في كائنات أو وظائف ، نركز عادةً على تحسين مستوى دالة أو حتى خوارزمية: نحاول تغيير ترتيب استدعاءات الوظائف ، أو تغيير طريقة الفرز ، أو حتى إعادة كتابة جزء من كود C بلغة التجميع.

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

نمطية


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

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

تجريب


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

عيوب التصميم الموجه للبيانات


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

المشكلة الرئيسية في التصميم الموجه نحو البيانات: إنها تختلف عما تعلمه معظم المبرمجين ويعتادون عليه. يتطلب تحويل نموذجنا العقلي للبرنامج تسعين درجة وتغيير وجهة نظر عليه. لكي تصبح هذه المقاربة طبيعة ثانية ، يلزم الممارسة.

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

باستخدام التصميم الموجه للبيانات


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

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

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

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

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

يجب أن يكون لديك الآن فهم جيد للغاية لبياناتك. كتابة التعليمات البرمجية لتحويلها سوف تصبح أسهل بكثير. سيكون مثل إنشاء رمز عن طريق ملء الفراغات. سوف تفاجأ بسرور أن الشفرة تبين أنها أبسط وأكثر إحكاما مما كنت تعتقد في الأساس ، مقارنة بنفس كود OOP.

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

هل هناك مساحة متبقية لاستخدام OOP؟


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

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

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

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

Source: https://habr.com/ru/post/ar472052/


All Articles