文章主要讨论了软件团队开发中代码质量的重要性以及如何提高团队成员能力。文章以一个具体项目为例,指出代码质量问题以及修复过程中遇到的困难,强调开发人员在初期就应注意代码质量,并遵循面向对象设计原则。同时,文章也提到了培养和提高研发团队成员能力的重要性,明确了技术路线下的细分方向,并强调了突破发展瓶颈的必要性。
文章指出开发人员缺乏精益求精的精神,代码存在不必要的重复和难以理解的逻辑,使用了复杂的容器对象导致性能问题和代码可读性差的后果。同时提到修复bug时的困难,因为遗留代码使用了复杂的Map结构,涉及广泛公开的方法和类,改善需要花费大量时间和资源。
文章提到虽然认识到了代码编写的诸多问题,并想到了更符合OO原则的解决方案,但由于遗留代码使用复杂容器对象的类和方法非常多,且没有充分的单元测试,改善需要大量时间和资源,难以在短期内见效。
文章强调了培养和提高研发团队成员能力的重要性,包括明确职业目标、选择发展方向、提高自我突破能力、保持持续学习等。提出了技术人员在进入发展瓶颈期后需要寻找突破方向,否则能力将难以提升。
EISaaS团队的开发人员无疑具备了快速实现软件功能的能力,但由于他们从一开始缺乏编写高质量代码的正确认识和严格训练,写出来的代码质量确实不敢恭维。public Object load(String name) {
Object obj = null;
Object objFromMap = map.get(name);
if (objFromMap != null) {
obj = objFromMap;
return obj;
} else {
Object objFromSpring = springFunctionLoader.load(name);
if (objFromSpring != null) {
obj = objFromSpring;
map.put(name, objFromSpring);
return obj;
} else {
Object objFromReflect = reflectionFunctionLoader.load(name);
if (objFromReflect != null) {
map.put(name, objFromReflect);
obj = objFromReflect;
return obj;
}
}
}
return null;
}
load()方法期望根据名字加载对应的对象,整个加载过程分为三个步骤:- 检查缓存中是否已经存在符合条件的对象,如果有,则返回,否则执行第2步;
- 通过Spring框架加载符合条件的对象,如果加载成功,加入缓存并返回,否则执行第3步;
通过反射创建符合条件的对象,如果创建成功,加入缓存并返回,否则返回null
返回的null的问题暂且不说,毕竟那个时候的Java版本还未引入Optional,开发人员应该也不知道Null Object模式。这里的问题在于不必要的代码重复。如果只使用一个临时变量obj,代码更简洁,也能减少不必要的临时变量,而多余的else也让代码更难理解。改进如下:public Object load(String name) {
Object obj = map.get(name);
if (obj != null) {
return obj;
}
obj = springFunctionLoader.load(name);
if (obj != null) {
map.put(name, obj);
return obj;
}
obj = reflectionFunctionLoader.load(name);
if (obj != null) {
map.put(name, obj);
}
return null;
}
整个方法少了7行,没有不必要的嵌套,逻辑更加清晰。类似这样的问题固然是初学者容易犯的毛病,却也是因为开发人员缺乏精益求精的精神,只管实现,不考虑可读性、可重用性、可扩展性等内部质量。我当时还未加入ThoughtWorks,对一些好的敏捷实践缺乏直观认识,也没有培养和提升团队成员能力的意识。看到一些具体问题,我会指出,却从未想过要在团队内部规定良好的编码纪律,形成良好的学习氛围,培养他们追求卓越的工匠意识。当时的我也不知道持续集成,也不曾用过代码的静态检查工具,在面对大量的代码内部质量问题,尤其是可读性、可复用性问题时,没有坚持精益求精,反而是得过且过。记得当时公司市场部反馈过来一个要命的bug,其他开发人员都在产品开发任务上,就由我来认领缺陷修复的任务。该缺陷来自成绩报告。需求假定客户设置分数段时,不同的分数段有不同的有效分,对应了不同的名次。这些数据都是经过分析器分析获得,并持久化到数据库中。需要生成成绩报告时,会从数据库中获取分析结果,将数据填充到iReport设置好的模板中,一个是二维表,一个是柱状和曲线图。现在发现某些学校需要给不同的分数段设置完全相同的有效分以及相同的名次。报告打印出来,二维表没有错,曲线图却出现了“缺斤少两”的现象。例如设置五个分数段,却可能只显示了四条曲线。通过阅读代码,我明白了原因。在实现中,由于默认不同分数段有不同名次,因此,在获取这些分数段的值时,将它们放入一个Map>
中。这个Map是根据科目进行分类的,子Map的key值为Integer类型,为分数段对应的名次,value则是设置的有效分。现在,因为作为key的名次出现重复,就会“吞”掉对应的有效分,导致Map的元素存在偏差。这就是五个分数段只显示四条曲线的缘由。且不说Bug的修复,单说这样的代码实现就够人头疼的,因为用到了类似Map>
这般的“超级容器”。这样的超级容器往往成为坏代码的泥沼。将这样的对象作为参数,在方法之间传来传去时,会带来诸多问题:- 强类型:虽然这里使用了泛型,但泛型类型却使用了基本类型;
- 封装性不够好:容器对象暴露了太多的数据细节,且不利于为其定义职责行为。
- 可读性差:看到这样的Map,你并不会在第一时间明白它到底存放了什么。
- 可扩展性差:当这个Map作为方法的参数时,相当于这个参数没有被对象化。如果拥有这个参数的方法被公开,且广泛调用,一旦需要改变参数,牵连到的代码就会非常多。
事实正是如此。当我在分析产品的遗留代码时,发现很多地方都在重复获取这个Map对象,该Map对象也在多个方法之间传递。例如这样的代码:public static JFreeChart createEliteTotalChart(Grade grade, String partial,
ExamSet es, Student stu, Map subToStuTotalMap,
MapMap> subToValidScoreMap,
List subList,long stuRank,
String[] validLineSeries,double barWidth,Color barColor) {
if (subToStuTotalMap == null || subList == null) {
return null;
}
Color[] color = {Color.RED,Color.BLUE,Color.GREEN,
Color.CYAN,Color.BLACK,Color.MAGENTA};
CategoryDataset[] dataset = EIDatasetFactory.createEliteTotalDataset(
subToStuTotalMap, subToValidScoreMap, subList,validLineSeries);
}
注意,在createEliteTotalChart()
方法中,调用了EIDatasetFactory
的createEliteTotalDataset()
,对Map>
对象进行了处理。EIDatasetFactory
正是一个公开的公共类,createEliteTotalDataset()
方法也是一个被广泛调用的方法。因为这种容器对象自身的缺陷,为我的bug修复带来了很多阻碍。要解决这个Bug,就不能再将名次作为Map的key值。查看相关的数据表,事实上我们还给出了一个分数段的名称。当名次和有效分存在重复的情况下,结合分数段名称就能确定唯一值。一个简单的做法就是将Map的key修改为“名次加名称”的组合,即将Integer修改为String。另一种做法则是运用对象的封装来实现这一目标,如定义ValidScore和ValidScoreRange。ValidScore定义了属性:Rank、LineName和Score,而作为ValidScore集合的ValidScoreRange,可以完全替换之前的Map
。一旦定义了这两个对象,就可以将与之相关的领域行为分配给它们,职责就会变得更加集中,避免了相同逻辑的代码蔓延。虽然当初我认识到了代码编写的诸多问题,也想到了更加符合OO原则的解决方案,并能够利用重构来完成代码的改造。但是,我却下不了这个决心。当前,我的最高优先级是修复Bug,用最简单的解决方案,几分钟就可以完成,且可以保证不会引入新的Bug,这虽然是得过且过的做法,却是目前最经济最高效的选择。倘若在这时要追求代码质量的精益求精,成本就太大了。首先,遗留代码使用Map>
的类和方法非常多,并被广泛公开,调用它们的代码不知凡几,甚至多不胜数。其次,遗留代码并没有提供充分的单元测试,因而无法保证通过重构对代码进行修改后,确保修改的正确性。最后,改善代码的工作量太大,在产品能够正常运行的前提下,我很难向CEO争取到一个较长时期的重构时间。CEO必须考虑成本,他要考虑花这么多时间去做看似不能增加功能的重构是否值得。因此,在这里时间才是真正的罪恶。不过,最大的罪恶还是人,是编写代码的人。如果在一开始开发系统时,每个开发人员就能注意代码质量,随时重构,并遵循面向对象设计原则,情况就会变得焕然一新。这就是所谓的“破窗理论”。只可惜,当时的团队成员既没有这个意识,也没有这个能力。在我先后经历了ThoughtWorks多个交付项目与咨询项目的锤炼后,我充分认识到团队成员能力培养的重要性,也积累了一些经验和方法,并在离开ThoughtWorks之后,在多家公司和团队运用过,效果也还不错。正好有专栏的读者问到此类问题,故而打算提前在本文给出我的意见,同时作为EISaaS项目札记的收官总结,就算介绍完了第一个让我印象深刻的项目。要培养并提升研发团队的成员能力,首先得明确各个成员的职业目标是什么,至少得明确1-3年的发展目标。在软件公司,常见的发展方向就是三条线:技术、管理和技术管理。这里单谈技术路线。单单是技术路线,其实也有更细的路线,例如成长为一名架构师、算法工程师、测试工程师、运维工程师、数据库专家、领域专家等。即便是研发岗,不同公司不同团队对技能的要求也是五花八门。刚刚踏入这个行业的菜鸟们根本没法弄清楚自己的发展方向,自然也就不知道该怎么去努力,于是,就只能在项目中浑浑噩噩地亦步亦趋,靠着时间的堆积,慢慢积累着各种经验,一旦到了瓶颈期,就难以突破了。在软件领域,一个技术人员往往可以随着时间的推移,通过积累更多经验得到进步,但这样的进步是有限制的。根据人的禀赋与努力程度不同,或早或晚,在3年以内,一定会面临一个发展的瓶颈期。经验累计与时间增长之间的关系大概如下图所示:一旦进入瓶颈期,如果没对自己给出下一阶段的准确定位,无法确定突破方向,能力就无法随着时间推移而继续提高。如果不寻求改变,在同一岗位工作5年甚至8年,和工作3年没有什么本质的不同。相反,因为工作精力与学习能力的下降,有可能整体能力还会呈下降趋势,由此就出现了所谓“程序员的中年危机”。其实,在当今技术迅猛发展的态势下,我估计的3年瓶颈期实在太保守了,它可能会缩短到18个月,甚至一年或者半年。我曾经戏称,如果一位程序员不问世事闭关潜修半年时间,那么,他出关之时就是他的淘汰之日。这个行业就是这么卷!团队负责人必须让每一位团队成员认识到这一行优胜劣汰的残酷性,进而在他们内心形成愿意寻求改变、突破自我、持续学习的内驱力,才能带好一支技术团队。