هل هذه هي الطريقة الصحيحة لاستخدام واختبار فئة تستفيد من نمط المصنع؟

StackOverflow https://stackoverflow.com/questions/1892532

سؤال

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

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

هذا هو الفصل الذي أحتاجه لاختباره:

public class SomeCalculator : ICalculateSomething
{
    private readonly IReducerFactory reducerFactory;
    private IReducer reducer;

    public SomeCalculator(IReducerFactory reducerFactory)
    {
        this.reducerFactory = reducerFactory;
    }

    public SomeCalculator() : this(new ReducerFactory()){}

    public decimal Calculate(SomeObject so)
    {   
        reducer = reducerFactory.Create(so.CalculationMethod);

        decimal calculatedAmount = so.Amount * so.Amount;

        return reducer.Reduce(so, calculatedAmount);
    }
}

فيما يلي بعض تعريفات الواجهة الأساسية ...

public interface ICalculateSomething
{
    decimal Calculate(SomeObject so);
}

public interface IReducerFactory
{
    IReducer Create(CalculationMethod cm);
}

public interface IReducer
{
    decimal Reduce(SomeObject so, decimal amount);
}

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

public class ReducerFactory : IReducerFactory
{
    public IReducer Create(CalculationMethod cm)
    {
        switch(cm.Method)
        {
            case CalculationMethod.MethodA:
                return new MethodAReducer();
                break;
            default:
                return DefaultMethodReducer();
                break;
        }
    }
}

هذه هي تقريب التطبيقين ... جوهر التنفيذ هو أنه يقلل فقط من المبلغ إذا كان الكائن في حالة معينة.

public class MethodAReducer : IReducer
{
    public decimal Reduce(SomeObject so, decimal amount)
    {   
        if(so.isReductionApplicable())
        {
            return so.Amount-5;
        }
        return amount;
    }
}

public class DefaultMethodReducer : IReducer
{
    public decimal Reduce(SomeObject so, decimal amount)
    {
        if(so.isReductionApplicable())
        {
            return so.Amount--;
        }
        return amount;
    }
}

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

[TestFixture]
public class SomeCalculatorTests
{
    private Mock<IReducerFactory> reducerFactory;
    private SomeCalculator someCalculator;

    [Setup]
    public void Setup()
    {
        reducerFactory = new Mock<IReducerFactory>();
        someCalculator = new SomeCalculator(reducerFactory.Object);     
    }

    [Teardown]
    public void Teardown(){}

الاختبار الأول

    //verify that we can calculate an amount
    [Test]
    public void Calculate_CalculateTheAmount_ReturnsTheAmount()
    {
        decimal amount = 10;
        decimal expectedAmount = 100;
        SomeObject so = new SomeObjectBuilder()
         .WithCalculationMethod(new CalculationMethodBuilder())                                                          
                     .WithAmount(amount);

        Mock<IReducer> reducer = new Mock<IReducer>();

        reducer
            .Setup(p => p.Reduce(so, expectedAmount))
            .Returns(expectedAmount);

        reducerFactory
            .Setup(p => p.Create(It.IsAny<CalculationMethod>))
            .Returns(reducer);

        decimal actualAmount = someCalculator.Calculate(so);

        Assert.That(actualAmount, Is.EqualTo(expectedAmount));
    }

الاختبار الثاني

    //Verify that we make the call to reduce the calculated amount
    [Test]
    public void Calculate_CalculateTheAmount_ReducesTheAmount()
    {
        decimal amount = 10;
        decimal expectedAmount = 100;
        SomeObject so = new SomeObjectBuilder()
         .WithCalculationMethod(new CalculationMethodBuilder())                                                          
                     .WithAmount(amount);

        Mock<IReducer> reducer = new Mock<IReducer>();

        reducer
            .Setup(p => p.Reduce(so, expectedAmount))
            .Returns(expectedAmount);

        reducerFactory
            .Setup(p => p.Create(It.IsAny<CalculationMethod>))
            .Returns(reducer);

        decimal actualAmount = someCalculator.Calculate(so);

        reducer.Verify(p => p.Reduce(so, expectedAmount), Times.Once());            
    }
}

فهل كل هذا ينظر أليس كذلك؟ أم أن هناك طريقة أفضل لاستخدام نمط المصنع؟

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

المحلول

إنه سؤال طويل جدا تسأل، ولكن هنا بعض الأفكار الضالة:

  • AFAIK، لا يوجد نمط "المصنع". هناك نمط يسمى مصنع مجردة ودعا آخر طريقة المصنع. وبعد الآن يبدو أنك تستخدم مصنع مجردة.
  • ليس هناك سبب أن somecalculator لها كلاهما reducerFactory و reducer ميدان. تخلص من أحدهم - في تنفيذك الحالي، لا تحتاج reducer ميدان.
  • جعل الاعتماد على حقن (reducerFactory) يقرأ فقط.
  • تخلص من المنشئ الافتراضي.
  • قد يكون بيان التبديل في المخفض رائحة رمز. ربما يمكنك نقل طريقة الإبداع إلى فئة حساب حسابية. من شأنه أن يغير أساسا مصنع مجردة إلى طريقة المصنع.

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

نعم، هناك سعر صغير يجب دفعه مقابل ذلك، لكنه يستحق كل هذا العناء.


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

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

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

لا تحتاج إلى حاوية DI للقيام بكل هذا - يمكنك الحصول على حالات الأسلاك يدويا بدلا من ذلك. هذا يسمي نقية دي.

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

public virtual IReducer CreateReducer()
{
    return new DefaultMethodReducer();
}

بالنسبة لحسابها الخاص، يمكنك بعد ذلك تجاوز طريقة CreaterEducer وإرجاع مخفض مختلف:

public override IReducer CreateReducer()
{
    return new MethodAReducer();
}

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

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