4

假設我有一些DDD服務需要一些IEnumerable<Foo>來執行一些計算。我想出了兩種設計:我是否應該將存儲庫接口與域模型分離

  1. 摘要與IFooRepository接口的數據訪問,這是相當典型的

    public class FooService 
    { 
        private readonly IFooRepository _fooRepository; 
    
        public FooService(IFooRepository fooRepository) 
         => _fooRepository = fooRepository; 
    
    
        public int Calculate() 
        { 
         var fooModels = _fooRepository.GetAll(); 
         return fooModels.Sum(f => f.Bar); 
        } 
    } 
    
  2. 不要依賴IFooRepository抽象和注入IEnumerable<Foo>直接

    public class FooService 
    { 
        private readonly IEnumerable<Foo> _foos; 
    
        public FooService(IEnumerable<Foo> foos) 
         => _foos = foos; 
    
    
        public int Calculate() 
         => _foos.Sum(f => f.Bar); 
    }  
    

這第二個設計s在我看來,更好的是FooService現在不關心數據來自何處,Calculate變成純粹的域邏輯(忽略IEnumerable可能來自不純的來源)。

使用第二個設計另一種說法是,當IFooRepository通過網絡進行異步IO,它通常會希望使用async-await,如:

public class AsyncDbFooRepository : IFooRepository 
{ 
    public async Task<IEnumerable<Foo>> GetAll() 
    { 
     // Asynchronously fetch results from database 
    } 
} 

但是當你需要async all the way downFooService現在不得不將其簽名更改爲async Task<int> Calculate()。這似乎違反了dependency inversion principle

但是,第二個設計也存在問題。首先,你必須依靠的DI容器(使用簡單注射器在這裏舉例),或組成根源上解決,如數據訪問代碼:

public class CompositionRoot 
{ 
    public void ComposeDependencies() 
    { 
     container.Register<IFooRepository, AsyncDbFooRepository>(Lifestyle.Scoped); 

     // Not sure if the syntax is right, but it demonstrates the concept 
     container.Register<FooService>(async() => new FooService(await GetFoos(container))); 
    } 

    private async Task<IEnumerable<Foo>> GetFoos(Container container) 
    { 
     var fooRepository = container.GetInstance<IFooRepository>(); 
     return await fooRepository.GetAll(); 
    } 
} 

而且在我的具體情況,AsyncDbFooRepository需要某種的運行參數來構建,這意味着您需要一個abstract factory來構造AsyncDbFooRepository

隨着抽象工廠,現在我必須管理所有依賴AsyncDbFooRepositoryAsyncDbFooRepository下的對象圖是不平凡的)的生命週期。我有一種預感,如果我選擇第二種設計,我會錯誤地使用DI。


總之,我的問題是:

  1. 我在我的第二個設計不當使用DI?
  2. 我該如何爲我的第二個設計令人滿意地構建我的依賴關係?
+0

如果Foo是運行時數據,則不應將其注入到組件構造函數中。閱讀:https://cuttingedge.it/blogs/steven/pivot/entry.php?id = 99 – Steven

+0

感謝您的鏈接。根據你的博客,我應該注入'AsyncDbFooRepository'而不是(首次設計),但是這不會違反依賴倒置原則(如問題所述)嗎? – rexcfnghk

+0

我會建議你使用存儲庫層。您可以擁有SqlFooRepository,FileSystemFooRepository,Neo4JFooRepository等。您可以從數據更改位置獲取好處。如果您在構造函數中通過IEnumerable ,則會限制自己,例如將數據添加回存儲庫,刪除等。 – user1628733

回答

2

async/await的一個方面是,它的根據定義需要應用「一路下來」,因爲你正確地說。但是,在注射IEnumerable<T>時,您仍然無法阻止使用Task<T>,正如您在第二種選擇中所建議的那樣。您將不得不將Task<IEnumerable<T>>注入到構造函數中,以確保數據異步檢索。注入IEnumerable<T>時,它意味着枚舉集合時線程被阻塞 - 或者 - 在對象圖構造期間必須加載所有數據。

但是在對象圖構造期間加載數據是有問題的,因爲我解釋here的原因。除此之外,由於我們在這裏處理數據集合,這意味着所有數據都必須從每個請求的數據庫中獲取,即使並非所有數據都可能是必需的或者甚至是被使用的。這可能會導致相當的性能損失。

我是否在我的第二個設計中錯誤地使用了DI?

這很難說。一個IEnumerable<T>是一個流,所以你可以認爲它是一個工廠,這意味着注入一個IEnumerable<T>不需要運行時數據在對象構造過程中加載。只要滿足這個條件,注入一個IEnumerable<T>就可以,但仍然不可能使系統異步。

但是,注射IEnumerable<T>時,最終可能會含糊不清,因爲注入IEnumerable<T>的含義可能並不十分清楚。這個集合是一個懶散評估的流嗎?它是否包含T的所有元素。運行時數據或服務是T

爲了避免這種混淆,在抽象背後加載運行時信息通常是最好的選擇。爲了使您的生活更輕鬆,你可以使存儲庫抽象通用以及:

public interface IRepository<T> where T : Entity 
{ 
    Task<IEnumerable<T>> GetAll(); 
} 

這可以讓你有一個通用的實現,使一個單一的登記系統中的所有實體。

我該如何爲我的第二個設計令人滿意地構建我的依賴關係?

你不行。爲了做到這一點,您的DI容器必須能夠異步解析對象圖。例如,它需要下列API:

Task<T> GetInstanceAsync<T>() 

,但簡單的注射卻沒有這樣的API,而且也不現有的DI容器任何其他,這就是很好的理由。原因在於對象構造必須是simple,fast和reliable,並且在對象圖構造期間執行I/O時會丟失該對象。

因此,不僅第二個設計不可取,在對象構造過程中加載數據時不可能這樣做,而不會破壞系統的異步性,並在使用DI容器時導致線程阻塞。

+0

感謝您的詳細解釋,如果我添加一個假設,IEnumerable 依賴將永遠是一個完整的集合(即運行時類型實際上是一個完全枚舉的集合,'ICollection '/'ISet ')是否會改變你的答案? – rexcfnghk

+1

@rexcfnghk不,它不。只要您將預加載的運行時數據注入到應用程序組件中,事情就會變得更加棘手。用什麼抽象表示這個運行時數據並不重要。 – Steven

0

我儘可能地嘗試(直到現在我每次都成功了),不注入任何在我的域模型中執行IO的服務,因爲我喜歡保持它們純淨且沒有副作用。

這就是說第二種解決方案似乎更好,但方法public int Calculate()的簽名有問題:它使用一些隱藏的數據來執行計算,所以它不是明確的。在這樣的情況下,我喜歡瞬態輸入數據作爲輸入參數直接傳遞到這樣的方法:

public int Calculate(IEnumerable<Foo> foos) 

通過這種方式,很清楚什麼方法需要和返回結果是什麼(基於組合類名稱和方法名稱)。

+0

我承認'Calculate'方法不是一個很好的例子,但希望它能證明我的問題。 – rexcfnghk

+0

此外,這個答案似乎並沒有回答問題 – rexcfnghk

+0

調用者已知'foos'還是邏輯上綁定到'Calculate'操作?也就是說,在'sum(a,b)'中有'a'和'b'' –

相關問題