dependency-injection - شرح - ما هو dependency injection




لماذا يستخدم أحد حقن التبعية؟ (4)

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

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

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


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

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

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

الآن تخيل أنك ستحتاج الآن إلى الحفاظ على كل هذا الكود في فصل واحد لأنه لا يتم فصله بشكل صحيح ، يمكنك أن تتخيل أنه بالنسبة لكل معالج جديد ستدعمه ، ستحتاج إلى إنشاء حالة جديدة لـ // switch ومع كل طريقة ، فإن هذا الأمر يصبح أكثر تعقيدًا ، وذلك باستخدام "Dependency Injection" (أو Inversion of Control - كما يطلق عليه أحيانًا ، مما يعني أن من يتحكم في تشغيل البرنامج لا يعرف إلا في وقت التشغيل ، وليس التعقيد) ، يمكنك تحقيق شيء ما أنيق جدا وصيانتها.

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** لن يتم تجميع الرمز ، أعرف :)


أعتقد أن الكثير من المرات يربك الناس حول الفرق بين حقن التبعية وإطار حقن التبعية (أو حاوية كما يطلق عليها غالباً).

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

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

تكتب رمزًا مثل هذا:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

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

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


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

هذا هو شائع بالفعل في OOP لفترة طويلة. في كثير من الأحيان إنشاء أجزاء التعليمات البرمجية مثل:

I_Dosomething x = new Impl_Dosomething();

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

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

مثال DI:

I_Dosomething x = DIContainer.returnThat();

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

D (b) I: Dependency Injection and Design by Interface. هذا القيد ليس مشكلة عملية كبيرة جدا رغم ذلك. الفائدة من استخدام D (b) I هو أنه يخدم الاتصال بين العميل والموفر. الواجهة هي منظور على كائن أو مجموعة من السلوكيات. هذا الأخير مهم هنا.

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


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

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

ميزة بلدي:

عندما أنظر إلى عرض ما ، أريد أن أضع علامة على نظرتي إليه تلقائيًا ، حتى لا أنسى القيام بذلك.

قد تختبر هذا كما يلي:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

لذلك في مكان ما في OfferWeasel ، فإنه يبني لك عرض كائن مثل هذا:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

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

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

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

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

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

بالكاد ستحتاج أكثر من مسجل واحد. ومع ذلك ، فإن حقن التبعية ضرورية للكود المكتوب بشكل ثابت مثل Java أو C #.

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

آمل أن ساعد.





dependency-injection