当前位置: 首页 > 科技观察

CodeClinic初诊

时间:2023-03-12 02:33:22 科技观察

几年前,我有机会领导一个咨询项目。团队很小,目标是用Java重写旧系统的后台,团队的开发人员都是C程序员。我的工作职责是负责项目设计和开发,并在项目开发过程中担任敏捷教练,培训Java开发人员。C程序员的特点之一就是基本没有面向对象的知识。初步掌握Java语法后,写出的代码仍然是过程式代码。团队开发人员的现状是没有CleanCode的意识,不知道什么是TDD和重构,写出来的Java代码质量很差。如果不从项目一开始就有效预防这个问题,可能会导致整个代码库陷入泥潭。为此,我要求在每日站会后及时开展代码审查活动。在review的过程中,我只能带头帮助大家发现代码的臭味。从一开始,难闻的代码就无处不在,就像我们每天呼吸的被污染的空气一样不可避免。净化空气任重道远,让团队成员写出好的代码也任重道远。必须有处方。于是我成了一名治疗代码病的医生。为了更方便地传播医学知识,我在团队工作室的一角开了一家小诊所,并大肆宣传——“一日一贴,百病通”。刚开的时候,诊所的门面还没有装修,我就找了一块白板写了一个方子:我个人觉得这些方子不仅对当时的客户团队有效,也适合大多数人开发团队。几年过去了,把这些药方分享出来,也算是一个小小的总结。我们先来看看这些药方。第1条:结构应始终保持清晰和简单:将所有查询统一为Repository。其实项目不需要访问数据库,而是通过远程Telnet(或其他协议)访问前端设备。但是,我们可以从DDD中的存储库中借用这个比喻。至于提到的架构,我在设计架构的时候参考了DDD的分层逻辑架构。为了保证架构的简单明了,我做了一些“一刀切”的简化原则:比如Repository和Service的定义。通过Telnet等网络协议获取设备信息的功能可以看成是对DB的查询。所以NodeConfigureGetter等类应该统一命名为NodeConfigureRepository。这个处方的主要目的是保持代码的一致性。如果不规范,就会出现Getter、Finder、Query等各种不统一的类名后缀,令人眼花缭乱。第二条:依赖注入(对象间的协作)许多OO初学者并不了解依赖注入。我的方法之一是让它们从可测试性的角度出发。比如在NodeConfigureRepository类中直接实例化TelnetService(该类提供连接、登录、命令执行等与Telnet相关的操作),如何在不需要Telnet环境的情况下为NodeConfigureRepository编写单元测试?Can'tsolveit出现这样的问题说明设计的可测试性不够好。解决方案是依赖注入。当时的项目没有引入第三方IoC容器,因为项目的Jar包需要配合另外一个系统驻留在Flash中。容量有限,不能引入过多的第三方包,保证Jar包的紧凑性。第三条:方法名体现意图。这个问题是很多开发者容易犯的错误,尤其是面向过程设计的程序员,很少从对象的角度去思考方法(也就是行为,准确的说,从设计的角度应该是对象的职责)).比如在NodeConfigureRepository类中,开发者定义了getNodeConfigure方法,但是返回值为void:getEnvId();getMasterSlot();getSlaveIp();getSlaveBoardTypeAndStatus();}}该方法调用的很多私有方法实际上是从构造函数传入的NodeConfigure中采集数据。这样的定义不仅让代码的调用者感觉怪怪的,也让测试变得异常怪异:getMasterLogicBoard(),is(12288));}怎么改?方法是让getNodeConfigure()方法直接返回组装好的NodeConfigure对象,去掉NodeConfigure和NodeConfigureRepository之间的生命周期依赖。有趣的是在getNodeConfigure方法内部调用的私有方法。它在设计上成为一个例外,因为在Java中通常需要避免直接修改输入参数并将其作为结果返回。这里出现的一系列方法实际上是对NodeConfigure对象进行数据收集,所以可以定义为:privatevoidcollectMasterLogicBoard(NodeConfigureconfigure){}privatevoidcollectMasterIp(NodeConfigureconfigure){}privatevoidcollectEnvId(NodeConfigureconfigure){}privatevoidcollectMasterSlot(NodeConfigureconfigure){}privatevoidIpcollect(NodeConfigureconfigure){}privatevoidcollectSlaveBoardTypeAndStatus(NodeConfigureconfigure){}我更改了这些方法的名称以更好地反映它们的意图。因此,getNodeConfigure()变为:这里其实就是KentBeck提出的CollectedParameter模式。它是访客模式的简化设计。当然,我们也可以使用Builder模式来组装NodeConfigure对象。第四条:同一方法中的实现代码应处于同一抽象层次。这实际上是陈词滥调。KentBeck在SmalltalkBestPracticePatterns一书中提到了“组合方法”模式,提出“让一个方法中的所有操作都在同一个抽象层”,即所谓的SLAP原则。这个原则在RobertMartin的CleanCode一书中也被反复提及,NealFord在EmergentDesign中也有详细描述。规则5:避免“哑对象”这里显示的难闻气味在MartinFowler的Refactoring一书中提到过。在项目中,有一些操作Xml文件的操作,将这些Xml文件的Element映射为Java对象。我们没有使用Jaxb,因为它对于我们有限的xml操作来说仍然太重了。但是,在我们的代码中,包括PackageStatusFileParser、StoragePackageGenerator、DownloadingConfigureParser等类,都有重复的代码将XmlElement转化为PackageInfo、SoftInfo等对象。原因是我们把这些对象看成是“哑巴”数据对象,而没有把这种转换行为封装到拥有这些数据的对象中(我们的转换只涉及Xml,没有扩展的可能,所以没有必要使用访客模式)。除了造成大量重复代码外,一旦转换逻辑发生变化,比如给XmlElement添加Attribute,可能需要处处修改,形成所谓的“鸟枪修改”。因此,需要将这些逻辑封装成对象,例如:publicclassPackageInfo{publicPackageInfocreateFrom(Elmentelement){}}PackageInfopackageInfo=PackageInfo.createFrom(element);代码虽然详细,看似琐碎,但在项目之初没有规范和约束,任由腐败蔓延,最终的苦果还是会波及整个系统;当代码质量下降到无法修复的程度时,再恢复可能就来不及了。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文