4

我正在開發一個側面項目,以便更好地理解控制和依賴注入的反轉以及不同的設計模式。在依賴注入中使用策略和工廠模式

我想知道是否有使用DI的工廠和戰略模式的最佳實踐

我的挑戰來自何時一個策略(由工廠構建)需要不同的參數爲每個可能的構造函數和實現。因此,我發現自己在服務入口點聲明所有可能的接口,並通過應用程序傳遞它們。因此,必須爲新的和各種策略類實現更改入口點。

爲了下面的說明,我已經組合了一個配對的例子。我的這個項目的堆棧是.NET 4.5/C#和Unity for IoC/DI。

在本示例應用程序中,我添加了一個默認的程序類,負責接受虛構的訂單,並根據訂單屬性和所選的運輸供應商計算運輸成本。對於UPS,DHL和Fedex有不同的計算方法,並且每個實現可能會或可能不會依賴額外的服務(打擊數據庫,api等)。

public class Order 
{ 
    public string ShippingMethod { get; set; } 
    public int OrderTotal { get; set; } 
    public int OrderWeight { get; set; } 
    public int OrderZipCode { get; set; } 
} 

虛擬程序或服務來計算運費成本

public class Program 
{ 
    // register the interfaces with DI container in a separate config class (Unity in this case) 
    private readonly IShippingStrategyFactory _shippingStrategyFactory; 

    public Program(IShippingStrategyFactory shippingStrategyFactory) 
    { 
     _shippingStrategyFactory = shippingStrategyFactory; 
    } 

    public int DoTheWork(Order order) 
    { 
     // assign properties just as an example 
     order.ShippingMethod = "Fedex"; 
     order.OrderTotal = 90; 
     order.OrderWeight = 12; 
     order.OrderZipCode = 98109; 

     IShippingStrategy shippingStrategy = _shippingStrategyFactory.GetShippingStrategy(order); 
     int shippingCost = shippingStrategy.CalculateShippingCost(order); 

     return shippingCost; 
    } 
} 

// Unity DI Setup 
public class UnityConfig 
{ 
    var container = new UnityContainer(); 
    container.RegisterType<IShippingStrategyFactory, ShippingStrategyFactory>(); 
    // also register IWeightMappingService and IZipCodePriceCalculator with implementations 
} 

public interface IShippingStrategyFactory 
{ 
    IShippingStrategy GetShippingStrategy(Order order); 
} 

public class ShippingStrategyFactory : IShippingStrategyFactory 
{ 
    public IShippingStrategy GetShippingStrategy(Order order) 
    { 
     switch (order.ShippingMethod) 
     { 
      case "UPS": 
       return new UPSShippingStrategy(); 

      // The issue is that some strategies require additional parameters for the constructor 
      // SHould the be resolved at the entry point (the Program class) and passed down? 
      case "DHL": 
       return new DHLShippingStrategy(); 

      case "Fedex": 
       return new FedexShippingStrategy(); 

      default: 
       throw new NotImplementedException(); 
     } 
    } 
} 

現在的策略接口和實現。 UPS是一個簡單的計算,而DHL和Fedex可能需要不同的服務(和不同的構造參數)。

public interface IShippingStrategy 
{ 
    int CalculateShippingCost(Order order); 
} 

public class UPSShippingStrategy : IShippingStrategy() 
{ 
    public int CalculateShippingCost(Order order) 
    { 
     if (order.OrderWeight < 5) 
      return 10; // flat rate of $10 for packages under 5 lbs 
     else 
      return 20; // flat rate of $20 
    } 
} 

public class DHLShippingStrategy : IShippingStrategy() 
{ 
    private readonly IWeightMappingService _weightMappingService; 

    public DHLShippingStrategy(IWeightMappingService weightMappingService) 
    { 
     _weightMappingService = weightMappingService; 
    } 

    public int CalculateShippingCost(Order order) 
    { 
     // some sort of database call needed to lookup pricing table and weight mappings 
     return _weightMappingService.DeterminePrice(order); 
    } 
} 

public class FedexShippingStrategy : IShippingStrategy() 
{ 
    private readonly IZipCodePriceCalculator _zipCodePriceCalculator; 

    public FedexShippingStrategy(IZipCodePriceCalculator zipCodePriceCalculator) 
    { 
     _zipCodePriceCalculator = zipCodePriceCalculator; 
    } 

    public int CalculateShippingCost(Order order) 
    { 
     // some sort of dynamic pricing based on zipcode 
     // api call to a Fedex service to return dynamic price 
     return _zipCodePriceService.CacluateShippingCost(order.OrderZipCode); 
    } 
} 

上述問題是每個策略都需要額外的不同服務來執行'CalculateShippingCost'方法。這些接口/實現是否需要在入口點(程序類)註冊並通過構造函數傳遞?

是否有其他模式更適合完成上述場景?也許是Unity可以專門處理的事情(https://msdn.microsoft.com/en-us/library/dn178463(v=pandp.30).aspx)?

我非常感謝任何幫助或在正確的方向微調。

感謝, 安迪

+1

請參見[使用DI和Ioc的工廠方法](http://stackoverflow.com/a/31971691/181087)。 – NightOwl888

回答

5

有這樣做的幾種方法,但我喜歡的方式是注入可用策略的列表到您的工廠,然後過濾他們返回的一個(S )你感興趣。

你的榜樣工作,我會修改IShippingStrategy添加一個新的屬性:

public interface IShippingStrategy 
{ 
    int CalculateShippingCost(Order order); 
    string SupportedShippingMethod { get; } 
} 

然後,我對工廠像這樣:

public class ShippingStrategyFactory : IShippingStrategyFactory 
{ 
    private readonly IEnumerable<IShippingStrategy> availableStrategies; 

    public ShippingStrategyFactory(IEnumerable<IShippingStrategy> availableStrategies) 
    { 
     this.availableStrategies = availableStrategies; 
    } 

    public IShippingStrategy GetShippingStrategy(Order order) 
    { 
     var supportedStrategy = availableStrategies 
       .FirstOrDefault(x => x.SupportedShippingMethod == order.ShippingMethod); 
     if (supportedStrategy == null) 
     { 
      throw new InvalidOperationException($"No supported strategy found for shipping method '{order.ShippingMethod}'."); 
     } 

     return supportedStrategy; 
    } 
} 

主要的原因,我喜歡用這樣我就不必回來修改工廠了。如果我不得不實施新的戰略,工廠不必改變。如果你在你的容器中使用自動註冊,你甚至不需要註冊新的策略,所以這只是一個讓你花更多時間來編寫新代碼的例子。

+0

感謝您的幫助@John H.我爲此問題添加了一個附加答案,並將您的答案與Silas Reinagel的答案結合在一起。 – apleroy

1

註冊和使用策略類型的字符串解決這些問題。

像這樣:

// Create container and register types 
IUnityContainer myContainer = new UnityContainer(); 
myContainer.RegisterType<IShippingStrategy, FedexShippingStrategy>("Fedex"); 
myContainer.RegisterType<IShippingStrategy, DHLShippingStrategy>("DHL"); 

// Retrieve an instance of each type 
IShippingStrategy shipping = myContainer.Resolve<IShippingStrategy>("DHL"); 
IShippingStrategy shipping = myContainer.Resolve<IShippingStrategy>("Fedex"); 
+0

感謝您的幫助@Silas Reinagel。我在John H的帖子的一些細節中加入了你的答案。我很感激。 – apleroy

1

請參閱John H和Silas Reinagel的答案。他們都非常有幫助。

我最終做了兩個答案的組合。

我更新了John H所提到的工廠和界面。

然後在Unity容器中,我添加了像Silas Reinagel所示的新命名參數的實現。

然後,我在這裏回答了使用Unity將注射集註冊到戰略工廠的答案。 Way to fill collection with Unity

現在每個策略都可以單獨實施而不需要修改上游。

謝謝大家。

4

應用依賴注入時,我們將所有類的依賴性定義爲構造函數中的必需參數。這將創建從類到客戶的依賴關係的負擔。同樣的規則也適用於班級的消費者。他們也需要在構造函數中定義它們的依賴關係。這一直沿着調用堆棧,這意味着所謂的「對象圖」在某些點上可能變得相當深刻。

依賴注入導致創建類直到應用程序入口點的責任; Composition Root。不過這意味着入口點需要知道所有的依賴關係。如果我們不使用DI容器 - 一種練習叫做Pure DI - 這意味着此時所有依賴關係必須在普通的舊C#代碼中創建。如果我們使用DI容器,我們仍然必須告訴DI容器所有的依賴關係。

但是有時候我們可以利用的技術,稱爲批處理或自動註冊,其中DI容器將使用反射在我們的項目和使用Convention over Configuration註冊類型。這爲我們節省了逐個註冊所有類型的負擔,並且經常阻止我們在每次將新類添加到系統時對Composition Root進行更改。

這些接口/實現是否需要註冊入口點(Program類)並通過構造函數傳遞?

絕對。

因此,我發現自己在服務入口點聲明所有可能的接口,並通過應用程序傳遞它們。因此,必須爲新的和各種策略類實現更改入口點。

應用程序的入口點是系統中最易變的部分,即使沒有DI也是如此。但是在DI中,我們讓系統的其他部分變得更加不穩定。再次,我們可以通過應用自動註冊來減少我們在入口點處所需的代碼更改量。

我想知道是否有與工廠和戰略模式使用DI的最佳做法?

我想說的是關於工廠的最佳做法是儘可能少地使用它們,正如this article中所解釋的那樣。事實上,你的工廠界面是多餘的,只會使需要它的用戶複雜化(如文章中所解釋的)。您的應用程序很容易就沒有,您可以直接注入IShippingStrategy,因爲這是消費者感興趣的唯一事情:獲得訂單的運輸成本。它並不關心它背後是否有一個或幾十個實現。它只是想要得到的運輸成本,並與它的工作繼續下去:

public int DoTheWork(Order order) 
{ 
    // assign properties just as an example 
    order.ShippingMethod = "Fedex"; 
    order.OrderTotal = 90; 
    order.OrderWeight = 12; 
    order.OrderZipCode = 98109; 

    return shippingStrategy.CalculateShippingCost(order); 
} 

這意味着然而,該注射航運戰略,現在必須的東西,可以決定如何計算基礎上,Order.Method財產的成本。但是有一種稱爲代理模式的模式。這裏有一個例子:

public class ShippingStrategyProxy : IShippingStrategy 
{ 
    private readonly DHLShippingStrategy _dhl; 
    private readonly UPSShippingStrategy _ups; 
    //... 

    public DHLShippingStrategy(DHLShippingStrategy dhl, UPSShippingStrategy ups, ...) 
    { 
     _dhl = dhl; 
     _ups = ups; 
     //... 
    } 

    public int CalculateShippingCost(Order order) => 
     GetStrategy(order.Method).CalculateShippingCost(order); 

    private IShippingStrategy GetStrategy(string method) 
    { 
     switch (method) 
     { 
      case "DHL": return dhl; 
      case "UPS": return ups: 
      //... 
      default: throw InvalidOperationException(method); 
     } 
    } 
} 

該代理行爲的內部有點像一個工廠,但這裏有兩個重要的區別:

  1. 沒有定義不同的接口。這允許消費者僅依賴於1概念:IShippingStrategy
  2. 它不會創建策略本身;他們仍然被注入到它。

該代理只是簡單地將傳入呼叫轉發到執行實際工作的基礎策略實施。

有多種方法可以實現這樣的代理。例如,您仍然可以手動在此處創建依賴關係 - 或者您可以將調用轉發給容器,容器將爲您創建依賴項。此外,注入依賴關係的方式可能因您的應用程序的最佳選擇而有所不同。

即使這樣的代理可能像工廠一樣在內部工作,但重要的是這裏沒有工廠抽象;這隻會使消費者複雜化。