编写高质量可维护的代码既是程序员的基本修养,也是能决定项目成败的关键因素,本文试图总结出问题项目广泛存在的共性问题并给出相应的解决方案。vue
1. 程序员的宿命?程序员的职业生涯中不免遇到烂项目,有些项目是你加入时已经烂了,有些是本身从头开始亲手作成了烂项目,有些是从里到外的烂,有些是表面光鲜等你深刻进去发现是个“焦油坑”,有些是此时还没烂可是已经出现问题征兆走在了腐烂的路上。java
国内基本上是这样,国外状况我了解很少,不过从英文社区和技术媒体上老外同行的抱怨程度看,应该是差很少的,虽然总体素质可能更高,可是也因更久的信息化而积累了更多问题。毕竟“焦油坑、Shit_Mountain 屎山”这些舶来的术语不是平白无故被发明出来的。程序员
Any way,这大概就是咱们这个行业的宿命——要么改行,要么就是与烂项目烂代码长相伴。
就像宇宙的“熵增长定律”同样:redis
孤立系统的一切自发过程均向着令其状态更无序的方向发展,若是要使系统恢复到原先的有序状态是不可能的,除非外界对它作功。算法
面对这宿命的阴影,有些人认命了麻木了,逐渐对这个行业失去热情。spring
那些不认命的选择与之抗争,可是地上并无路,当年软件危机的阴云也从未真正散去,人月神话仍然是神话,因而人们作出了各自不一样的判断和尝试:sql
若是把一个问题项目比做病入膏肓的病人,那么这三种作法分别至关因而放弃治疗、截肢手术、保守治疗。docker
2. 一个 35+ 程序员的反思年轻时候我也是掀桌子派和激进派的,新工程新框架大开大合,一路走来经验值技能树蹭蹭的涨,跳槽加薪好不快活。数据库
可是近几年随着年龄增加,一方面新东西学不动了,另外一方面对经历过的项目反思的多了观念逐渐改变了。编程
对我触动最大的一件事是那个我在 2016 年初开始从零搭建起的项目,在我 2018 年末离开的时候(仅从代码质量角度)已经让我很不满意了。只是,这一次没有任何借口了:
因而我意识到一个很是浅显的道理:拥有一张空白的画卷、一支最高级的画笔、一间专业的画室,没法保证你能够画出美丽的画卷。若是你不善于画画,那么一切都是空想和意淫。
而后我变成了一个“保守改良派”,由于我意识到掀桌子和激进的改革都是不负责任的,说很差听的那样实际上是掩耳盗铃、逃避困难,人不可能逃避一生,你总要面对。
即使掀了桌子另起炉灶了,你仍是须要找到一种办法把这个新的炉灶烧好,由于随着项目发展以前的老问题仍是会一个一个冒出来,仍是须要面对现实、不逃避、找办法。
面对问题不只有助于你把当前项目作好,也一样有助于未来有新的项目时更好的把握住机会。
不管是职业生涯仍是天然年龄,人到了这个阶段都开始喜欢回顾和总结,也变得比过去更在意项目、产品乃至公司的商业成败。
软件开发做为一种商业活动,判断其成败的依据应该是:可否以可接受的成本、可预期的时间节奏、稳定的质量水平、持续交付知足业务须要的功能市场须要的产品。
其实就是项目管理四要素——成本、进度、范围、质量,传统项目管理理论认为这四要素彼此制约难以兼得,项目管理的艺术在于四要素的平衡取舍。
关于软件工程和项目管理的理论和著做已经不少很成熟,这里我从程序员的视角提出一个新的观点——质量不可妥协:
一个项目的衰败一如一我的健康情况的恶化,固然可能有多种多样的缘由——好比需求失控、业务调整、人员变更流失。可是做为咱们技术人,若是能作好本身份内的工做——编写出可维护的代码、减小技术债利息成本、交付一个健壮灵活的应用架构,那也绝对是功德无量的。
虽然很难估算出这究竟能挽救多少项目,可是在我十多年职业生涯中,经历的和近距离观察的几十个项目,确实看到了大量的项目正是因为代码质量不佳致使的失败和遗憾,同时我也发现其实失败项目的不少问题、症结也确确实实均可以归因到项目代码的混乱和质量低下,好比一个常见的项目腐烂恶性循环:代码乱》bug 多》排查问题耗时》复用度低》加班 996》士气低落……
所谓“千里之堤,毁于蚁穴”,代码问题就是蚁穴。
接下来,让咱们从项目管理聚焦到项目代码质量这个相对小的领域来深刻剖析。编写高质量可维护的代码是程序员的基本修养,本文试图在代码层面找到一些失败项目中广泛存在的症结问题,同时基于我的十几年开发经验总结出的一些设计模式做为药方分享出来。
关于代码质量的话题其实很难经过一篇文章阐述明白,甚至须要一本书的篇幅,里面涉及到的不少概念关注点之间存在复杂微妙关系。
推荐《设计模式之美》的第二章节《从哪些维度评判代码质量的好坏?如何具有写出高质量代码的能力?》,这是我看到的关于代码质量主题最精彩深入的论述。
4. 一个失败项目复盘先贴几张代码截图,看一下这个重病缠身的项目的病灶和症状:
这里先不去分析这个类的问题,只是初步展现一下病情严重程度。
我相信这应该不算是特别糟糕的状况,比这个严重的项目俯拾皆是,可是这也应该足够拿来暴露问题、剖析成因了。
分层的理念早已深刻人心,尤为是业务逻辑层的独立,完全杜绝了以前(不分层的年代)业务逻辑与展示逻辑、持久化逻辑等混杂的问题。
可是好景不长,随着业务的复杂和变动,在业务逻辑层的复杂性也急剧增长,成为了新的开发效率瓶颈,
问题就出在了业务逻辑组件的划分方式——按领域模型划分业务逻辑组件:
前面截图的那个问题组件 ContractService 就是一个典型案例,这样的组件每每是热点代码以及整个项目的开发效率的瓶颈。
问题根源的反面其实就藏着解决方案,只是须要咱们有意识的去改变习惯、遵循新的设计风格,而不是凭直觉去设计:
经典面向对象理论告诉咱们,好的代码结构应该是“高内聚、低耦合”的:
其实这二者就是一体两面,作到了高内聚基本也就作到了低耦合,相反若是内聚度很低,势必存在大量高耦合的组件。
我观察发现,很低项目都存在低内聚、高耦合的问题。根本缘由在于不少程序员,甚至是不少经验丰富的程序员也缺乏这方面的意识——对概念不甚清楚、对危害没有认识、对如何避免更是无从谈起。
不少人从一开始就凭直觉写程序,有了必定经验之后通常能认识到重复代码的危害,对复用性有很强的认识,因而就会掉进一个陷阱——盲目追求复用,结果破坏了内聚性。
软件架构中有两种东西来实现复用——lib 和 framework,
当咱们说“代码中包含的业务逻辑”的时候,咱们到底在说什么?业界并无一个标准,你们常常讲的 CRUD 增删改查其实属于更底层的数据访问逻辑。
个人观点是:所谓代码中的业务逻辑,是指这段代码所表现出的全部输入输出规则、算法和行为,一般能够分为如下 5 类:
固然具体到某一个组件实例,可能不会包括上述所有 5 类业务逻辑,可是也可能每一类业务逻辑存在多个。
单这样看你可能以为并非特别复杂,可是现实中上述 5 类业务逻辑中的每个一般还包含着一到多个底层实现逻辑,如 CRUD 数据访问逻辑或第三方 API 的调用。
例如输入合法性校验,一般须要查询对应记录是否存在,外部接口调用前一般须要查询相关记录以得到调用接口须要的参数,调用接口后还须要根据结果更新相关记录状态。
显然这里存在两个 Level 的逻辑——High Level 的与业务需求对应且关联紧密的逻辑、Low Level 的实现逻辑。
若是对两个 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<CrawlingTask> 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<Dperson> 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() { @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<Dperson> 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<CrawlingTask> 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); }
若是你熟悉经典的 GOF23 种设计模式,很容易发现上面的代码示例其实就是 Template Method 设计模式的运用,没什么新鲜的。
没错,我这个方案没有提出和创造任何新东西,我只是在实践中偶然发现 Template Method 设计模式真的很是适合解决普遍存在的逻辑纠缠问题,并且也发现不多有程序员能主动运用这个设计模式;
一部分缘由多是意识到“逻辑纠缠”问题的人本就很少,同时熟悉这个设计模式并能自如运用的人也不算多,二者的交集天然就是少得可怜;无论是什么缘由,结果就是这个问题普遍存在成了通病。
我看到一部分对代码质量有追求的程序员 他们的解决办法是经过"结构化编程"和“模块化编程”:
下面介绍一下 Template Method 设计模式的运用,简单概括就是:
那么它是如何避免上面两个方案的 4 个局限性的:
SpringFramework 等框架型的开源项目中,其实早已大量使用 Template Method 设计模式,这本该给咱们这些应用开发程序员带来启发和示范,可是很惋惜业界没有注意到和充分发挥它的价值。
NestedBusinessTemplat 模式就是对其充分和积极的应用,前面一节提到过的复用的两种正确姿式——打造本身的 lib 和 framework,其实 NestedBusinessTemplat 就是项目自身的 framework。
不管你的编程启蒙语言是什么,最先学会的逻辑控制语句必定是 if else,可是不幸的是它在你开始真正的编程工做之后,会变成一个损害项目质量的坏习惯。
几乎全部的项目都存在 if else 泛滥的问题,可是却没有引发足够重视警戒,甚至被不少程序员认为是正常现象。
首先我来解释一下为何 if else 这个看上去人畜无害的东西是有害的、是须要严格管控的:
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<Dperson> 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 条件判断,仅仅把被比较的值重构成常量或 enum 枚举类型并无太大改善——使用者仍然直接依赖具体的枚举值或常量,而不是依赖一个抽象。
因而解决方案就天然浮出水面了:在 enum 枚举类型基础上进一步抽象封装,获得一个所谓的“充血”的枚举类型,代码说话:
enum NOTIFY_TYPE { email,sms,wechat; } //先定义一个enum——一个只定义了值不包含任何行为的“贫血”的枚举类型 if(type==NOTIFY_TYPE.email){ //if判断类型 调用不一样通知机制的实现 。。。 }else if (type=NOTIFY_TYPE.sms){ 。。。 }else{ 。。。 }
enum NOTIFY_TYPE { //一、定义一个包含通知实现机制的“充血”的枚举类型 email("邮件",NotifyMechanismInterface.byEmail()), sms("短信",NotifyMechanismInterface.bySms()), wechat("微信",NotifyMechanismInterface.byWechat()); String memo; NotifyMechanismInterface notifyMechanism; private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//二、私有构造函数,用于初始化枚举值 this.memo=memo; this.notifyMechanism=notifyMechanism; } //getters ... } public interface NotifyMechanismInterface{ //三、定义通知机制的接口或抽象父类 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){ ....... } }; } } //四、使用场景 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
以上就是我总结出的最多见也最影响代码质量的 4 个问题及其解决方案:
接下来就是如何动手去针对这 4 个方面进行重构了,可是事情尚未那么简单。
上面全部的内容虽然来自实践经验,可是要应用到你的具体项目,还须要一个步骤——火力侦察——弄清楚你要重构的那个模块的逻辑脉络、算法以至实现细节,不然贸然动手,很容易遗漏关键细节形成风险,重构的效率更难以保证,陷入进退两难的尴尬境地。
我 2019 年一全年经历了 3 个代码十分混乱的项目,最大的收获就是摸索出了一个梳理烂代码的最佳实践——CODEX:
毫无疑问这是程序员最好的时代,互联网浪潮已经席卷了世界每一个角落,各行各业正在愈来愈多的依赖 IT。过去只有软件公司、互联网公司和银行业会雇佣程序员,随着云计算的普及、产业互联网和互联网+兴起,已经有愈来愈多的传统企业开始雇佣程序员搭建 IT 系统来支撑业务运营。
资本的推进 IT 需求的旺盛,使得程序员成了稀缺人才,各大招聘平台上,程序员的岗位数量和薪资水平长期名列前茅。
可是咱们这个群体的总体表现怎么样呢,扪心自问,我以为很难使人满意,我所经历过的以及近距离观察到的项目,鲜有可以称得上成功的。这里的成功不是商业上的成功,仅限于做为一个软件项目和工程是否可以以可接受的成本和质量长期稳定的交付。
商业的短时间成功与否,不少时候与项目工程的成功与否没有必然联系,一个商业上很成功的项目可能在工程上作的并很差,只是经过巨量的资金资源投入换来的暂时成功而已。
归根结底,咱们程序员群体须要为本身的声誉负责,长期来看也终究会为本身的声誉获益或受损。
我认为程序员最大的声誉、最重要的职业素养,就是经过写出高质量的代码作好一个个项目、产品,来帮助团队、帮助公司、帮助组织创造价值、增长成功的机会。
但愿本文分享的经验和方法可以对此有所帮助!
你好,我是四猿外。
一家上市公司的技术总监,管理的技术团队一百余人。
我从一名非计算机专业的毕业生,转行到程序员,一路打拼,一路成长。
我会经过公众号,
把本身的成长故事写成文章,
把枯燥的技术文章写成故事。