2016-03-08 94 views
0

說我有如下代碼:OOP分解和單元測試困境

class BookAnalysis { 
    final List<ChapterAnalysis> chapterAnalysisList; 
} 

class ChapterAnalysis { 
    final double averageLettersPerWord; 
    final int stylisticMark; 
    final int wordCount; 
    // ... 20 more fields 
} 

interface BookAnalysisMaker { 
    BookAnalysis make(String text); 
} 

class BookAnalysisMakerImpl implements BookAnalysisMaker { 
    public BookAnalysis make(String text) { 
    String[] chaptersArr = splitIntoChapters(text); 

    List<ChapterAnalysis> chapterAnalysisList = new ArrayList<>(); 
    for(String chapterStr: chaptersArr) { 
     ChapterAnalysis chapter = processChapter(chapterStr); 
     chapterAnalysisList.add(chapter); 
    } 

    BookAnalysis book = new BookAnalysis(chapters); 
    } 

    private ChapterAnalysis processChapter(String chapterStr) { 
     // Prepare 
     int letterCount = countLetters(chapterStr); 
     int wordCount = countWords(chapterStr); 
     // ... and 20 more 

     // Calculate 
     double averageLettersPerWord = letterCount/wordCount; 
     int stylisticMark = complexSytlisticAppraising(letterCount, wordCount); 
     HumorEvaluation humorEvaluation = evaluateHumor(letterCount, stylisticMark); 
     // ... and 20 more 

     // Return 
     return new ChapterAnalysis(averageLettersPerWord, stylisticMark, wordCount, ...); 
    } 
} 

在我的具體情況,我有嵌套的一個多水平(認爲BookAnalysis - > ChapterAnalysis - > SectionAnalysis)和幾個ChapterAnalysis上的類(認爲每章都包含PageAnalysis)和SectionAnalysis(認爲FootnotesAnalysis等)級別。我對如何構建這個問題感到困惑。的問題是,在processChapter方法:

  • 兩種製劑和計算步驟需要時間/資源的不可忽略量的
  • 計算步驟取決於多個準備步驟

一些憂慮:

  • 上面這個類,考慮到ChapterAnalysis中有20個字段會比較長
  • 測試整個測試需要一個非常複雜的準備方法,可以測試大量的代碼。要確認例如countLetters按預期工作,我不得不不必要的複製,幾乎整個輸入只是爲了測試兩種不同的情況下,countLetters行爲不同

解決方案包含的複雜性,並允許testabilty:

  • 拆分成processChapter私人方法,但不能/不應該測試它們
  • 拆分成多個類,但然後我需要大量的輔助數據類(對於計算階段中的每種方法)或一個大廚房水槽(保存所有數據準備階段)
  • 使輔助方法包私有。雖然這解決了測試問題,但我可以測試它們,但「我不應該」部分仍然適用

任何提示,特別是來自類似現實世界的經驗?

編輯:更新命名,並根據當前答案添加了一些說明。

我分裂成類的主要問題是它不是線性/單層。例如,以上countLetters產生complexSytlisticAppraising所需的結果。假設爲這兩種方法(LetterCounterComplexSytlisticAppraiser)分別制定類別是有意義的。現在,我必須做出獨立豆的ComplexSytlisticAppraiser.appraise輸入,即是這樣的:

class ComplexSytlisticAppraiserInput { 
    final int letterCount; 
    final int wordCount; 
    // ... 10 more things it might need 
} 

這很好,但現在我有HumorEvaluator爲此,我需要這樣的:

class HumorEvaluatorInput { 
    final int letterCount; 
    final int stylisticMark; 
    // ... 5 more things it might need 
} 

雖然這可能在許多情況下僅僅通過列出參數來完成,一個大問題是返回參數。即使當我必須返回兩個整數時,我也必須創建一個具有這兩個整數,構造函數,equals/hashCode和getters的獨立bean。

class HumorEvaluatorOutput { 
    final int letterCount; 
    final int stylisticMark; 

    public HumorEvaluatorOutput(int letterCount, int stylisticMark) { 
     this.letterCount = letterCount; 
     this.stylisticMark = stylisticMark; 
    } 

    public int getLetterCount() { 
     return this.letterCount; 
    } 

    public int getStylisticMark() { 
     return this.stylisticMark; 
    } 

    @Override 
    public String toString() { 
     StringBuilder sb = new StringBuilder(); 
     sb.append("HumorEvaluatorOutput [letterCount="); 
     sb.append(letterCount); 
     sb.append(", stylisticMark="); 
     sb.append(stylisticMark); 
     sb.append("]"); 
     return sb.toString(); 
    } 

    @Override 
    public int hashCode() { 
     final int prime = 31; 
     int result = 1; 
     result = prime * result + letterCount; 
     result = prime * result + stylisticMark; 
     return result; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     if (this == obj) 
     return true; 
     if (obj == null) 
     return false; 
     if (getClass() != obj.getClass()) 
     return false; 
     HumorEvaluatorOutput other = (HumorEvaluatorOutput) obj; 
     if (letterCount != other.letterCount) 
     return false; 
     if (stylisticMark != other.stylisticMark) 
     return false; 
     return true; 
    } 
} 

這是2對53行代碼 - yikes!

所以這一切是好的,但它:

  • 是不可重用。其中絕大多數僅用於使代碼可測試。想想分析儀,例如:BookAnalyzer,CarAnalyzer,GrainAnalyzer, ToothAnalyzer。他們分享絕對沒有共同
  • 使20班列的1不會產生太大除允許測試
  • 你可以爭辯說,不管是分成類或方法,作出足以零件小點的differene被理解和操縱的並不是那麼大
  • 另一方面,如果我想要在考慮可測試性的情況下進行適當的面向對象操作,將會出現大量的噪音和間接性。比較:
    • 管理10個文件= 10個分析儀* 1個文件與20種私有方法
    • 800文件= 10個分析器*(20個接口,20個implementions,20個輸入和20種輸出豆)
    • 400個文件,如果我們刪除輸入/輸出bean並轉到其他路徑(例如每個分析器破解一個大I/O bean) 請注意,數百個文件將非常短,主要是樣板文件 - 可能大多數邏輯將不到10行==第一種情況下的私有方法)
  • 有很大的開銷在這。如果我打電話給一個私有方法1m次,創建額外的輸入和輸出bean將會加起來......

也許做正確的事情就是做正確的事情。只是想看看是否有其他一些我可以追求的選擇,我錯過了。或者我的邏輯純粹是壞的?

編輯:根據評論的額外更新。我們可以HumorEvaluatorOutput較短,而不是一個巨大的問題:

class HumorEvaluatorOutput { 
    final HumorCategoryEnum humorCategory; 
    final int humorousWordsCount; 

    public HumorEvaluatorOutput(HumorCategoryEnum humorCategory, int humorousWordsCount) { 
     this.humorCategory = humorCategory; 
     this.humorousWordsCount = humorousWordsCount; 
    } 

    public HumorCategoryEnum getHumorCategory() { 
     return this.humorCategory; 
    } 

    public int getHumorousWordsCount() { 
     return this.humorousWordsCount; 
    } 
} 

這是2比17行代碼 - 仍然讓人驚訝!當你考慮一個例子時並不多。當您有20個不同的分析儀(BookAnalyzer,CarAnalyzer,...)和20個不同的子分析儀時(對於上述書:ComplexSytlisticAppraiserHumorEvaluator以及其他所有分析儀類似,顯然差別很大),代碼增加8倍向上。

至於BookAnalyzer VS CarAnalyzerBook VS Chapter子儀 - 其實,我需要比較BookAnalyzer VS CarAnalyzer,因爲這就是我都會有。我肯定會重用所有章節的章節子分析器。但是,我不會將其用於任何其他分析儀。即我也會有這樣的:

BookAnalyzer 
    ChapterSubAnalyzer 
    HumorSubAnalyzer 
    ... // 25 more 
CarAnalyzer 
    EngineSubAnalyzer 
    DrivertrainSubAnalyzer 
    ... // 15 more 
GrainAnalyzer 
    LiquidContentSubAnalyzer 
    FiberContentSubAnalyzer 
    ... // 20 more 

通過上面去,而不是每分析儀1級,我現在要創建20個接口,20極短的子類有20個輸入/輸出豆類和沒有人會永遠重新使用。分析書籍和汽車很少在流程的任何地方使用相同的方法和步驟。

再次 - 我很好,做了上述,但我只是沒有看到任何好處,除了允許測試。這就像駕駛Toyota Thundra到你的隔壁鄰居的派對。你能做到與所有其他參加派對的人一樣嗎?當然。你應該這樣做嗎? Ehhh ...

所以:

  • 難道真的更好地使在10個文件500線到800個文件5000行(可能不是完全正確的數字,但你明白了吧)剛剛跟隨OOP並啓用測試?
  • 如果沒有,其他人是如何做的並且仍然保持在不破壞OOP /測試「規則」的方面(例如,通過使用反射來測試不應該首先測試的私有方法)?
  • 如果是,其他人都這樣做,那很好。其實,那麼一個子問題 - 你如何設法找到你需要的東西,並在所有噪音中跟蹤應用程序的流程?
+1

有時可以用assert對私有方法進行測試,並將複雜但效率較高的算法與簡單但很慢的實現進行比較,或者檢查通常只是假定存在但未驗證的輸入/輸出的不變量。它並不能解決所有情況,但可以減輕編寫單元測試的需要。 – the8472

+0

@ the8472在這種特殊情況下,情況並非如此。我總體上同意,並且我總體上使用相同的原則,這非常有用。我在帖子中增加了更多信息以突出關注點/評論,現在可能會更清楚一些:) –

+0

@levantpied關於更新的一些初步想法。 'letterCount'確實屬於'HumorEvaluatorOutput',它似乎屬於其他地方,除非這是一個幽默因素?你爲參數類建議使用'hashCode','equals'和'toString'方法,你真的需要這些嗎,你是否將一個HumourEvaluatorOutput與另一個HumourEvaluatorOutput相比較?比較書和汽車分析儀,比較書和章。能夠分析一本書的一個章節是否有意義?它看起來可能是這樣,然後這可以在書中多個章節重複使用。 – forsvarir

回答

2

此問題可能更適合於CodeReview。也就是說,感覺就像你已經知道解決方案是將課程分成更小的類,以便更容易測試。

看着BookMakerImpl,它似乎已經在做至少兩個不同的工作。它將文本分成不同的部分,並對這些部分進行分析。乍一看,目前還不清楚你是否有命名問題。在您提供的代碼示例中,Chapter並不真正代表一個章節(就像我期望的那樣)。它實際上代表了給定章節的分析結果(你似乎沒有將章節文本傳遞給它的構造函數,儘管這可能是你的發佈代碼中的一個遺漏)。

您可能採取的一種方法來簡化測試(假設我對Chapter所代表的內容是正確的,如果不是這種方法會類似,但名稱和元素顯然需要更改)是將分析提取到一個(或更多班級)。用你提供的代碼,看起來你可以創建類似ChapterTextAnalyser類的東西。這將需要string(在示例中提供它將是章節文本),然後返回結果,如ChapterAnalysis(替換當前的Chapter類)。

如果你有​​和其他部分之間類似的分析,那麼這個結構可能需要修改,以使感,在給定域中,並在適當情況下共享功能,但本質上你可以有類似這樣的(僞代碼)的東西...

class BookAnalyserImpl implements BookAnalyser 
    // Pass in analyser factory and book parser 
    // to constructor so mocked version can 
    // be used for testing 
    public BookAnalyserImpl(TextAnalyserFactory textAnalyserFactory, 
          BookParser bookParser) { 
     if(null != textAnalyserFactory) { 
      mTextAnalyserFactory = textAnalyserFactory; 
     } else { 
      mTextAnalyserFactory = new AnalyserFactoryImpl(); 
     } 
     // Same for bookParser 
    } 
    BookAnalysis analyse(String bookText) { 
     BookAnalysis bookAnalysis = new BookAnalysis(); 
     ChapterAnalyser chapterAnalyser = mTextAnalyserFactory.GetChapterAnalyser(); 

     foreach(chapterText in mBookParser.splitIntoChapters(bookText)) { 
      bookAnalysis.AddChapterAnalysis(chapterAnalyser.analyse(chapterText)); 
     } 
    } 
} 

class TextAnalyserFactoryImpl implements TextAnalyserFactory { 
    ChapterAnalyser GetChapterAnalyser() {...} 
} 

class ChapterAnalyserImpl implements ChapterAnalyser { 
    ChapterAnalysis analyse(String chapterText) { ... } 
} 

正如你所說,這將導致你有更多的課程。如果這些課程有意義並且有明確的責任,這本身不是一件壞事。

如果您不喜歡有很多類的想法,那麼您可以簡單地將分析推送到另一個具有公共接口的類。

class BookAnalyser { 
    ChapterAnalysis analyseChapter(String text) { ... } 
    PageAnalysis analysePage(String text) {...} 
    // ... 
} 

這使得您想調用的方法成爲您要調用它的類的一部分功能,從而避免了私人測試問題。

針對部分修改:

首先,重要的是要記住,OOP是可選的,它是完全有效的採取alternate approach解決您的問題。

你真的在編寫分析書籍,汽車,糧食和牙齒的軟件嗎?這種感覺有點令人費解,這使得問題空間難以購買並理解,而你的編碼示例並不完整,這被放大了。雖然在問題域的當前迭代中,分析儀之間沒有明顯的共性,但不難想象分析可能類似的區域。例如孔隙度分析可應用於穀物,牙齒和書籍的頁面以提供有意義的信息。但是,您的圖書分析基於純文本輸入,因此這不太可能成爲問題域的一部分,至少對於Book而言。

在800個文件(可能不是完全正確的數字,但你明白了)中,將10個文件中的500行轉換爲5000行真的更好嗎?只是遵循OOP並啓用測試?

我不是軟件複雜性度量的代碼行的巨大粉絲。你最初的比較是2:53,你已經在C#中下降到2:17,比例將接近2:9,儘管實際差異是5(樣板行數)+ 2 *字段數量(一個分配線和一個獲取)。正在使用if .. else ...(5行)明顯比1行三元運算符更詳細/不太清晰?這是非常subjective,我曾在有編碼標準的地方工作,說你不能使用三元運算符。

4000000行代碼優於5000,似乎不太可能。但我也懷疑,如果你打破了問題領域,以提取明顯的功能和共同性,這將是結果。

考慮這條線從您的代碼

HumorEvaluation humorEvaluation = evaluateHumor(letterCount, stylisticMark); 

你沒有做與humorEvaluation評價任何東西,但是好像這代表了不同的事情。看起來這將會傳入在方法中構建並存儲在那裏的ChapterAnalysis。這消除了在章節級別存儲組成HumorEvaluation的各個字段的需要。這種不同的類型似乎也代表了與HumorEvaluatorOutput代表的概念相同的概念。除非您從中獲得一些好處,否則無需兩次表示相同的概念。在這裏你看起來並沒有那麼做,所以把它扔掉。

如果不是,其他人是怎麼做的,仍然保持在不破壞OOP /測試「規則」(例如通過使用反射來測試不應該首先測試的私有方法)的一面?

我不喜歡直接測試私有方法。它們是一個實現細節,直接測試它們很脆弱。從測試人員的角度來看,私有方法是否存在應該沒有關係,或者在測試中調用的方法中全部寫入。重要的是代碼整體的可衡量的副作用。你說:

測試整個人需要一個非常複雜的準備方法,將測試大量的代碼。要確認例如countLetters按預期工作,我不得不不必要的複製,幾乎整個輸入只是爲了測試兩種不同的情況下,countLetters行爲不同

現實情況是,從測試的角度看,重要的是,所構建的狀態Chapter是正確的。如果countLetters按預期工作,那麼它會。如果不是,那麼測試將失敗。特別是,如果countLetters沒有返回您期望的數字,那麼Chapter類上預期的averageLettersPerWord與預期的wordCount之間的關係將不正確。

看看你的一些方法,countLetterscountWords他們有不同的輸入和輸出,並且不修改他們所在類的狀態。正如我之前所說,這表明他們可能在一個不同的階級,他們是公開的。

class GenericTextAnalyserImpl implements GenericTextAnalyser { 
    int countLetters(String text); 
    int countWords(String text); 
    int complexSytlisticAppraising(int letterCount, int wordCount); 
    // ... 
} 

countLetters大概是一些可能由其他分析儀(圖書,章,頁等)一起使用。這些方法不再需要複製到這些其他類中,這些方法的測試變得微不足道。它還可以減少對Book等元素的測試,因爲您可以模擬調用以確保正在進行的調用,而不必爲要測試的每個變體複製整個調用結構。

如果是的,其他人都這樣做,那很好。其實,那麼一個子問題 - 你如何設法找到你需要的東西,並在所有噪音中跟蹤應用程序的流程?

如果您通過添加800個額外的類將5000行轉換爲4000000,那麼您可能找不到自己的方式。如果你創建了一組合理的類,將問題分解成這些區域內的區域和元素,那麼通常並不那麼困難。

+0

感謝forsvarir的闡述。我可以交給SE CR,但我不確定最好的選擇是什麼。我可能會將其標記爲考慮。我已編輯帖子以添加相關的其他信息,並根據您的建議進行更新。如果您有時間,請告訴我更新後您的想法。 –

1

你是對的,它不是一般考慮良好的做法,以測試私人方法。然而,理解這種情況爲什麼很重要,因爲只有這樣你才能判斷它是否適用於你的情況。

反對測試私有方法的主要觀點是預期爲測試代碼增加的維護工作量:如果軟件設計得當,私有元素比公共元素更有可能發生變化。因此,如果測試僅使用公共API,那麼保持測試代碼構建和正確工作的努力會更小。 (他們也應該最好只使用關於被測代碼的黑盒智慧,但這是一個不同的故事......)

看看你的例子,我認爲方法countLetters可能是私有的。然而,這個名字讓我覺得這個方法可能會在你的代碼中實現一個很好理解和穩定的概念。如果是這樣的話,一些替代的設計選項就是將這種方法分解成它自己的類 - 它不會是私人的。

然而,這個想法並不意味着暗示你將這個函數分解出來(你可以這樣做,但這不是我的觀點)。關鍵是要清楚地說明,這一切都歸結爲預計某段代碼會有多穩定的問題。

對於穩定性的這種期望必須與測試的努力進行權衡:首先測試私有元素可能更容易(這可以節省您的工作量),但從長遠來看這可能會讓您付出代價。可能仍然是,長期成本永遠不會超過短期勝利。這你必須判斷。

+0

謝謝德克 - 你的思路是好的。如果您有機會讓我知道您的想法,我已經編輯了答案,詳細闡述了您的想法和意見。 –