16

我在一個支持大約10個使用我們的代碼的產品的大型平臺項目上工作。大型項目DI的重構

到目前爲止,所有的產品一直在使用我們的平臺的全部功能:
- 配置數據檢索從數據庫
- 遠程文件系統訪問
- 安全授權
- 基本邏輯(我們付出的東西)

對於一款新產品,我們被要求支持功能的一個較小的子集,而不需要平臺帶來的基礎架構。我們的架構比較陳舊(從2005年左右開始編碼),但相當穩固。

我們有信心我們可以在我們現有的課程上使用DI來完成這項工作,但估計的時間範圍爲5到70周,具體取決於您與誰交流。

有很多文章告訴你如何做DI,但是我沒有發現任何告訴你如何以最有效的方式重構DI的文章?有沒有工具可以做到這一點,而不必經過30,000行代碼,並且必須打CTRL + R才能獲取接口並將它們添加到construcors中多次? (如果有幫助,我們會重新提供幫助)如果不是的話,您會發現什麼是快速實現這一目標的理想工作流程?

+1

您可以通過重構僅爲當前任務所需的代碼的小部分開始。通過這種方式,你和你的團隊將獲得DI的感受並收集一些DI經驗。由於DI激勵的解耦架構非常友好,您可以使用單元測試來確保不會破壞任何東西。 – lasseeskildsen

+1

這個問題可能更適合http://programmers.stackexchange.com –

+2

這裏沒有工具。考慮一下你將如何自己分析每個類來找出要提取的依賴關係。一個工具不能可靠地爲你做這個分析(或者它會提取太多)。然而,有些工具(如Resharper和Code Rush)可以幫助您提取提取方法並提取類重構,但這仍然適用於當時的單個類;而不是整個項目的一次點擊。 – Steven

回答

2

感謝所有的答覆。我們現在差不多一年了,我想我主要回答我自己的問題。

lasseeskildsen指出,我們當然只是將我們的平臺的部分產品重新用於新產品。由於這只是代碼庫的部分轉換,我們採用DIY方法進行依賴注入。

我們的重點是使這些部件可用,而不帶來不必要的依賴關係,而不是啓用它們的單元測試。這在解決問題的方式上有所不同。這種情況下沒有真正的設計更改。

所涉及的工作是世俗的,因此如何快速甚至自動地完成這項工作。 答案是它不能被自動化,但使用一些鍵盤快捷鍵並重新使用它可以很快完成。對我來說,這是最佳流程:

  1. 我們在多個解決方案中工作。我們創建了一個包含所有解決方案文件中所有項目的臨時「主」解決方案。雖然重構工具並不總是足夠聰明,可以找出二進制和項目引用之間的差異,但這至少可以使它們在多個解決方案中部分工作。

  2. 創建您需要剪切的所有依賴項的列表。按功能分組。在大多數情況下,我們能夠一次處理多個相關的依賴關係。

  3. 您將在許多文件中進行許多小代碼更改。此任務最好由單個開發人員完成,或者最多兩個,以避免不斷合併您的更改。

  4. 擺脫單身首先: 從這個模式轉換他們離開後,提取接口(ReSharper的 - >重構 - >提取接口) 刪除單訪問得到構建錯誤的列表。轉到第6步。

  5. 要擺脫其他參考: a。如上所述提取界面。 b。註釋掉原來的實現。這會得到一個構建錯誤列表。

  6. ReSharper的現在變成了很大的幫助:

    • ALT + SHIFT + PG /掉電快速導航時斷引用。
    • 如果多個引用共享一個公共基類,請導航到其構造函數,然後按ctrl + r + s(「change method signature」)將新接口添加到構造函數中。 Resharper 8爲您提供了「通過調用樹解析」的選項,這意味着您可以讓繼承類自動更改其簽名。這是一個非常整潔的功能(新版本8似乎)。
    • 在構造函數體中,將注入的接口分配給一個不存在的屬性。按Alt + Enter鍵選擇「創建屬性」,將其移動到需要的位置,然後完成。從5b取消註釋代碼。
  7. 測試!沖洗並重復。

要使用這些類的原液沒有重大更改代碼,我們創建了通過服務定位器取回他們的依賴,因爲佈雷特Veenstra提到的重載構造。這可能是一種反模式,但適用於這種情況。在所有代碼支持DI之前它不會被刪除。

我們在大約2-3周(1.5人)內將約四分之一的代碼轉換爲DI。 還有一年,我們現在將所有的代碼切換到DI。隨着焦點轉向單元可測試性,這是一種不同的情況。我認爲上述的一般步驟仍然有效,但是這需要一些實際的設計更改來實施SOC。

0

不要認爲有任何工具可以做這種代碼轉換。

因爲 - >

在現有的代碼庫使用DI將涉及,

  • 使用接口/抽象類。再次,在這裏應該採取正確的選擇,以簡化保持DI原理&代碼功能的轉換。

  • 有效隔離/統一多個/單個類中的現有類,以保持代碼模塊化或小型可重用單元。

0

我接近轉換的方式是查看永久修改狀態的系統的任何部分;文件,數據庫,外部內容。一旦改變並重新閱讀,它有沒有改變?這是第一個想要改變它的地方。

所以你要做的第一件事,就是發現修改這樣一個源的地方:

class MyXmlFileWriter 
{ 
    public bool WriteData(string fileName, string xmlText) 
    { 
     // TODO: Sort out exception handling 
     try 
     { 
     File.WriteAllText(fileName, xmlText); 
     return true; 
     } 
     catch(Exception ex) 
     { 
     return false; 
     } 
    } 
} 

其次,你寫一個單元測試,以確保你是不是打破了代碼,而重構。

[TestClass] 
class MyXmlWriterTests 
{ 
    [TestMethod] 
    public void WriteData_WithValidFileAndContent_ExpectTrue() 
    { 
     var target = new MyXmlFileWriter(); 
     var filePath = Path.GetTempFile(); 

     target.WriteData(filePath, "<Xml/>"); 

     Assert.IsTrue(File.Exists(filePath)); 
    } 

    // TODO: Check other cases 
} 

接下來,從原來的類提取接口:

interface IFileWriter 
{ 
    bool WriteData(string location, string content); 
} 

class MyXmlFileWriter : IFileWriter 
{ 
    /* As before */ 
} 

重新運行測試,並希望一切都很好。保持原來的測試,因爲它正在檢查您的舊實施作品。

接下來寫一個什麼都不做的假實現。我們只想在這裏實現一個非常基本的行爲。

// Put this class in the test suite, not the main project 
class FakeFileWriter : IFileWriter 
{ 
    internal bool WriteDataCalled { get; private set; } 

    public bool WriteData(string file, string content) 
    { 
     this.WriteDataCalled = true; 
     return true; 
    } 
} 

然後單元測試...

class FakeFileWriterTests 
{ 
    private IFileWriter writer; 

    [TestInitialize()] 
    public void Initialize() 
    { 
     writer = new FakeFileWriter(); 
    } 

    [TestMethod] 
    public void WriteData_WhenCalled_ExpectSuccess() 
    { 
     writer.WriteData(null,null); 
     Assert.IsTrue(writer.WriteDataCalled); 
    } 
} 

現在用它進行單元測試和重構仍在工作,我們需要確保注射時的版本中,調用類所使用的接口,不具體版本!

// Before 
class FileRepository 
{ 
    public FileRepository() { } 

    public void Save(string content, string xml) 
    { 
     var writer = new MyXmlFileWriter(); 
     writer.WriteData(content,xml); 
    } 
} 

// After 
class FileRepository 
{ 
    private IFileWriter writer = null; 

    public FileRepository() : this(new MyXmlFileWriter()){ } 
    public FileRepository(IFileWriter writer) 
    { 
     this.writer = writer; 
    } 

    public void Save(string path, string xml) 
    { 
     this.writer.WriteData(path, xml); 
    } 
} 

那麼我們做了什麼?

  • 有一個使用普通型
  • 有一個構造函數的IFileWriter
  • 用一個實例字段來保存所引用對象的默認構造函數。

那麼它編寫單元測試爲FileRepository和檢查,該方法被調用的情況:

[TestClass] 
class FileRepositoryTests 
{ 
    private FileRepository repository = null; 

    [TestInitialize()] 
    public void Initialize() 
    { 
    this.repository = new FileRepository(new FakeFileWriter()); 
    } 

    [TestMethod] 
    public void WriteData_WhenCalled_ExpectSuccess() 
    { 
     // Arrange 
     var target = repository; 

     // Act 
     var actual = repository.Save(null,null); 

     // Assert 
     Assert.IsTrue(actual); 
    } 
} 

好了,但在這裏,我們是否真的測試FileRepositoryFakeFileWriter?我們正在測試FileRepository,因爲我們的其他測試正在單獨測試FakeFileWriter。這個類 - FileRepositoryTests對於測試空值的傳入參數會更有用。

假的是沒有做任何聰明的事 - 沒有參數驗證,沒有I/O。它只是坐在裏面,以便FileRepository可以保存任何工作的內容。其目的是雙重的;顯着加快單元測試並且不會破壞系統狀態。

如果這個FileRepository也必須讀取文件,那麼也可以實現一個IFileReader(這有點極端),或者只是將最後寫入的filePath/xml存儲到內存中的字符串中,然後檢索它。


因此,隨着基礎知識 - 如何處理這個?

對於需要大量重構的大型項目,最好將單元測試合併到經歷DI更改的任何類中。理論上說,你的數據不應該被提交給[在你的代碼中]的數百個位置,而是通過幾個關鍵位置。在代碼中找到它們併爲它們添加一個接口。我用一個技巧是隱藏每個DB或指數類源這樣的界面背後:

interface IReadOnlyRepository<TKey, TValue> 
{ 
    TValue Retrieve(TKey key); 
} 

interface IRepository<TKey, TValue> : IReadOnlyRepository<TKey, TValue> 
{ 
    void Create(TKey key, TValue value); 
    void Update(TKey key, TValue); 
    void Delete(TKey key); 
} 

其中規定您從數據源中一個非常通用的方法來檢索。您可以從XmlRepository切換到DbRepository,只需更換它的注入位置即可。這對於從一個數據源遷移到另一個數據源而不影響系統內部的項目非常有用。它可以簡單地將XML操作更改爲使用對象,但使用此方法維護和實現新功能要容易得多。

我可以給出的唯一的其他建議是每次做1個數據源並堅持下去。抵制一次性做太多的誘惑。如果你最終不得不一次保存到文件,數據庫和網絡服務中,請使用Extract Interface,假冒這些調用並不返回任何內容。這是一個真正的雜耍行爲,一次做很多,但你可以比從第一個原則開始更容易地插入。

祝你好運!

+0

你的方法聽起來很合理。但是使用一些模仿庫創建一個IFileWriter的存根而不是創建一個FakeFileWriter會更好嗎? –

+0

是的,嘲笑是好的。保存你編寫啞版實現的單元測試。再次使用'CreateInstance'方法來設置模擬對象並集中創建和必要的場景。 –

+0

「有一個使用正常類型的默認構造函數」我認爲如果可能,最好避免這種情況。問題是你的類仍然很難連接到接口的具體實現。理想情況下,我們只想依靠接口。默認的構造函數強制具體依賴。 – Robin

3

我假設你正在尋找使用一個IoC的工具,像StructureMap,Funq,Ninject等

在這種情況下,重構工作真正與更新您的入口點(或Composition Roots)開始在代碼庫中。這可能會產生很大的影響,特別是如果您正在普遍使用靜態和管理對象的生命週期(例如緩存,延遲加載)。一旦你有了一個IoC工具並且連接了對象圖表,你就可以開始展示你對DI的使用並享受其好處。

我會首先關注類似設置的依賴項(應該是簡單的值對象),並開始使用IoC工具進行解析調用。接下來,看看創建Factory類並注入這些類來管理對象的生命週期。它會感覺到你正在倒退(並且緩慢),直到你到達頂峯,你的大部分物體都使用DI,並且推測SRP - 從那裏開始它應該是下坡。一旦你有更好的問題分離,你的代碼庫的靈活性和你可以進行更改的速度將大大增加。

謹慎的說法:不要讓自己被愚弄到思想中,到處都是「服務定位器」是你的靈丹妙藥,它其實是一個DI antipattern。我認爲需要首先使用,但是您應該使用構造函數或setter注入來完成DI工作,並刪除Service Locator。

0

這本書很可能是非常有幫助的:

與遺留代碼一起工作有效 - 邁克爾羽毛 - http://www.amazon.com/gp/product/0131177052

我建議開始與小的變化。逐步移動依賴項以通過構造函數注入。始終保持系統正常工作。從構造函數中提取接口注入依賴關係,並開始使用單元測試進行包裝。有意義時帶上工具。您不必立即開始使用依賴注入和模擬框架。您可以通過構造函數手動注入依賴關係來進行大量改進。

1

你問過關於工具。一個可能有助於像這樣大型重構的工具是nDepend。我用它來幫助識別重定位的目標。

我毫不猶豫地提到它,因爲我不想給出這樣的印象:nDepend這樣的工具對於承擔這個項目是必要的。但是,可視化代碼庫中的依賴關係很有幫助。它配備了爲期14天的全功能試用版,可能足以滿足您的需求。

0

你所描述的是這個過程的重要組成部分;遍歷每個課程,創建一個界面並註冊。如果您立即提交重構到組合根,那麼這是最大的問題,在MVC的情況下,這意味着假設您要注入控制器。

這可能是很多工作,如果代碼做了很多直接的對象創建,它可能是非常複雜的嘗試一次完成所有。在這些情況下,我認爲可以使用服務定位器模式並手動調用解析。

首先將您的某些直接調用替換爲帶有服務定位器解析調用的構造函數。這將降低最初需要的重構量,並開始爲您提供DI的好處。

隨着時間的推移,您的調用將越來越接近組合根,然後您可以開始移除服務定位器的使用。