一个技术总监的忠告:你精通那么多技术,为什么仍是作很差一个项目?

编写高质量可维护的代码既是程序员的基本修养,也是能决定项目成败的关键因素,本文试图总结出问题项目广泛存在的共性问题并给出相应的解决方案。vue

1. 程序员的宿命?

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

国内基本上是这样,国外状况我了解很少,不过从英文社区和技术媒体上老外同行的抱怨程度看,应该是差很少的,虽然总体素质可能更高,可是也因更久的信息化而积累了更多问题。毕竟“焦油坑、Shit_Mountain 屎山”这些舶来的术语不是平白无故被发明出来的。程序员

Any way,这大概就是咱们这个行业的宿命——要么改行,要么就是与烂项目烂代码长相伴。
就像宇宙的“熵增长定律”同样:redis

孤立系统的一切自发过程均向着令其状态更无序的方向发展,若是要使系统恢复到原先的有序状态是不可能的,除非外界对它作功。算法

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

那些不认命的选择与之抗争,可是地上并无路,当年软件危机的阴云也从未真正散去,人月神话仍然是神话,因而人们作出了各自不一样的判断和尝试:sql

  • 掀桌子另起炉灶派:
    • 不少人把项目作烂的缘由归咎于项目前期的基础没打好、需求不稳定一路打补丁、前面的架构师和程序员留下的烂摊子难以收拾。
    • 他们要么没有信心去收拾烂摊子,要么以为这是费力不讨好,因而要放弃掉项目,寄但愿于出现一个机会能重头再来。
    • 可是他们对于如何避免重蹈覆辙、作出另外一个烂项目是没有把握也没有深刻思考的,只是盲目乐观的认为本身比前任更高明。
  • 激进改革派:
    • 这个派别把缘由归结于烂项目当初没有采用正确的编程语言、最新最强大的技术栈或工具。
    • 他们中一部分人也想着有机会另起炉灶,用时下最流行最热门的技术栈(spring boot、springcloud、redis、nosql、docker、vue)。
    • 或者即使不另起炉灶,也认为现有技术栈太过期没法容忍了(其实可能并不算过期),不用微服务不用分布式就不能接受,因而激进的引入新技术栈,鲁莽的对项目作大手术。
    • 这种对刚刚流行还不成熟技术的盲目跟风、技术选型不慎重的状况很是广泛,今天在他们眼中落伍的技术栈,其实也不过是几年前另外一批人赶的时髦。
    • 我不反对技术上的追新,可是一样的,这里的问题是:他们对于大手术的风险和反作用,对如何避免重蹈覆辙用新技术架构作出另外一个烂项目,没有把握也没有深刻思考的,只是盲目乐观的认为新技术能带来成功。
    • 也没人能阻止这种简历驱动的技术选型浮躁风气,毕竟花的是公司的资源,用新东西显得本身颇有追求,失败了也不影响简历美化,简历上只会增长一段项目履历和几种精通技能,不会提到又作烂了一个项目,名利双收稳赚不赔。
  • 保守改良派:
    • 还有一类人他们不肯轻易放弃这个有问题但仍在创造效益的项目,由于他们看到了项目仍然有维护的价值,也看到了另起炉灶的难度(万事开头难,其实项目的冷启动存在不少外部制约因素)、大手术对业务形成影响的代价、系统迁移的难度和风险。
    • 同时他们尝试用温和渐进的方式逐步改善项目质量,采用一系列工程实践(主要包括重构热点代码、补自动化测试、补文档)来清理“技术债”,消除制约项目开发效率和交付质量的瓶颈。

若是把一个问题项目比做病入膏肓的病人,那么这三种作法分别至关因而放弃治疗、截肢手术、保守治疗。docker

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

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

可是近几年随着年龄增加,一方面新东西学不动了,另外一方面对经历过的项目反思的多了观念逐渐改变了。编程

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

  • 从技术选型到架构设计到代码规范,都是我本身作的,团队不大,也是我本身组建和一手带出来的;
  • 最开始的半年进展很是顺利,用着我最趁手的技术和工具一路狂奔,年末前替换掉了以前采购的那个垃圾产品(对的,有个前任在业务上作参照也算是个很大的有利因素);
  • 作的过程我也算是尽心尽力,用尽毕生所学——前面 13 年工做的经验值和走过的弯路、教训,使得公司只用其它同类公司同类项目 20% 的资源就把平台作起来了;
  • 若是说多快好省是最高境界,那么当时的我算是作到了多、快、省——交付的功能很是丰富且贴近业务需求、开发节奏快速、对公司开发资源很节省;
  • 可是如今看来,“好”就远远没有达到了,到了项目中期,简单优先级高的需求都已经作完了,公司业务上出现了新的挑战——接入另外一个核心系统以及外部平台,真正的考验来了。
  • 那个改造工程影响面比较大,须要对咱们的系统作大面积修改,最麻烦的是这意味着从一个简单的单体系统变成了一个分布式的系统,并且业务涉及资金交易,可靠性要求较高,是难上加难。
  • 因而问题开始出现了:我以前架构的优势——简单直接——这个时候再也不是优势了,简单直接的架构在业务环境、技术环境都简单的状况下能够作到多快好省,可是当业务、技术环境都陡然复杂起来时,就不行了;
  • 具体的表现就是:架构和代码层面的结构都快速的变得复杂、混乱起来了——熵急剧增长;
  • 后面的事情就一发不可收拾:代码改起来愈来愈吃力、测试问题变多、生产环境故障和问题变多、因而消耗在排查测试问题生产问题和修复数据方面的精力急剧增长、出现恶性循环。。。
  • 到了这个境地,项目就算是作烂了!一个我从头开始作起的没有任何借口的失败!

因而我意识到一个很是浅显的道理:拥有一张空白的画卷、一支最高级的画笔、一间专业的画室,没法保证你能够画出美丽的画卷。若是你不善于画画,那么一切都是空想和意淫。

而后我变成了一个“保守改良派”,由于我意识到掀桌子和激进的改革都是不负责任的,说很差听的那样实际上是掩耳盗铃、逃避困难,人不可能逃避一生,你总要面对。

即使掀了桌子另起炉灶了,你仍是须要找到一种办法把这个新的炉灶烧好,由于随着项目发展以前的老问题仍是会一个一个冒出来,仍是须要面对现实、不逃避、找办法。

面对问题不只有助于你把当前项目作好,也一样有助于未来有新的项目时更好的把握住机会。

不管是职业生涯仍是天然年龄,人到了这个阶段都开始喜欢回顾和总结,也变得比过去更在意项目、产品乃至公司的商业成败。

软件开发做为一种商业活动,判断其成败的依据应该是:可否以可接受的成本、可预期的时间节奏、稳定的质量水平、持续交付知足业务须要的功能市场须要的产品。

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

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

  • 质量要素不是一个能够被牺牲和妥协的要素——牺牲质量会致使其它三要素全都受损,反之同理,追求质量会让你在其它三个方面同时受益。
  • 在保持一个质量水平的前提下,成本、进度、范围三要素确确实实是互相制约关系——典型的好比牺牲成本(加班加点)来加快进度交付急需的功能。
  • 正如著名的“破窗效应”所启示的那样:任何一种不良现象的存在,都在传递着一种信息,这种信息会致使不良现象的无限扩展,同时必须高度警觉那些看起来是偶然的、个别的、轻微的“过错”,若是对这种行为漠不关心、熟视无睹、反应迟钝或纠正不力,就会纵容更多的人“去打烂更多的窗户玻璃”,就极有可能演变成“千里之堤,溃于蚁穴”的恶果——质量不佳的代码之于一个项目,正如一扇破了的窗之于一幢建筑、一个蚂蚁巢之于一座大堤。
  • 好消息是,只要把质量提上去项目就会逐渐走上健康的轨道,其它三个方面也都会改善。管好了质量,你就很大程度上把握住了项目成败的关键因素。
  • 坏消息是,项目的质量很容易失控,现实中质量不佳、越作越臃肿混乱的项目比比皆是,质量改善越作越好的案例闻所未闻,以致于人们将其视为如同物理学中“熵增长定律”同样的必然规律了。
  • 固然任何事情都有一个度的问题,当质量低于某个水平时才会致使其它三要素同时受损。反之当质量高到某个水平之后,继续追求质量不只得不到明显收益,并且也会损害其它三要素——边际效用递减定律。
  • 这个度须要你为本身去评估和测量,若是目前的质量水平还在二者之间,那么就应该重点改进项目质量。固然,现实世界中不多看到哪一个项目质量高到了不须要重视的程度。
3. 项目走向衰败的最多见诱因——代码质量不佳

一个项目的衰败一如一我的健康情况的恶化,固然可能有多种多样的缘由——好比需求失控、业务调整、人员变更流失。可是做为咱们技术人,若是能作好本身份内的工做——编写出可维护的代码、减小技术债利息成本、交付一个健壮灵活的应用架构,那也绝对是功德无量的。

虽然很难估算出这究竟能挽救多少项目,可是在我十多年职业生涯中,经历的和近距离观察的几十个项目,确实看到了大量的项目正是因为代码质量不佳致使的失败和遗憾,同时我也发现其实失败项目的不少问题、症结也确确实实均可以归因到项目代码的混乱和质量低下,好比一个常见的项目腐烂恶性循环:代码乱》bug 多》排查问题耗时》复用度低》加班 996》士气低落……

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

接下来,让咱们从项目管理聚焦到项目代码质量这个相对小的领域来深刻剖析。编写高质量可维护的代码是程序员的基本修养,本文试图在代码层面找到一些失败项目中广泛存在的症结问题,同时基于我的十几年开发经验总结出的一些设计模式做为药方分享出来。

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

推荐《设计模式之美》的第二章节《从哪些维度评判代码质量的好坏?如何具有写出高质量代码的能力?》,这是我看到的关于代码质量主题最精彩深入的论述。

4. 一个失败项目复盘

先贴几张代码截图,看一下这个重病缠身的项目的病灶和症状:

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

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

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

这里先不去分析这个类的问题,只是初步展现一下病情严重程度。

我相信这应该不算是特别糟糕的状况,比这个严重的项目俯拾皆是,可是这也应该足够拿来暴露问题、剖析成因了。

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

分层的理念早已深刻人心,尤为是业务逻辑层的独立,完全杜绝了以前(不分层的年代)业务逻辑与展示逻辑、持久化逻辑等混杂的问题。

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

  • 业界关于如何设计业务逻辑层 并无标准和最佳实践,绝大多数项目(我本身经历过的项目以及我有机会深刻了解的项目)中你们都是想固然的按照业务领域对象来设计;
  • 例如:领域实体对象有 Account、Order、Delivery、Campaign。因而业务逻辑层就设计出 AccountService、OrderService、DeliveryService、CampaignService
  • 这种作法在项目简单是没什么问题,事实上项目简单时 你随便怎么设计都问题不大。
  • 可是当项目变大和复杂之后,就会出现问题了:
    • 组件臃肿:Service 组件的个数跟领域实体对象个数基本至关,必然形成个别 Service 组件变得很是臃肿——API 很是多,代码行数达到几千行;
    • 职责模糊:业务逻辑每每跨多个领域实体,不管放在哪一个 Service 都不合适,一样的,要找一个功能的实现逻辑也没法肯定在哪一个 Service 中;
    • 代码重复 or 逻辑纠缠的两难选择:当遇到一个业务逻辑,其中的某个环节在另外一个业务逻辑 API 中已经实现,这时若是不想忍受重复实现和代码,就只能去调用那个 API。但这样就形成了业务逻辑组件之间的耦合与依赖,这种耦合与依赖很快会扩散——新的 API 又会被其它业务逻辑依赖,最终造成蜘蛛网同样的复杂依赖甚至循环依赖;
    • 复用代码、减小重复虽然是好的,可是复杂耦合依赖的害处也很大——赶走一只狼引来了一只虎。两杯毒酒给你选!

前面截图的那个问题组件 ContractService 就是一个典型案例,这样的组件每每是热点代码以及整个项目的开发效率的瓶颈。

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

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

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

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

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

  • 高内聚:组件自己应该尽量的包含其所实现功能的全部重要信息和细节,以便让维护者无需跳转到其它多个地方去了解必要的知识。
  • 低耦合:组件之间的互相依赖和了解尽量少,以便在一个组件须要改动时其它组件不受影响。

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

我观察发现,很低项目都存在低内聚、高耦合的问题。根本缘由在于不少程序员,甚至是不少经验丰富的程序员也缺乏这方面的意识——对概念不甚清楚、对危害没有认识、对如何避免更是无从谈起。

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

  • 业界关于“复用性”的认识存在一个误区——认为包括业务逻辑组件在内的任何层面的组件都应该追求最大限度的可复用性
  • 复用固然是好的,但那应该有个前提条件:不增长系统复杂度的状况下的复用,才是好的。
  • 什么样的复用会增长系统复杂性、是很差的呢?前面提到的,一个业务逻辑 API 被另外一个业务逻辑 API 复用——就是很差的:
    • 损害了稳定性:由于业务逻辑自己是跟现实世界的业务挂钩的,而业务会发生变化;当你复用一个会发生变化的 API,至关于在沙子上建高楼——地基是松动的;
    • 增长了复杂性:这样的依赖还形成代码可读性下降——在一个本就复杂的业务逻辑代码中,包含了对另外一个复杂业务逻辑的调用,复杂度会急剧增长,并且会不断泛滥和传递;
    • 内聚性被破坏:因为业务逻辑被打散在了多个组件的方法内,变得支离破碎,没法在一个地方看清总体逻辑脉络和实现步骤——内聚性被破坏,同时也意味着,这个调用链条上涉及的全部组件之间存在高耦合。

4.4 药方 2:复用的两种正确姿式——打造本身的 lib 和 framework

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

  • lib 库是供你(应用程序)调用的,它帮你实现特定的能力(好比日志、数据库驱动、json 序列化、日期计算、http 请求)。
  • framework 框架是供你扩展的,它自己就是半个应用程序,定义好了组件划分和交互机制,你须要按照其规则扩展出特定的实现并绑定集成到其中,来完成一个应用程序。
  • lib 就是组合方式的复用,framework 则是继承式的复用,继承的 Java 关键字是 extends,因此本质上是扩展。
  • 过去有个说法:“组合优于继承,能用组合解决的问题尽可能不要继承”。我不一样意这个说法,这容易误导初学者觉得组合优于继承,其实继承才是面向对象最强大的地方,固然任何东西都不能乱用。
  • 典型的继承乱用就是为了得到父类的某个 API 而去继承,继承必定是为了扩展,而不是为了直接得到一个能力,得到能力应该调用 lib,父类不该该去实现具体功能,那是 lib 该作的事。
  • 也不该该为了使用 lib 而去继承 lib 中的 Class。lib 就是用来被组合被调用的,framework 就是用来被继承、扩展的。
  • 再展开一下:lib 既能够是第三方的(log4j、httpclient、fastjson),也但是你本身工程的(好比你的持久层 Dao、你的 utils);
  • framework 同理,既能够是第三方的(springmvc、jpa、springsecurity),也能够是你项目内封装的面向具体业务领域的(好比 report、excel 导出、paging 或任何可复用的算法、流程)。
  • 从这个意义上说,一个项目中的代码其实只有 3 种:自定义的 lib class、自定义的 framework 相关 class、扩展第三方或自定义 framework 的组件 class。
  • 再扩展一下:相对于过去,如今咱们已经有了足够多的第三方 lib 和 framework 来复用,来帮助项目节省大量代码,开发工做彷佛变成了索然无味、没技术含量的 CRUD。可是对于业务很是复杂的项目,则须要有经验、有抽象思惟、懂设计模式的人,去设计面向业务的 framework 和面向业务的 lib,只有这样才能交付可维护、可扩展、可复用的软件架构——高质量架构,帮助项目或产品取得成功。

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

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

个人观点是:所谓代码中的业务逻辑,是指这段代码所表现出的全部输入输出规则、算法和行为,一般能够分为如下 5 类:

  • 输入合法性校验:
  • 业务规则校验:典型的如检查交易记录状态、金额、时限、权限等,一般包含数据库或外部接口的查询做为参考;
  • 数据持久化行为:数据库、缓存、文件、日志等任何形式的数据写入行为;
  • 外部接口调用行为;
  • 输出/返回值准备。

固然具体到某一个组件实例,可能不会包括上述所有 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<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);
    }
}

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

解决“逻辑纠缠”最关键是要找到一种隔离机制,把两个 Level 的逻辑分开——控制逻辑分离,分离的好处不少:

  • 根据经验,当咱们着手维护一段代码时,必定是想先弄清楚它的总体流程、算法和行为,而不是一上来就去研究它的细枝末节;
  • 控制逻辑分离后,只须要去看 High Level 部分就能了解到上述内容,阅读代码的负担大幅度下降,代码可读性显著加强;
  • 读懂代码是后续一切维护、重构工做的前提,并且一份代码被读的次数远远高于被修改的次数(高一个数量级),所以代码对人的可读性再怎么强调都不为过,可读性加强能够大幅度提升系统可维护性,也是重构的最主要目标。
  • 同时,根据个人经验,High Level 业务逻辑的变动每每比 Low Level 实现逻辑变动要来的频繁,毕竟前者跟业务直接对应。固然不一样类型项目状况不同,另外它们发生变动的时间点每每也不一样;
  • 在这样的背景下,控制逻辑分离的好处就更明显了:每次维护、扩充系统功能只需改动一个 Levle 的代码,另外一个 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 设计模式真的很是适合解决普遍存在的逻辑纠缠问题,并且也发现不多有程序员能主动运用这个设计模式;
一部分缘由多是意识到“逻辑纠缠”问题的人本就很少,同时熟悉这个设计模式并能自如运用的人也不算多,二者的交集天然就是少得可怜;无论是什么缘由,结果就是这个问题普遍存在成了通病。

我看到一部分对代码质量有追求的程序员 他们的解决办法是经过"结构化编程"和“模块化编程”:

  • 把 Low Level 逻辑提取成 private function,被 High Level 代码所在的 function 直接调用;
    • 问题 1 硬链接不灵活:首先,这样虽然起到了必定的隔离效果,可是两个 level 之间是静态的硬关联,Low Level 没法被简单的替换,替换时仍是须要修改和影响到 High Level 部分;
    • 问题 2 组件内可见性形成混乱:提取出来的 private function 在当前组件内是全局可见的——对其它无关的 High Level function 也是可见的,各个模块之间仍然存在逻辑纠缠。这在不少项目中的热点代码中很常见,问题也很突出:试想一个包含几十个 API 的组件,每一个 API 的 function 存在一两个关联的 private function,那这个组件内部的混乱程度、维护难度是难以承受的。
  • 把 Low Level 逻辑抽取到新的组件中,供 High Level 代码所在的组件依赖和调用;更有经验的程序员可能会增长一层接口而且借助 Spring 依赖注入;
    • 问题 1 API 泛滥:提取出新的组件彷佛避免了“结构化编程”的局限性,可是带来了新的问题——API 泛滥:由于组件之间调用只能走 public 方法,而这个 API 其实没有太多复用机会根本不必作成 public 这种最高可见性。
    • 问题 2 同层组件依赖失控:组件和 API 泛滥后必然致使组件之间互相依赖成为常态,慢慢变得失控之后最终变成全部组件都依赖其它大部分组件,甚至出现循环依赖;好比那个拥有 130 个 import 和 40 个 Spring 依赖组件的 ContractService。

下面介绍一下 Template Method 设计模式的运用,简单概括就是:

  • High Level逻辑封装在抽象父类AbsUpdateFromMQ的一个final function中,造成一个业务逻辑的模板;
  • final function保证了其中逻辑不会被子类有意或无心的篡改破坏,所以其中封装的必定是业务逻辑中那些相对固定不变的东西。至于那些可变的部分以及暂时不肯定的部分,以abstract protected function形式预留扩展点;
  • 子类(一个匿名内部类)像“作填空题”同样填充,模板实现Low Level逻辑——实现那些protected function扩展点;因为扩展点在父类中是abstract的,所以编译器会提醒子类的程序员该扩展什么。

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

  • Low Level 须要修改或替换时,只需从父类扩展出一个新的子类,父类全然不知无需任何改动;
  • 不管是父类仍是子类,其中的 function 对外层的 XyzService 组件都是不可见的,即使是父类中的 public function 也不可见,由于只有持有类的实例对象才能访问到其中的 function;
  • 不管是父类仍是子类,它们都是做为 XyzService 的内部类存在的,不会增长新的 java 类文件更不会增长大量无心义的 API(API 只有在被项目内复用或发布出去供外部使用才有意义,只有惟一的调用者的 API 是没有必要的);
  • 组件依赖失控的问题固然也就不存在了。

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

NestedBusinessTemplat 模式就是对其充分和积极的应用,前面一节提到过的复用的两种正确姿式——打造本身的 lib 和 framework,其实 NestedBusinessTemplat 就是项目自身的 framework。

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

不管你的编程启蒙语言是什么,最先学会的逻辑控制语句必定是 if else,可是不幸的是它在你开始真正的编程工做之后,会变成一个损害项目质量的坏习惯。

几乎全部的项目都存在 if else 泛滥的问题,可是却没有引发足够重视警戒,甚至被不少程序员认为是正常现象。

首先我来解释一下为何 if else 这个看上去人畜无害的东西是有害的、是须要严格管控的

  • if else if ...else 以及相似的 switch 控制语句,本质上是一种 hard coding 硬编码行为,若是你赞成“magic number 魔法数字”是一种错误的编程习惯,那么同理,if else 也是错误的 hard coding 编程风格;
  • hard coding 的问题在于当需求发生改变时,须要处处去修改,很容易遗漏和出错;
  • 以一段代码为例来具体分析:
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 ("3".equals(object.getString("type")))
    • 显然这里的"3"是一个 magic number,没人知道 3 是什么含义,只能推测;
    • 可是仅仅将“3”重构成常量 ABC_XYZ 并不会改善多少,由于 if (ABC_XYZ.equals(object.getString("type"))) 仍然是面向过程的编程风格,没法扩展;
    • 处处被引用的常量 ABC_XYZ 并无比处处被 hard coding 的 magic number 好多少,只不过有了含义而已;
    • 把常量升级成 Enum 枚举类型呢,也没有好多少,当须要判断的类型增长了或判断的规则改变了,仍是须要处处修改——Shotgun Surgery(霰弹式修改)
  • 并不是全部的 if else 都有害,好比上面示例中的 if (list1 !=null) { 就是无害的,没有必要去消除,也没有消除它的可行性。判断是否有害的依据:
    • 若是 if 判断的变量状态只有两种可能性(好比 boolean、好比 null 判断)时,是无伤大雅的;
    • 反之,若是 if 判断的变量存在多种状态,并且未来可能会增长新的状态,那么这就是个问题;
    • switch 判断语句无疑是有害的,由于使用 switch 的地方每每存在不少种状态。

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 {    //一、定义一个包含通知实现机制的“充血”的枚举类型
  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);
  • 充血枚举类型——Rich Enum Type 模式的优点:
    • 不难发现,这其实就是 enum 枚举类型和 Strategy Pattern 策略模式的巧妙结合运用,
    • 当须要增长新的通知方式时,只需在枚举类 NOTIFY_TYPE 增长一个值,同时在策略接口 NotifyMechanismInterface 中增长一个 by 方法返回对应的策略实现;
    • 当须要修改某个通知机制的实现细节,只需修改 NotifyMechanismInterface 中对应的策略实现;
    • 不管新增仍是修改通知机制,调用方彻底不受影响,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
  • 与传统 Strategy Pattern 策略模式的比较优点:常见的策略模式也能消灭 if else 判断,可是实现起来比较麻烦,须要开发更多的 class 和代码量:
    • 每一个策略实现需单独定义成一个 class;
    • 还须要一个 Context 类来作初始化——用 Map 把类型与对应的策略实现作映射;
    • 使用时从 Context 获取具体的策略;
  • Rich Enum Type 的进一步的充血:
    • 上面的例子中的枚举类型包含了行为,所以已经算做充血模型了,可是还能够为其进一步充血;
    • 例若有些场景下,只是要对枚举值作个简单的计算得到某种 flag 标记,那就不必把计算逻辑抽象成 NotifyMechanismInterface 那样的接口,杀鸡用了牛刀;
    • 这是就能够在枚举类型中增长 static function 封装简单的计算逻辑;
  • 策略实现的进一步抽象:
    • 当各个策略实现(byEmail bySms byWechat)存在共性部分、重复逻辑时,能够将其抽取成一个抽象父类;
    • 而后就像前一章节——业务模板 Pattern of NestedBusinessTemplate 那样,在各个子类之间实现优雅的逻辑分离和复用。
5. 重构前的火力侦察:为你的项目编制一套代码库目录/索引——CODEX

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

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

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

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

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

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

  • 所谓结构化注释,就是在注释内容中经过规范命名的编号前缀、分隔符等来体现出其所对应的项目、模块、流程步骤等信息,相似文本编辑中的标题 一、二、3;
  • 而后设置 IDE 工具识别这种特殊的注释,以便结构化的显示。Eclipse 的 Tasks 显示效果相似下图;

  • 这个结构化视图,本质上相对因而代码库的索引、目录,不一样于 javadoc 文档,CODEX 具备更清晰的逻辑层次和更强的代码查找便利性,在 Eclipse Tasks 中点击就能跳转到对应的代码行;
  • 这些结构化注释随着代码一块儿提交后就实现了团队共享;
  • 这样的一份精确无误、共享的、活的源代码索引,无疑会对整个团队的开发维护工做产生巨大助力。
  • 进一步的,若是在 CODEX 中添加 Markdown 关键字,甚至能够将导出的 CODEX 简单加工后,变成一张业务逻辑的 Sequence 序列图,以下所示。

6. 总结陈词——不要辜负这个程序员最好的时代

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

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

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

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

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

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

但愿本文分享的经验和方法可以对此有所帮助!


你好,我是四猿外。

一家上市公司的技术总监,管理的技术团队一百余人。

我从一名非计算机专业的毕业生,转行到程序员,一路打拼,一路成长。

我会经过公众号,
把本身的成长故事写成文章,
把枯燥的技术文章写成故事。

相关文章
相关标签/搜索