一个技术总监的忠告:精通那么多技术,为何还是做不好一个项目?

日期: 2025-03-24 07:04:39 |浏览: 15|编号: 84265

友情提醒:信息内容由网友发布,本站并不对内容真实性负责,请自鉴内容真实性。

一个技术总监的忠告:精通那么多技术,为何还是做不好一个项目?

编写高质量可维护的代码,既是程序员的基本修养,也是决定项目成败的关键。本文试图总结项目的共性问题,并给出相应的解决方案。万字好文,推荐阅读。

1. 程序员的宿命?

程序员的职业生涯中难免遇到烂项目,有些项目是你加入时已经烂了,有些是自己从头开始亲手做成了烂项目,有些是从里到外的烂,有些是表面光鲜等你深入进去发现是个“焦油坑”,有些是此时还没烂,但是已经出现问题征兆,走在了腐烂的路上。

国内基本上是这样,国外情况我了解不多,不过从英文社区和技术媒体上老外同行的抱怨程度看,应该是差不多的。

虽然整体素质可能更高,但是也因更长久的信息化而积累了更多问题。毕竟“焦油坑、 屎山”这些舶来的术语不是无缘无故被发明出来的。

这大概就是这个行业的宿命——要么改行,要么就是与烂项目,烂代码长期相伴。

面对这宿命的阴影,有些人认命了,逐渐对这个行业失去热情。

有些不认命的伙伴,则选择与之抗争,但是地上并没有路,当年软件危机的阴云也从未真正散去,《人月神话》仍然是神话,于是人们做出了各自不同的判断和尝试:

掀桌子另起炉灶派:

很多人把项目做烂的原因归咎于项目前期的基础没打好,需求不稳定,一路打补丁。他们要么没有信心去收拾烂摊子,要么觉得这是费力不讨好,于是准备放弃掉项目,寄希望于出现一个能重头再来的机会。但是他们对于如何避免重蹈覆辙,如何做出好项目是没有把握,也没有深入思考的,只是盲目乐观地认为自己比前任更高明。

激进改革派:

他们把烂项目的原因归结于当初没有采用正确的编程语言,最新最强大的技术栈或工具。他们中一部分人也想着有机会另起炉灶,用上时下最流行最热门的技术栈( boot、、redis、nosql、、vue)。

或者即便不另起炉灶,也认为现有技术栈太过时无法容忍了(其实可能并不算过时),于是激进地引入新技术栈,鲁莽地对项目做大手术。

这种对刚刚流行还不成熟技术的盲目跟风,技术选型不慎重的情况非常普遍,今天在他们眼中落伍的技术栈,其实也不过是几年前另一批人赶超的时髦。

我不反对技术上的创新。

这里的问题是:他们对于大手术的风险和副作用也没有深入思考的,只是盲目乐观地认为新技术能带来成功。

也没人能阻止这种简历驱动的技术选型浮躁风气,毕竟花的是公司的资源,用新东西显得自己很有追求,失败了也不影响简历美化,简历上只会增加一段项目履历和几种精通技能,不会提到又做烂了一个项目,名利双收稳赚不赔。

保守改良派:

这一类人不愿轻易放弃有问题,但仍在创造效益的项目。因为他们看到了项目仍然有维护的价值,也看到了另起炉灶的难度,大手术对业务造成影响的代价,系统迁移的难度和风险等等。

因此他们尝试用温和渐进的方式逐步改善项目质量,采用一系列工程实践(主要包括重构热点代码、补自动化测试、补文档)来清理“技术债”,消除制约项目开发效率和交付质量的瓶颈。

如果把一个问题项目比作病入膏肓的病人,那么这三种做法分别相当于是放弃治疗、截肢手术、保守治疗。

2. 一个 35+ 程序员的反思

在我年轻的时候,我是属于掀桌子派和激进派的。新工程新框架大开大合,一路走来经验值技能树蹭蹭地涨,跳槽加薪好不快活。

但是近几年随着年龄增长,一方面新东西学不动了,

另一方面对经历过的项目反思得多了观念逐渐改变了。

对我触动最大的一件事是:

在 2016 年初开始从零搭建起的项目,在我 2018 年底离开的时候(仅从代码质量角度)已经让我很不满意了。只是,这一次没有任何借口了。

从技术选型到架构设计到代码规范,都是我自己做的,

最开始的半年进展非常顺利,用着我最顺手的技术和工具一路狂奔,年底前替换掉了之前采购的那个垃圾产品经理;

开发的过程,我全力以赴,用尽 13 年工作的经验值和走过的弯路、教训,使得公司只用其它同类项目 20% 的资源就把平台做起来了。

如果说“多快好省”是最高境界,那么当时的我算是做到了多、快、省——交付的功能非常丰富且贴近业务需求、开发节奏快速、对公司开发资源很节省。

但是现在看来,“好”就远远没有达到了,到了项目中期,简单优先级高的需求都已经做完了,

公司业务上出现了新的挑战——接入另一个核心系统以及外部平台,真正的考验来了。

那个改造工程影响面比较大,需要对我们的系统做大面积修改,最困难的是:这次改造将意味着从一个简单的单体系统,变成了一个分布式的系统,而且业务涉及资金交易,可靠性要求较高,难如登天。

我之前搭建的架构的优点——简单直接,这个时候,不再是优点了。具体的表现就是:架构和代码层面的结构都快速地变得复杂、混乱起来了——熵急剧增加;

代码改起来越来越吃力、测试问题变多、生产环境故障和问题变多、于是消耗在排查测试问题生产问题和修复数据方面的精力急剧增加、出现恶性循环……

到了这个境地,项目就算是做烂了!这时候我才意识到一个非常浅显的道理:

拥有一张空白的画卷、一支最高级的画笔、一间专业的画室,无法保证你可以画出美丽的画卷。如果你不善于画画,那么一切都是空想和意淫。

然后我变成了一个“保守改良派”,因为我意识到掀桌子和激进的改革都是不负责任的。

即便掀了桌子另起炉灶了,你是需要找到一种办法,把这个新的炉灶烧好。

人到了 35 岁之后,都开始喜欢回顾和总结,都开始变得比过去更在乎项目、产品乃至公司的商业成败。

软件开发作为一种商业活动,判断其成败的依据应该是:

能否以可接受的成本、可预期的时间节奏、稳定的质量水平、持续交付满足业务需要的功能。

其实就是项目管理四要素——成本、进度、范围、质量,传统项目管理理论认为这四要素彼此制约难以兼得,项目管理的艺术在于四要素的平衡取舍。

关于软件工程和项目管理的理论和著作已经很多很成熟,这里我从程序员的视角提出一个新的观点——质量不可妥协:

质量要素是一个不可以被牺牲和妥协的要素,在保持一个质量水平的前提下,成本、进度、范围三要素是互相的制约关系——典型的比如牺牲成本(加班加点)来加快进度交付急需的功能。

好消息是,只要把质量提上去,项目就会逐渐走上健康的轨道,其它三个方面也都会改善。

坏消息是,项目的质量很容易失控,现实中质量不佳、越做越臃肿混乱的项目比比皆是。

当然任何事情都有一个度的问题,当质量低于某个水平时才会导致其它三要素同时受损。反之当质量高到某个水平以后,继续追求质量不仅得不到明显收益,而且也会损害其它三要素——边际效用递减定律。

这个度需要你为自己去评估和测量,如果目前的质量水平还在两者之间,那么就应该重点改进项目质量。

3. 项目走向衰败的最常见诱因——代码质量不佳

一个项目的衰败如同一个人健康状况的恶化。

当然可能有多种多样的原因——比如需求失控、业务调整、人员变动流失。但是作为我们技术人,如果能做好自己分内的工作——编写出可维护的代码、减少技术债利息成本、交付一个健壮灵活的应用架构,就足以自傲了。

在我十多年职业生涯中,经历过以及近距离观察的几十个项目,确实看到了大量的项目正是由于代码质量不佳导致的失败和遗憾。

比如一个常见的项目腐烂恶性循环:代码乱》bug 多》排查问题耗时》复用度低》加班 996》士气低落……

所谓“千里之堤,毁于蚁穴”,代码问题就是蚁穴。

接下来,我们从项目管理聚焦到项目代码质量这个相对小的领域来深入剖析。编写高质量可维护的代码是程序员的基本修养。

本文试图在代码层面找到一些失败项目中普遍存在的症结问题,同时基于个人十几年开发经验总结出的一些设计模式作为药方分享出来。

关于代码质量的话题,其实很难通过一篇文章阐述明白,甚至需要一本书的篇幅,里面涉及到的很多概念关注点之间存在复杂微妙关系。

4. 一个失败项目复盘

先看几段代码,了解这个重病缠身的项目的病灶和症状:

这是该项目中一个最核心、最复杂也是最经常要被改动的 class,代码行数 4881;结果就是冗长的 API 列表(列表需要滚动 4 屏才能到底,公有私有 API 180 个);

还是那个 Class,头部的 延绵到了 139 行,去掉第一行 声明和少量空行总共 引入了 130 个 class!

还是那个坑爹的组件,从 156 行开始到 235 行声明了 依赖注入的组件 40 个!

这里先不去分析这个类的问题,只是初步展示一下病情严重程度。我相信这应该不算是特别糟糕的情况,比这个严重的项目俯拾皆是。

4.1 症结 1:组件粒度过大、API 泛滥

分层的理念早已深入人心,尤其是业务逻辑层的独立,彻底杜绝了不分层的年代的业务逻辑与展现逻辑、持久化逻辑等混杂的问题。

但是好景不长,随着业务的复杂和变更,在业务逻辑层的复杂性也急剧增加,成为了新的开发效率瓶颈,问题就出在了业务逻辑组件的划分方式——按领域模型划分业务逻辑组件:

业界关于如何设计业务逻辑层 并没有标准和最佳实践,绝大多数项目中都是想当然地按照业务领域对象来设计;

例如:领域实体对象有 、Order、、。于是业务逻辑层就设计出 、、、,事实上项目简单时你随便怎么设计都问题不大。但是当项目变大很复杂以后,就会出现问题了:

组件臃肿

组件的个数跟领域实体对象个数基本相当,必然造成个别 组件变得非常臃肿——API 非常多,代码行数达到几千行;

职责模糊

业务逻辑往往跨多个领域实体,无论放在哪个 都不合适,同样的,要找一个功能的实现逻辑也无法确定在哪个 中;

代码重复 or 逻辑纠缠的两难选择

当遇到一个业务逻辑,其中的某个环节在另一个业务逻辑 API 中已经实现,这时如果不想忍受重复实现和代码,就只能去调用那个 API。但这样就造成了业务逻辑组件之间的耦合与依赖,这种耦合与依赖很快会扩散——新的 API 又会被其它业务逻辑依赖,最终形成蜘蛛网一样的复杂依赖甚至循环依赖;复用代码、减少重复虽然是好的,但是复杂耦合依赖的害处也很大——赶走一只狼引来了一只虎。两杯毒酒给你选!

组件 就是一个典型案例,这样的组件往往是热点代码以及整个项目的开发效率的瓶颈。

4.2 药方 1:倒金字塔结构——业务逻辑组件职责单一、禁止层内依赖

问题根源的反面其实就藏着解决方案,只是需要我们有意识的去改变习惯、遵循新的设计风格,而不是凭直觉去设计:

业务逻辑层应该被设计成一个个功能非常单一的小组件,所谓小是指 API 数量少、代码行数少;由于职责单一,因此必然组件数量多,每一个组件对应一个很具体的业务功能点;复用(调用、依赖)只应该发生在相邻的两层之间——上层调用下层的 API 来实现对下层功能的复用;于是系统架构就自然呈现出倒立的金字塔形状:越接近顶层的业务场景组件数量越多,越往下层的复用性高,于是组件数量越少。

4.3 症结 2:低内聚、高耦合

经典面向对象理论告诉我们,好的代码结构应该是“高内聚、低耦合”的。

高内聚:组件本身应该尽可能地包含其所实现功能的所有重要信息和细节,以便让维护者无需跳转到其它多个地方去了解必要的知识。

低耦合:组件之间的互相依赖和了解尽可能少,以便在一个组件需要改动时其它组件不受影响。

其实这两者就是一体两面,做到了高内聚基本也就做到了低耦合,相反如果内聚度很低,势必存在大量高耦合的组件。

很多项目都存在低内聚、高耦合的问题。根本原因在于很多程序员,甚至是很多经验丰富的程序员也缺少这方面的意识——对“内聚性”概念不甚清楚,对内聚性被破坏的危害没有意识,对如何避免更是无从谈起。

很多人从一开始就凭直觉写程序,有了一定经验以后一般能认识到重复代码的危害,对复用性有很强的认识,于是就会掉进一个陷阱——盲目追求复用,结果破坏了内聚性。

不过业界关于“复用性”的认识存在一个误区——认为包括业务逻辑组件在内的任何层面的组件都应该追求最大限度的可复用性;

复用当然是好的,但应该有个前提条件:不增加系统复杂度的情况下的复用,一个业务逻辑 API 被另一个业务逻辑 API 复用——就是不好的。因为这样做,损害了稳定性,增加了复杂性,还会造成代码可读性降低

4.4 药方 2:复用的两种正确姿势——打造自己的 lib 和

软件架构中有两种东西来实现复用——lib 和 ,

lib 库是供你(应用程序)调用的,它帮你实现特定的能力(比如日志、数据库驱动、json 序列化、日期计算、http 请求)。 框架是供你扩展的,它本身就是半个应用程序,定义好了组件划分和交互机制,我们需要按照其规则扩展出特定的实现并绑定集成到其中,来完成一个应用程序。

lib 就是组合方式的复用, 则是继承式的复用,继承的 Java 关键字是 ,所以本质上是扩展。

过去有个说法:“组合优于继承,能用组合解决的问题尽量不要继承”。

我不同意这个说法,这容易误导初学者以为组合优于继承,其实继承才是面向对象最强大的地方,当然任何东西都不能乱用。

典型的继承误用就是为了获得父类的某个 API 而去继承,继承一定是为了扩展,而不是为了直接获得一个能力,获得能力应该调用 lib,父类不应该去实现具体功能,那是 lib 该做的事。

也不应该为了使用 lib 而去继承 lib 中的 Class。lib 就是用来被组合被调用的, 就是用来被继承、扩展的。

相对于过去,现在我们已经有了足够多的第三方 lib 和 来复用,来帮助项目节省大量代码,开发工作似乎变成了索然无味、没技术含量的 CRUD。但是对于业务非常复杂的项目,则需要有经验、有抽象思维、懂设计模式的人,去设计面向业务的 和面向业务的 lib,只有这样才能交付可维护、可扩展、可复用的软件架构——高质量架构,帮助项目或产品取得成功。

4.5 症结 3:抽象不够、逻辑纠缠——High Level 业务逻辑和 Low Level 实现逻辑纠缠

当我们说“代码中包含的业务逻辑”的时候,我们到底在说什么?业界并没有一个标准,大家经常讲的 CRUD 增删改查其实属于更底层的数据访问逻辑。

我的观点是:所谓代码中的业务逻辑,是指这段代码所表现出的所有输入输出规则、算法和行为,通常可以分为以下 5 类:

输入合法性校验

业务规则校验

典型的如检查交易记录状态、金额、时限、权限等,通常包含数据库或外部接口的查询作为参考;

数据持久化行为

数据库、缓存、文件、日志等任何形式的数据写入行为;

外部接口调用行为

输出 / 返回值准备

单纯地这样看,你可能觉得并不是特别复杂,但是现实中上述 5 类业务逻辑中的每一个通常还包含着一到多个底层实现逻辑,如 CRUD 数据访问逻辑或第三方 API 的调用。

例如输入合法性校验,通常需要查询对应记录是否存在,外部接口调用前通常需要查询相关记录以获得调用接口需要的参数,调用接口后还需要根据结果更新相关记录状态。

显然这里存在两个 Level 的逻辑——High Level 的与业务需求对应且关联紧密的逻辑、Low Level 的实现逻辑。

如果对两个 Level 的逻辑不加以区分、混为一谈,代码质量立刻就会遭到严重损害:

可读性变差

两个维度的复杂性——业务复杂性和底层实现的技术复杂性——被掺杂在了一起,复杂度 1+1>2 剧增,给其他人阅读代码增加很大负担;

可维护性差

可维护性通常指排查和解决问题所需花费的代价高低,当两个 level 的逻辑纠缠在一起,会使排查问题变得更困难,修复问题时也更容易出错;

可扩展性无从谈起

扩展性通常指为系统增加一个特性所需花费的代价高低,代价越高扩展性越差;与排查修复问题类似,逻辑纠缠显然也会使添加新特性变得困难、一不小心就破坏了已有功能。

下面这段代码就是一个典型案例——High Level 的逻辑流程(参数获取、反序列化、参数校验、缓存写入、数据库持久化、更新相关交易记录)完全淹没在了 Low Level 的实现逻辑(字符串比较、Json 反序列化、redis 操作、dao 操作以及前后各种琐碎的参数准备和返回值处理)。

@Override
public 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);
}
}

4.6 药方 3:控制逻辑分离——业务模板 of te

解决“逻辑纠缠”最关键是要找到一种隔离机制,把两个 Level 的逻辑分开。当我们着手维护一段代码时,一定是想先弄清楚它的整体流程、算法和行为,而不是一上来就去研究它的细枝末节;控制逻辑分离后,只需要去看 High Level 部分就能了解到上述内容,阅读代码的负担大幅度降低,代码可读性显著增强;

读懂代码是后续一切维护、重构工作的前提,而且一份代码被读的次数远远高于被修改的次数(高一个数量级),可读性增强可以大幅度提高系统可维护性,也是重构的最主要目标。

同时,根据我的经验,High Level 业务逻辑的变更往往比 Low Level 实现逻辑变更要来的频繁,毕竟前者跟业务直接对应。在这样的背景下,控制逻辑分离的好处就更明显了。每次维护、扩充系统功能只需改动一个 Levle 的代码,另一个 Level 不受影响或影响很小,这会大幅降低修改成本和风险。

我在总结过去多个项目中的教训和经验后,总结出了一项最佳实践或者说是设计模式——业务模板 of t,可以非常简单、有效的分离两类逻辑,先看代码:

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() {
@Override
protected 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);
}
}
}
}

@Override
protected 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);
}
@Override
protected 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);
}
@Override
protected 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);
}

我这个方案没有提出和创造任何新东西,我只是在实践中,偶然发现 设计模式真的非常适合解决广泛存在的逻辑纠缠问题,而且也发现很少有程序员能主动运用这个设计模式;

一部分原因可能是意识到“逻辑纠缠”问题的人本就不多,同时熟悉这个设计模式并能自如运用的人也不算多,两者的交集自然就是少得可怜;

下面介绍一下 设计模式的运用。High Level 逻辑封装在抽象父类 的一个 final 中,形成一个业务逻辑的模板;final 保证了其中逻辑不会被子类有意或无意的篡改破坏,因此其中封装的一定是业务逻辑中那些相对固定不变的东西。至于那些可变的部分以及暂时不确定的部分,以 形式预留扩展点;

子类(一个匿名内部类)像“做填空题”一样,填充模板实现 Low Level 逻辑——实现那些 扩展点;由于扩展点在父类中是 的,因此编译器会提醒子类的程序员该扩展什么。

那么它是如何避免上面两个方案的 4 个局限性的:

首先,Low Level 需要修改或替换时,只需从父类扩展出一个新的子类,父类全然不知无需任何改动;

其次,无论是父类还是子类,其中的 对外层的 组件都是不可见的,即便是父类中的 也不可见,因为只有持有类的实例对象才能访问到其中的 ;

最后,无论是父类还是子类,它们都是作为 的内部类存在的,不会增加新的 java 类文件更不会增加大量无意义的 API(API 只有在被项目内复用或发布出去供外部使用才有意义,只有唯一的调用者的 API 是没有必要的);

组件依赖失控的问题当然也就不存在了。

等框架型的开源项目中,其实早已大量使用 设计模式,这本该给我们这些应用开发程序员带来启发和示范,但是很可惜业界没有注意到和充分发挥它的价值。

4.7 症结 4:无处不在的 if else 牛皮癣

无论你的编程启蒙语言是什么,最早学会的逻辑控制语句一定是 if else,但是不幸的是,它在你开始真正的编程工作以后,会变成一个损害项目质量的坏习惯。

几乎所有的项目都存在 if else 泛滥的问题,但是却没有引起足够重视警惕,甚至被很多程序员认为是正常现象。

为什么 if else 这个看上去人畜无害的东西是有害的、是需要严格管控的:

if else if ...else 以及类似的 控制语句,本质上是一种 hard 硬编码行为,如果你同意“magic 魔法数字”是一种错误的编程习惯,那么同理,if else 也是错误的 hard 编程风格;

hard 的问题在于当需求发生改变时,需要到处去修改,很容易遗漏和出错;

以一段代码为例来具体分析:

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);
}
}
}

if ("3".(.("type"))), 显然这里的"3"是一个 magic ,没人知道 3 是什么含义,只能推测,但是仅仅将“3”重构成常量 并不会改善多少,因为 if (.(.("type"))) 仍然是面向过程的编程风格,无法扩展。到处被引用的常量 并没有比到处被 hard 的 magic 好多少,只不过有了含义而已,把常量升级成 Enum 枚举类型呢,也没有好多少,当需要判断的类型增加了或判断的规则改变了,还是需要到处修改—— (霰弹式修改)

并非所有的 if else 都有害,比如上面示例中的 if (list1 !=null) { 就是无害的,没有必要去消除,也没有消除它的可行性。

4.8 药方 4:充血枚举类型——Rich Enum Type

正如前面分析呈现的那样,对于代码中广泛存在的状态、类型 if 条件判断,仅仅把被比较的值重构成常量或 enum 枚举类型并没有太大改善——使用者仍然直接依赖具体的枚举值或常量,而不是依赖一个抽象。

于是解决方案就自然浮出水面了:在 enum 枚举类型基础上进一步抽象封装,得到一个所谓的“充血”的枚举类型,代码说话:

实现多种系统通知机制,传统做法:

enum NOTIFY_TYPE {    email,sms,wechat;  }  // 先定义一个 enum——一个只定义了值不包含任何行为的“贫血”的枚举类型
if(type==NOTIFY_TYPE.email){ //if 判断类型 调用不同通知机制的实现
。。。
}else if (type=NOTIFY_TYPE.sms){
。。。
}else{
。。。
}

实现多种系统通知方式,充血枚举类型——Rich Enum Type 模式:

enum NOTIFY_TYPE {    //1、定义一个包含通知实现机制的“充血”的枚举类型
email("邮件",NotifyMechanismInterface.byEmail()),
sms("短信",NotifyMechanismInterface.bySms()),
wechat("微信",NotifyMechanismInterface.byWechat());

String memo;
NotifyMechanismInterface notifyMechanism;

private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//2、私有构造函数,用于初始化枚举值
this.memo=memo;
this.notifyMechanism=notifyMechanism;
}
//getters ...
}
public interface NotifyMechanismInterface{ //3、定义通知机制的接口或抽象父类
public boolean doNotify(String msg);

public static NotifyMechanismInterface byEmail(){//3.1 返回一个定义了邮件通知机制的策的实现——一个匿名内部类实例
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
public static NotifyMechanismInterface bySms(){//3.2 定义短信通知机制的实现策略
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
public static NotifyMechanismInterface byWechat(){//3.3 定义微信通知机制的实现策略
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
}
//4、使用场景
NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);

充血枚举类型——Rich Enum Type 模式的优势:

不难发现,这其实就是 enum 枚举类型和 策略模式的巧妙结合运用;

当需要增加新的通知方式时,只需在枚举类 增加一个值,同时在策略接口 face 中增加一个 by 方法返回对应的策略实现;

当需要修改某个通知机制的实现细节,只需修改 face 中对应的策略实现;无论新增还是修改通知机制,调用方完全不受影响,仍然是 .(type).().(msg);

与传统 策略模式的比较优势:常见的策略模式也能消灭 if else 判断,但是实现起来比较麻烦,需要开发更多的 class 和代码量:

每个策略实现需单独定义成一个 class;还需要一个 类来做初始化——用 Map 把类型与对应的策略实现做映射;使用时从 获取具体的策略;

Rich Enum Type 的进一步的充血:

上面的例子中的枚举类型包含了行为,因此已经算作充血模型了,但是还可以为其进一步充血;

例如有些场景下,只是要对枚举值做个简单的计算获得某种 flag 标记,那就没必要把计算逻辑抽象成 face 那样的接口,杀鸡用了牛刀;这时就可以在枚举类型中增加 封装简单的计算逻辑。

策略实现的进一步抽象:

当各个策略实现( bySms )存在共性部分、重复逻辑时,可以将其抽取成一个抽象父类;然后就像前一章节——业务模板 of te 那样,在各个子类之间实现优雅的逻辑分离和复用。

5. 为你的项目编制一套代码库目录 / 索引——CODEX

以上就是我总结出的最常见也最影响代码质量的 4 个问题及其解决方案:

职责单一、小颗粒度、高内聚、低耦合的业务逻辑层组件——倒金字塔结构;打造项目自身的 lib 层和 ——正确的复用姿势;业务模板 of te——控制逻辑分离;充血的枚举类型 Rich Enum Type——消灭硬编码风格的 if else 条件判断;

接下来就是如何动手去针对这 4 个方面进行重构了,但是事情还没有那么简单。

上面所有的内容虽然来自实践经验,但是要应用到你的具体项目,还需要一个步骤——火力侦察——弄清楚你要重构的那个模块的逻辑脉络、算法以致实现细节,否则贸然动手,很容易遗漏关键细节造成风险,重构的效率更难以保证,陷入进退两难的尴尬境地。

我 2019 年一整年经历了 3 个代码十分混乱的项目,最大的收获就是摸索出了一个梳理烂代码的最佳实践——CODEX:

在阅读代码过程中,在关键位置添加结构化的注释,形如://CODEX 1 体检预约流程 1 预约服务 API 入口

所谓结构化注释,就是在注释内容中通过规范命名的编号前缀、分隔符等来体现出其所对应的项目、模块、流程步骤等信息,类似文本编辑中的标题 1、2、3;

然后设置 IDE 工具识别这种特殊的注释,以便结构化的显示。 的 Tasks 显示效果类似下图;

这个结构化视图,本质上相对于是代码库的索引、目录,不同于 文档,CODEX 具有更清晰的逻辑层次和更强的代码查找便利性,在 Tasks 中点击就能跳转到对应的代码行;

这些结构化注释随着代码一起提交后就实现了团队共享;

这样的一份精确无误、共享的、活的源代码索引,无疑会对整个团队的开发维护工作产生巨大助力;

进一步的,如果在 CODEX 中添加 关键字,甚至可以将导出的 CODEX 简单加工后,变成一张业务逻辑的 序列图,如下所示。

6. 不要辜负这个程序员最好的时代

这是程序员最好的时代,互联网浪潮已经席卷了世界每个角落。过去只有软件公司、互联网公司和银行业会雇佣程序员,随着云计算的普及、产业互联网和互联网 + 兴起,已经有越来越多的传统企业开始雇佣程序员搭建 IT 系统来支撑业务运营。

资本的推动 IT 需求的旺盛,使得程序员成了稀缺人才,各大招聘平台上,程序员的岗位数量和薪资水平长期名列前茅。

但是我们这个群体的整体表现怎么样呢,扪心自问,我觉得很难令人满意,我所经历过的以及近距离观察到的项目,鲜有能够称得上成功的。这里的成功不是商业上的成功,仅限于作为一个软件项目和工程是否能够以可接受的成本和质量长期稳定的交付。

商业的短期成功与否,很多时候与项目工程的成功与否没有必然联系,一个商业上很成功的项目可能在工程上做的并不好,只是通过巨量的资金资源投入换来的暂时成功而已。

归根结底,我们程序员群体需要为自己的声誉负责,长期来看也终究会为自己的声誉获益或受损。

我认为程序员最大的声誉、最重要的职业素养,就是通过写出高质量的代码做好一个个项目、产品,来帮助团队,组织创造价值。

提醒:请联系我时一定说明是从旅游网上看到的!