Java 面向对象的分解与单元测试困境
假设我有这样的代码:Java 面向对象的分解与单元测试困境,java,unit-testing,oop,Java,Unit Testing,Oop,假设我有这样的代码: class BookAnalysis { final List<ChapterAnalysis> chapterAnalysisList; } class ChapterAnalysis { final double averageLettersPerWord; final int stylisticMark; final int wordCount; // ... 20 more fields } interface B
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, ...);
}
}
这很好,只是现在我有了HumorEvaluator
,我需要这个:
class HumorEvaluatorInput {
final int letterCount;
final int stylisticMark;
// ... 5 more things it might need
}
虽然在许多情况下,这可能只是通过列出参数来实现,但一个大问题是返回参数。即使我必须返回两个int,我也必须创建一个单独的bean,其中包含这两个int、constructor、equals/hashCode和getter
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行代码-哎呀
所以这一切都很好,但它:
- 不可重复使用。其中绝大多数将仅用于使代码可测试。请考虑以下分析器:
,BookAnalyzer
,CarAnalyzer
,GrainAnalyzer
。他们毫无共同之处TootAnalyzer
- 在1个类中设置20个类,除了允许测试外,不会产生太多结果
- 你可以争辩说,无论它被划分成类还是方法,从使部分小到足以被理解和操作的角度来看,区别并没有那么大
- 另一方面,如果我在考虑可测试性的情况下进行适当的OOP,那么会增加大量的干扰和间接性。比较:
- 管理10个文件=10个分析器*1个文件和20个专用方法
- 800个文件=10个分析器*(20个接口、20个实现、20个输入和20个输出bean)
- 400个文件,如果我们删除输入/输出bean并采用其他方法(例如每个analyzer hack使用一个大I/O bean) 请注意,数百个文件将非常短,大部分是样板文件-可能大多数逻辑都在10行以下(=第一种情况下的私有方法)
- 这里面有很大的开销。如果我调用一个私有方法一百万次,那么创建额外的输入和输出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行代码-仍然是yikes!当你考虑一个例子的时候并不多。当你有20个不同的分析器(BookAnalyzer
,CarAnalyzer
,…)和20个不同的子分析器(对于上面的书:ComplexSytlisticaAppraiser
和HumoralEvaluator
和所有其他分析器类似,显然是不同的类别),代码增加了8倍
至于BookAnalyzer
vsCarAnalyzer
和Book
vsChapter
子分析器-实际上,我需要比较BookAnalyzer
vsCarAnalyzer
,因为这就是我将拥有的。我一定会在所有章节中重复使用章节子分析器。但是,我不会将其重新用于任何其他分析仪。也就是说,我要这个:
BookAnalyzer
ChapterSubAnalyzer
HumorSubAnalyzer
... // 25 more
CarAnalyzer
EngineSubAnalyzer
DrivertrainSubAnalyzer
... // 15 more
GrainAnalyzer
LiquidContentSubAnalyzer
FiberContentSubAnalyzer
... // 20 more
如上所述,我现在必须创建20个接口、20个极短的子类和20个输入/输出bean,而不是每个分析器1个类,并且没有一个类会被重用。在分析书籍和汽车的过程中,很少使用相同的方法和步骤
再说一次——我对做上述工作很满意,但我只是觉得除了允许测试之外,没有任何好处。这就像开车去隔壁邻居的派对。你能做到和其他来参加聚会的人一样吗?当然你应该这样做吗?呃
因此:
- 仅仅为了遵循OOP并启用测试,将10个文件中的500行转换为800个文件中的5000行(可能不是完全正确的数字,但你明白了这一点)真的更好吗
- 如果没有,其他人如何做到这一点,并且仍然站在不违反OOP/测试“规则”的一边(例如,通过使用反射来测试不应该首先测试的私有方法)
- 如果是的,其他人都是这样做的,好吧。事实上,还有一个子问题——你如何找到你需要的东西,并在如此嘈杂的环境中遵循应用程序的流程
countlets
可能是私有的。然而,这个名称给我的印象是,这个方法可能会在代码中实现一个易于理解且稳定的概念。如果是这样的话,一些替代的设计选项将是将这个方法分解成它自己的类——它将不再是私有的
然而,这个想法并不意味着建议您考虑这个函数(您可以这样做,但这不是我的观点)。关键是要明确,这一切归结为一个问题,即某段代码的稳定性如何
这种对稳定性的期望必须与测试的努力相权衡:首先,测试私有元素可能更容易(这节省了您的努力),但从长远来看,这可能会让您付出代价。也许长期成本永远不会超过短期收益。您必须对此作出判断。您是对的,测试私有方法通常被认为不是一种好的做法。然而,重要的是要理解为什么会这样,因为只有这样你才能判断我
BookAnalyzer
ChapterSubAnalyzer
HumorSubAnalyzer
... // 25 more
CarAnalyzer
EngineSubAnalyzer
DrivertrainSubAnalyzer
... // 15 more
GrainAnalyzer
LiquidContentSubAnalyzer
FiberContentSubAnalyzer
... // 20 more
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) {...}
// ...
}
HumorEvaluation humorEvaluation = evaluateHumor(letterCount, stylisticMark);
class GenericTextAnalyserImpl implements GenericTextAnalyser {
int countLetters(String text);
int countWords(String text);
int complexSytlisticAppraising(int letterCount, int wordCount);
// ...
}