2

私はファクトリ パターンの経験があまりなく、それが必要だと思われるシナリオに出くわしましたが、パターンを正しく実装したかどうか確信が持てず、その影響が心配です。単体テストの可読性に問題がありました。

仕事で取り組んでいるシナリオの本質を (記憶から) 近似するコード スニペットを作成しました。誰かがそれを見て、私がしたことが合理的であるかどうかを確認していただければ幸いです。

これは私がテストする必要があるクラスです:

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);
}

これが私が作った工場です。私の現在の要件では、特定のシナリオで使用する特定の Reducer MethodAReducer を追加する必要があるため、ファクトリを導入しようとしています。

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

これらは 2 つの実装の概算です...実装の本質は、オブジェクトが特定の状態にある場合にのみ量を削減することです。

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());            
    }
}

それで、それはすべて正しく見えますか?または、ファクトリ パターンを使用するより良い方法はありますか?

4

1 に答える 1

9

あなたが尋ねているのはかなり長い質問ですが、ここにいくつかの迷いの考えがあります:

  • 私の知る限り、「ファクトリー」パターンはありません。Abstract FactoryというパターンとFactory Methodというパターンがあります。現在、Abstract Factory を使用しているようです。
  • SomeCalculator にreducerFactoryreducerフィールドの両方がある理由はありません。それらの 1 つを取り除きます。現在の実装では、reducerフィールドは必要ありません。
  • 注入された依存関係 ( reducerFactory) を読み取り専用にします。
  • デフォルトのコンストラクターを取り除きます。
  • ReducerFactory の switch ステートメントは、コードの匂いかもしれません。おそらく、作成メソッドを CalculationMethod クラスに移動できます。それは本質的に、Abstract Factory を Factory Method に変更します。

いずれにせよ、疎結合の導入には常にオーバーヘッドが伴いますが、これをテスト容易性のためだけに行っているとは思わないでください。テスト容易性は実際には Open/Closed Principleにすぎないため、テストを可能にするだけでなく、さまざまな方法でコードをより柔軟にすることができます。

はい、それには少額の費用がかかりますが、それだけの価値があります。


ほとんどの場合、注入された依存関係は読み取り専用にする必要があります。技術的には必要ではありませんが、フィールドを C#readonlyキーワードでマークすることは、安全性を高めるために適切です。

DI を使用する場合は、一貫して使用する必要があります。これは、オーバーロードされたコンストラクターがさらに別のアンチパターンであることを意味します。これにより、コンストラクターがあいまいになり、密結合漏れやすい抽象化につながる可能性もあります。

これは連鎖しており、欠点のように見えるかもしれませんが、実際には利点です。他のクラスで SomeCalculator の新しいインスタンスを作成する必要がある場合は、それを再度注入するか、それを作成できる抽象ファクトリを注入する必要があります。次に、SomeCalculator (ISomeCalculator など) からインターフェイスを抽出し、代わりにそれを挿入すると、利点が得られます。これで、SomeCalculator のクライアントを IReducer および IReducerFactory から効果的に切り離すことができました。

これらすべてを実行するために DI コンテナーは必要ありません。代わりに、インスタンスを手動で接続できます。これをピュア DIと呼びます。

ReducerFactory のロジックを CalculationMethod に移動することになると、仮想メソッドについて考えていました。このようなもの:

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

特別な Cal​​culationMethods の場合、CreateReducer メソッドをオーバーライドして、別のレデューサーを返すことができます。

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

この最後のアドバイスが理にかなっているかどうかは、私が持っていない多くの情報に依存するため、検討する必要があると言っているだけです- あなたの特定のケースでは意味がないかもしれません.

于 2009-12-12T07:11:34.393 に答える