Zenject: كيف يمكن لحاوية IoC أن تقتل حقن التبعية في مشروعك

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

من قد يكون مهتمًا بهذا؟


ستكون هذه المقالة مفيدة لكل من الذين هم على دراية بأتباع DI والمبتدئين. لفهم المعرفة الأساسية الكافية حول الأنماط التي يستخدمها DI ، والغرض من DI والوظائف التي تؤديها حاوية IoC. لا يتعلق الأمر بتعقيدات تنفيذ Zenject ، ولكن حول تطبيق جزء من وظائفه. تعتمد المقالة فقط على وثائق Zenject الرسمية وأمثلة التعليمات البرمجية منه ، وكذلك على كتاب Mark Siman "Dependency Injection in .NET" ، وهو عمل شامل شامل حول موضوع نظرية DI. جميع الاقتباسات في هذا المقال مقتطفات من كتاب مارك سيمان. على الرغم من حقيقة أننا سنتحدث عن حاوية معينة ، قد تكون المقالة مفيدة لأولئك الذين يستخدمون حاويات أخرى.

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

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

مقدمة


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

لقد استخدمت Zenject في 3 مشاريع Unity كبيرة ، وتواصلت أيضًا مع عدد كبير من المطورين الذين استخدموها. غالبًا ما يتكرر سبب كتابة هذه المقالة في الأسئلة:

  • هل استخدام Zenject هو حل جيد؟
  • ما هو الخطأ في Zenject؟
  • ما الصعوبات التي تنشأ عند استخدام Zenject؟

وأيضًا بعض المشاريع التي لم يؤد فيها استخدام Zenject إلى حل مشكلات الاتصال القوي بالشفرة والعمارة الفاشلة ، بل على العكس فاقم الوضع.

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

قبل البدء في تفكيك وظيفة Zenject الخطيرة المحتملة ، من المنطقي تحديث العديد من الجوانب الأساسية لـ DI بشكل سطحي.

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

  1. تنفيذ المنشئ - كيف يمكننا ضمان أن التبعية المطلوبة ستكون متاحة دائمًا للفئة التي يتم تطويرها؟
  2. تنفيذ الملكية - كيف يمكنني تمكين DI كخيار في الفصل إذا كان هناك تقصير محلي مناسب؟
  3. تطبيق الطريقة - كيف يمكنني حقن التبعيات في الفصل إذا كانت مختلفة لكل عملية؟
  4. السياق المحيط - كيف يمكننا جعل التبعية متاحة في كل وحدة نمطية دون تضمين الجوانب الشاملة للتطبيق في كل مكون API؟

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

وظائف خطيرة.


تنفيذ الخصائص


هذا هو ثاني نمط DI الأكثر شيوعًا ، بعد تنفيذ المُنشئ ، ولكن يتم استخدامه بشكل أقل كثيرًا. نفذت في Zenject على النحو التالي:

public class Foo { [Inject] public IBar Bar { get; private set; } } 

بالإضافة إلى ذلك ، يمتلك Zenject أيضًا مفهومًا مثل "الحقن الميداني". دعونا نرى لماذا تعتبر هذه الوظيفة الأكثر خطورة في Zenject.

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

المصانع (و MemoryPool)


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

 public class Enemy { DiContainer Container; public Enemy(DiContainer container) { Container = container; } public void Update() { ... var player = Container.Resolve<Player>(); WalkTowards(player.Position); ... etc. } } 

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

  public abstract class PlaceholderFactory<TValue> : IPlaceholderFactory { [Inject] void Construct(IProvider provider, InjectContext injectContext) 

نرى فيه المعلمة InjectContext التي تحتوي على العديد من المنشئات ، من النموذج:

  public InjectContext(DiContainer container, Type memberType) : this() { Container = container; MemberType = memberType; } 

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

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

تنفيذ الأسلوب


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

 public class Foo { [Inject] public Init(IBar bar, Qux qux) { _bar = bar; _qux = qux; } } 

ما هي عيوب هنا:

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

ويترتب على ذلك أن وجود مثل هذا التنفيذ لتطبيق الطريقة في الحاوية ، والذي يتعارض مع نظرية DI ، ليس له زائد واحد. مع التحذير الكبير ، لا يمكن اعتبار الجمع إلا إمكانية استخدام الطريقة المنفذة ، كمنشئ لـ MonoBehaviour. ولكن هذه لحظة مثيرة للجدل إلى حد ما ، حيث أنه من وجهة نظر منطق الحاوية وأنماط DI وجهاز الذاكرة الداخلية Unity3D ، يمكن اعتبار جميع كائنات MonoBehaviour في تطبيقك مُدارة من قبل الموارد ، وفي هذه الحالة ، سيكون من الأفضل تفويض إدارة دورة حياة هذه الكائنات ليست حاوية DI ، ولكن فئة مساعدة (سواء كانت Wrapper أو ViewModel أو Fasade أو أي شيء آخر).

الارتباطات العالمية


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

المعرفات


القدرة على تعيين ربط محدد لمعرف من أجل الحصول على تبعية معينة من مجموعة من التبعيات المماثلة في فئة. مثال:

 Container.Bind<IFoo>().WithId("foo").To<Foo1>().AsSingle(); Container.Bind<IFoo>().To<Foo2>().AsSingle(); public class Bar1 { [Inject(Id = "foo")] IFoo _foo; } public class Bar2 { [Inject] IFoo _foo; } 

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

الإشارات و ITickable


الإشارات هي نظير لآلية مجمِّع الأحداث المضمنة في الحاوية. إن فكرة تنفيذ هذه الوظيفة هي بلا شك نبيلة ، لأنها تهدف إلى تقليل عدد الاتصالات بين الكائنات التي تتواصل من خلال آلية الاشتراكات في الأحداث. يمكن العثور على مثال ضخم إلى حد ما في الوثائق ، ومع ذلك ، لن يكون في المقالة ، لأن التنفيذ المحدد لا يهم.

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

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

بدلاً من الإخراج


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

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


All Articles