编写高质量可维护的代码既是开发人员的基本修养,也是能决定项目成败的关键因素,本文试图总结出问题项目普遍存在的共性问题并给出相应的解决方案。
01
遇到烂项目,你会怎么做?
▿
我们的职业生涯中难免遇到烂项目,有些项目是你加入时已经烂了,有些是自己从头开始亲手做成了烂项目,有些是从里到外的烂,有些是表面光鲜等你深入进去发现是个“焦油坑”,有些是此时还没烂但是已经出现问题征兆走在了腐烂的路上。
国内基本上是这样,国外情况我了解不多,不过从英文社区和技术媒体上老外同行的抱怨程度看,应该是差不多的,虽然整体素质可能更高,但是也因更久的信息化而积累了更多问题。毕竟“焦油坑、Shit_Mountain 屎山”这些舶来的术语不是无缘无故被发明出来的。
Any way,这大概就是我们这个行业的宿命——要么改行,要么就是与烂项目烂代码长相伴。就像宇宙的“熵增加定律”一样:
孤立系统的一切自发过程均向着令其状态更无序的方向发展,如果要使系统恢复到原先的有序状态是不可能的,除非外界对它做功。
面对这宿命的阴影,有些人认命了麻木了,逐渐对这个行业失去热情。
那些不认命的选择与之抗争,但是地上并没有路,当年软件危机的阴云也从未真正散去,人月神话仍然是神话,于是人们做出了各自不同的判断和尝试:
如果把一个问题项目比作病入膏肓的病人,那么这三种做法分别相当于是放弃治疗、截肢手术、保守治疗。
02
为什么说“质量不可妥协”?
▿
年轻时候我也是掀桌子派和激进派的,新工程新框架大开大合,一路走来经验值技能树蹭蹭的涨,跳槽加薪好不快活。
但是近几年随着年龄增长,一方面新东西学不动了,另一方面对经历过的项目反思的多了观念逐渐改变了。
对我触动最大的一件事是那个我在 2016 年初开始从零搭建起的项目,在我 2018 年底离开的时候(仅从代码质量角度)已经让我很不满意了。只是,这一次没有任何借口了:
于是我意识到一个非常浅显的道理:拥有一张空白的画卷、一支最高级的画笔、一间专业的画室,无法保证你可以画出美丽的画卷。如果你不善于画画,那么一切都是空想和意淫。
然后我变成了一个“保守改良派”,因为我意识到掀桌子和激进的改革都是不负责任的,说不好听的那样其实是掩耳盗铃、逃避困难,人不可能逃避一辈子,你总要面对。
即便掀了桌子另起炉灶了,你还是需要找到一种办法把这个新的炉灶烧好,因为随着项目发展之前的老问题还是会一个一个冒出来,还是需要面对现实、不逃避、找办法。
面对问题不仅有助于你把当前项目做好,也同样有助于将来有新的项目时更好的把握住机会。
无论是职业生涯还是自然年龄,人到了这个阶段都开始喜欢回顾和总结,也变得比过去更在乎项目、产品乃至公司的商业成败。
软件开发作为一种商业活动,判断其成败的依据应该是:能否以可接受的成本、可预期的时间节奏、稳定的质量水平、持续交付满足业务需要的功能市场需要的产品。
其实就是项目管理四要素——成本、进度、范围、质量,传统项目管理理论认为这四要素彼此制约难以兼得,项目管理的艺术在于四要素的平衡取舍。
关于软件工程和项目管理的理论和著作已经很多很成熟,这里我的观点——质量不可妥协:
03
项目走向衰败的最常见诱因是:
代码质量不佳
▿
一个项目的衰败一如一个人健康状况的恶化,当然可能有多种多样的原因——比如需求失控、业务调整、人员变动流失。但是作为我们技术人,如果能做好自己分内的工作——编写出可维护的代码、减少技术债利息成本、交付一个健壮灵活的应用架构,那也绝对是功德无量的。
虽然很难估算出这究竟能挽救多少项目,但是在我十多年职业生涯中,经历的和近距离观察的几十个项目,确实看到了大量的项目正是由于代码质量不佳导致的失败和遗憾,同时我也发现其实失败项目的很多问题、症结也确确实实都可以归因到项目代码的混乱和质量低下,比如一个常见的项目腐烂恶性循环:代码乱>Bug 多>排查问题耗时>复用度低>加班多>士气低落……
所谓“千里之堤,毁于蚁穴”,代码问题就是蚁穴。
接下来,让我们从项目管理聚焦到项目代码质量这个相对小的领域来深入剖析。编写高质量可维护的代码是程序员的基本修养,本文试图在代码层面找到一些失败项目中普遍存在的症结问题,同时基于个人十几年开发经验总结出的一些设计模式作为药方分享出来。
关于代码质量的话题其实很难通过一篇文章阐述明白,甚至需要一本书的篇幅,里面涉及到的很多概念关注点之间存在复杂微妙关系。
推荐《设计模式之美》的第二章节《从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?》,这是我看到的关于代码质量主题最精彩深刻的论述。
04
一个失败项目复盘
▿
先贴几张代码截图,看一下这个重病缠身的项目的病灶和症状:
这里先不去分析这个类的问题,只是初步展示一下病情严重程度。
我相信这应该不算是特别糟糕的情况,比这个严重的项目俯拾皆是,但是这也应该足够拿来暴露问题、剖析成因了。
分层的理念早已深入人心,尤其是业务逻辑层的独立,彻底杜绝了之前(不分层的年代)业务逻辑与展现逻辑、持久化逻辑等混杂的问题。
但是好景不长,随着业务的复杂和变更,在业务逻辑层的复杂性也急剧增加,成为了新的开发效率瓶颈, 问题就出在了业务逻辑组件的划分方式——按领域模型划分业务逻辑组件:
前面截图的那个问题组件 ContractService 就是一个典型案例,这样的组件往往是热点代码以及整个项目的开发效率的瓶颈。
问题根源的反面其实就藏着解决方案,只是需要我们有意识的去改变习惯、遵循新的设计风格,而不是凭直觉去设计:
经典面向对象理论告诉我们,好的代码结构应该是“高内聚、低耦合”的:
其实这两者就是一体两面,做到了高内聚基本也就做到了低耦合,相反如果内聚度很低,势必存在大量高耦合的组件。
我观察发现,很多项目都存在低内聚、高耦合的问题。根本原因在于很多程序员,甚至是很多经验丰富的程序员也缺少这方面的意识——对“内聚性”概念不甚清楚,对内聚性被破坏的危害没有意识,对如何避免更是无从谈起。
很多人从一开始就凭直觉写程序,有了一定经验以后一般能认识到重复代码的危害,对复用性有很强的认识,于是就会掉进一个陷阱——盲目追求复用,结果破坏了内聚性。
软件架构中有两种东西来实现复用——lib 和 framework,
当我们说“代码中包含的业务逻辑”的时候,我们到底在说什么?业界并没有一个标准,大家经常讲的 CRUD 增删改查其实属于更底层的数据访问逻辑。
我的观点是:所谓代码中的业务逻辑,是指这段代码所表现出的所有输入输出规则、算法和行为,通常可以分为以下 5 类:
当然具体到某一个组件实例,可能不会包括上述全部 5 类业务逻辑,但是也可能每一类业务逻辑存在多个。
单这样看你可能觉得并不是特别复杂,但是现实中上述 5 类业务逻辑中的每一个通常还包含着一到多个底层实现逻辑,如 CRUD 数据访问逻辑或第三方 API 的调用。
例如输入合法性校验,通常需要查询对应记录是否存在,外部接口调用前通常需要查询相关记录以获得调用接口需要的参数,调用接口后还需要根据结果更新相关记录状态。
显然这里存在两个 Level 的逻辑——High Level 的与业务需求对应且关联紧密的逻辑、Low Level 的实现逻辑。
如果对两个 Level 的逻辑不加以区分、混为一谈,代码质量立刻就会遭到严重损害:
下面这段代码就是一个典型案例——High Level 的逻辑流程(参数获取、反序列化、参数校验、缓存写入、数据库持久化、更新相关交易记录)完全淹没在了 Low Level 的实现逻辑(字符串比较、Json 反序列化、redis 操作、dao 操作以及前后各种琐碎的参数准备和返回值处理)。下一节我会针对这段问题代码给出重构方案。
@Overridepublic void updateFromMQ(String compress) { try { JSONObject object = JSON.parseObject(compress); if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){ throw new AppException("MQ返回参数异常"); } logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type")); Map map = new HashMap(); map.put("type",CrawlingTaskType.get(object.getInteger("type"))); map.put("mobile", object.getString("mobile")); List list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map); redisClientTemplate.set(object.getString("mobile") + "_" + object.getString("type"),CompressUtil.compress( object.getString("data"))); redisClientTemplate.expire(object.getString("mobile") + "_" + object.getString("type"), 2*24*60*60); //保存成功 存入redis 保存48小时 CrawlingTask crawlingTask = null; // providType:(0:新颜,1XX支付宝,2:ZZ淘宝,3:TT淘宝) if (CollectionUtils.isNotEmpty(list)){ crawlingTask = list.get(0); crawlingTask.setJsonStr(object.getString("data")); }else{ //新增 crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), object.getString("data"), object.getString("mobile"), CrawlingTaskType.get(object.getInteger("type"))); crawlingTask.setNeedUpdate(true); } baseDAO.saveOrUpdate(crawlingTask); //保存芝麻分到xyz if ("3".equals(object.getString("type"))){ String data = object.getString("data"); Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score"); Map param = new HashMap(); param.put("phoneNumber", object.getString("mobile")); List list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param); if (list1 !=null){ for (Dperson dperson:list1){ dperson.setZmScore(zmf); personBaseDaoI.saveOrUpdate(dperson); AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);//查询多租户表 身份认证、淘宝认证 为0 置为1 } } } } catch (Exception e) { logger.error("更新my MQ授权信息失败", e); throw new AppException(e.getMessage(),e); }}
解决“逻辑纠缠”最关键是要找到一种隔离机制,把两个 Level 的逻辑分开——控制逻辑分离,分离的好处很多:
我在总结过去多个项目中的教训和经验后,总结出了一项最佳实践或者说是设计模式——业务模板 Pattern of NestedBusinessTemplat,可以非常简单、有效的分离两类逻辑,先看代码:
public class XyzService {abstract class AbsUpdateFromMQ { public final void doProcess(String jsonStr) { try { JSONObject json = doParseAndValidate(jsonStr); cache2Redis(json); saveJsonStr2CrawingTask(json); updateZmScore4Dperson(json); } catch (Exception e) { logger.error("更新my MQ授权信息失败", e); throw new AppException(e.getMessage(), e); } } protected abstract void updateZmScore4Dperson(JSONObject json); protected abstract void saveJsonStr2CrawingTask(JSONObject json); protected abstract void cache2Redis(JSONObject json); protected abstract JSONObject doParseAndValidate(String json) throws AppException;}
@SuppressWarnings({ "unchecked", "rawtypes" })public void processAuthResultDataCallback(String compress) { new AbsUpdateFromMQ() {@Overrideprotected void updateZmScore4Dperson(JSONObject json) { //保存芝麻分到xyz if ("3".equals(json.getString("type"))){ String data = json.getString("data"); Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score"); Map param = new HashMap(); param.put("phoneNumber", json.getString("mobile")); List list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param); if (list1 !=null){ for (Dperson dperson:list1){ dperson.setZmScore(zmf); personBaseDaoI.saveOrUpdate(dperson); AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf); } } }}@Overrideprotected void saveJsonStr2CrawingTask(JSONObject json) { Map map = new HashMap(); map.put("type",CrawlingTaskType.get(json.getInteger("type"))); map.put("mobile", json.getString("mobile")); List list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map); CrawlingTask crawlingTask = null; // providType:(0:xx,1yy支付宝,2:zz淘宝,3:tt淘宝) if (CollectionUtils.isNotEmpty(list)){ crawlingTask = list.get(0); crawlingTask.setJsonStr(json.getString("data")); }else{ //新增 crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), json.getString("data"), json.getString("mobile"), CrawlingTaskType.get(json.getInteger("type"))); crawlingTask.setNeedUpdate(true); } baseDAO.saveOrUpdate(crawlingTask);}@Overrideprotected void cache2Redis(JSONObject json) { redisClientTemplate.set(json.getString("mobile") + "_" + json.getString("type"),CompressUtil.compress( json.getString("data"))); redisClientTemplate.expire(json.getString("mobile") + "_" + json.getString("type"), 2*24*60*60);}@Overrideprotected JSONObject doParseAndValidate(String json) throws AppException { JSONObject object = JSON.parseObject(json); if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){ throw new AppException("MQ返回参数异常"); } logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type")); return object; } }.doProcess(compress);}
如果你熟悉经典的 GOF23 种设计模式,很容易发现上面的代码示例其实就是 Template Method 设计模式的运用,没什么新鲜的。
没错,我这个方案没有提出和创造任何新东西,我只是在实践中偶然发现 Template Method 设计模式真的非常适合解决广泛存在的逻辑纠缠问题,而且也发现很少有程序员能主动运用这个设计模式;一部分原因可能是意识到“逻辑纠缠”问题的人本就不多,同时熟悉这个设计模式并能自如运用的人也不算多,两者的交集自然就是少得可怜;不管是什么原因,结果就是这个问题广泛存在成了通病。
我看到一部分对代码质量有追求的程序员 他们的解决办法是通过"结构化编程"和“模块化编程”:
下面介绍一下 Template Method 设计模式的运用,简单归纳就是:
那么它是如何避免上面两个方案的 4 个局限性的:
SpringFramework 等框架型的开源项目中,其实早已大量使用 Template Method 设计模式,这本该给我们这些应用开发程序员带来启发和示范,但是很可惜业界没有注意到和充分发挥它的价值。
NestedBusinessTemplat 模式就是对其充分和积极的应用,前面一节提到过的复用的两种正确姿势——打造自己的 lib 和 framework,其实 NestedBusinessTemplat 就是项目自身的 framework。
无论你的编程启蒙语言是什么,最早学会的逻辑控制语句一定是 if else,但是不幸的是它在你开始真正的编程工作以后,会变成一个损害项目质量的坏习惯。
几乎所有的项目都存在 if else 泛滥的问题,但是却没有引起足够重视警惕,甚至被很多程序员认为是正常现象。
首先我来解释一下为什么 if else 这个看上去人畜无害的东西是有害的、是需要严格管控的:
正如前面分析呈现的那样,对于代码中广泛存在的状态、类型 if 条件判断,仅仅把被比较的值重构成常量或 enum 枚举类型并没有太大改善——使用者仍然直接依赖具体的枚举值或常量,而不是依赖一个抽象。
于是解决方案就自然浮出水面了:在 enum 枚举类型基础上进一步抽象封装,得到一个所谓的“充血”的枚举类型,代码说话:
以上就是我总结出的最常见也最影响代码质量的 4 个问题及其解决方案:
接下来就是如何动手去针对这 4 个方面进行重构了,但是事情还没有那么简单。
上面所有的内容虽然来自实践经验,但是要应用到你的具体项目,还需要一个步骤——火力侦察——弄清楚你要重构的那个模块的逻辑脉络、算法以致实现细节,否则贸然动手,很容易遗漏关键细节造成风险,重构的效率更难以保证,陷入进退两难的尴尬境地。
我 2019 年一整年经历了 3 个代码十分混乱的项目,最大的收获就是摸索出了一个梳理烂代码的最佳实践——CODEX:
本文是我的一位技术总监好友:权哥花了半个月时间写出来的良心文章,强烈推荐给大家,文章很长很硬很有价值,大家可以收藏多看几遍。希望大家看完之后转发、点在看,好文章要让更多的人看到。
软考备考学习资料