提炼方法顾名思义是将一个代码片段提炼出来放在一个独立的方法中。提炼方法是重构手段中最简单也是最常用的一个手段,我自己在重构一段代码中,很多时候第一件事就是提炼方法。
那么该怎么使用这个手段呢,什么情况下需要使用这个手段。很多人可能也都知道,当一个方法较长时,就可以提炼方法来保证每个方法都足够的精短;当一个代码片段被多个地方使用时,考虑到代码复用,我们就可以将其提炼出来。这两种情况都没有错,但我认为最合理的一个观点是:提炼方法,以将意图与实现分开。如果你需要花时间浏览一段代码才弄清它到底在干什么,那么就应该将其提炼到一个方法中,并根据它所做的事为其命名。以后再读到这段代码时,我们通过方法名就能知道方法的用途,大多时候并不关系方法是怎么达到目的的。
方法名应该描述方法的用途,而不是方法是怎么实现的。
我们先看下下面这段代码:
public static void merge(BInvoiceReg ivcReg) {
List<BIvcRegSumLine> bIvcRegSumLines = new ArrayList<BIvcRegSumLine>();
int lineNumber = 0;
// 按照合同,科目,税率,单价进行分组
Collection<List> group = CollectionHelper.group(ivcReg.getRegLines(),
new Comparator<IvcRegLine>() {
@Override
public int compare(IvcRegLine o1, IvcRegLine o2) {
if (ObjectUtils.equals(o1.getAcc1().getContract().getUuid(), o2.getAcc1().getContract()
.getUuid())
&& ObjectUtils.equals(o1.getAcc1().getSubject().getUuid(), o2.getAcc1()
.getSubject().getUuid())
&& ObjectUtils.equals(o1.getAcc1().getTaxRate().getRate(), o2.getAcc1()
.getTaxRate().getRate()) && ObjectUtils.equals(o1.getPrice(), o2.getPrice())){
return 0;
} else{
return 1;
}
}
});
for (List<IvcRegLine> regLines : group) {
if (regLines.size() > 1) {
// 按照起始日期升序
Collections.sort(regLines, new Comparator<IvcRegLine>() {
@Override
public int compare(IvcRegLine o1, IvcRegLine o2) {
if (o1.getAcc1().getBeginTime() == null) {
return -1;
} else if (o2.getAcc1().getBeginTime() == null) {
return 1;
} else {
return o1.getAcc1().getBeginTime().compareTo(o2.getAcc1().getBeginTime());
}
}
});
// 根据账期二次分组的结果(用于校验)
List<List<IvcRegLine>> lists = new ArrayList<List<IvcRegLine>>();
Map<DateRange,List<IvcRegLine>> rangeListMap = new HashMap<>();
for (int i = 0; i < regLines.size(); i++) {
boolean isContain = false;
List<IvcRegLine> ivcRegLines = new ArrayList<IvcRegLine>();
isContain = isContain(lists, regLines.get(i));
if (!isContain) {
ivcRegLines.add(regLines.get(i));
Date tmpBegin = regLines.get(i).getAcc1().getBeginTime() == null ? null
: (Date) regLines.get(i).getAcc1().getBeginTime().clone();
Date tmpEnd = regLines.get(i).getAcc1().getEndTime() == null ? null : (Date) regLines
.get(i).getAcc1().getEndTime().clone();
DateRange dateRange = new DateRange(tmpBegin,tmpEnd);
rangeListMap.put(dateRange,ivcRegLines);
lists.add(ivcRegLines);
mergeIncludeDateRange(i,dateRange,lists,regLines,ivcRegLines);
}
if (!CollectionUtils.isEmpty(ivcRegLines)) {
lineNumber = lineNumber + 1;
bIvcRegSumLines.add(createNewSumLine(ivcRegLines, lineNumber));
}
}
} else {
lineNumber = lineNumber + 1;
bIvcRegSumLines.add(createNewSumLine(regLines, lineNumber));
}
}
ivcReg.setIvcRegSumLines(bIvcRegSumLines);
}
如果让你们去维护这段代码,你们一定跟我一样。
(1)一个方法写这么长,有毛病吧。
(2)这都写了些啥,它想干嘛,谁能告诉我。
(3)改不动,还是不改了,重新写一个吧。
上面的代码是我工作项目中真是存在的一段代码,当时碰到一个死循环的问题,然后被迫来修复这段代码。实际上这段代码的逻辑并不复杂,但刚拿到这段代码,光理解逻辑就理了一天(主要逻辑在mergeIncludeDateRange
方法,这里就不展示了)。不过最终我也没在这个代码上改,而是……另外写了。如下:
public static List<BIvcRegSumLine> merge(List<IvcRegLine> regLines) {
List<BIvcRegSumLine> result = new ArrayList<>();
if (CollectionUtils.isEmpty(regLines)) {
return result;
}
Collection<List> lists = groupByLineKey(regLines);
for (List<IvcRegLine> lines : lists) {
List<BIvcRegSumLine> sumLines = createSumLines(lines);
if (sumLines.isEmpty() == false) {
result.addAll(sumLines);
}
}
sortBySubjectCodeASC(result);
LineNumberGenerator lineNumberGenerator = new LineNumberGenerator();
result.forEach(line -> line.setLineNumber(lineNumberGenerator.next()));
return result;
}
看完重构后的这段方法,也许不能完全分析出整个方法的逻辑,但至少大意能够完全理清楚吧?
(1)将入参regLines
按照lineKey
进行分组,其中lineKey
是用于表示一个InvRegLine
对象的复合键。
(2)循环遍历每组,然后根据组内的lines
创建sumLines
,这里其实是根据一定规则将lines
再次分组并汇总数据。
(3)将汇总后的数据按照subjectCode
字段升序排序。
所以你们是喜欢看重构前还是重构后的代码呢?