一线互联网技术总监的忠告:我们精通那么多技术为何还是做不好一个项目?(深度好文,值得收藏!)AndroidAlvin

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

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

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

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

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

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

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

掀桌子另起炉灶派:

很多人把项目做烂的原因归咎于项目前期的基础没打好、需求不稳定一路打补丁、前面的架构师和程序员留下的烂摊子难以收拾。

他们要么没有信心去收拾烂摊子,要么觉得这是费力不讨好,于是要放弃掉项目,寄希望于出现一个机会能重头再来。

但是他们对于如何避免重蹈覆辙、做出另一个烂项目是没有把握也没有深入思考的,只是盲目乐观的认为自己比前任更高明。

激进改革派:

这个派别把原因归结于烂项目当初没有采用正确的编程语言、最新最强大的技术栈或工具。

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

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

我不反对技术上的追新,但是同样的,这里的问题是:他们对于大手术的风险和副作用,对如何避免重蹈覆辙用新技术架构做出另一个烂项目,没有把握也没有深入思考的,只是盲目乐观的认为新技术能带来成功。

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

保守改良派:

还有一类人他们不愿轻易放弃这个有问题但仍在创造效益的项目,因为他们看到了项目仍然有维护的价值,也看到了另起炉灶的难度(万事开头难,其实项目的冷启动存在很多外部制约因素)、大手术对业务造成影响的代价、系统迁移的难度和风险。

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

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

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

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

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

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

然后我变成了一个“保守改良派”,因为我意识到掀桌子和激进的改革都是不负责任的,说不好听的那样其实是掩耳盗铃、逃避困难,人不可能逃避一辈子,你总要面对。

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

面对问题不仅有助于你把当前项目做好,也同样有助于将来有新的项目时更好的把握住机会。

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

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

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

一个项目的衰败一如一个人健康状况的恶化,当然可能有多种多样的原因——比如需求失控、业务调整、人员变动流失。但是作为我们技术人,如果能做好自己分内的工作——编写出可维护的代码、减少技术债利息成本、交付一个健壮灵活的应用架构,那也绝对是功德无量的。

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

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

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

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

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

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

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

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

业界关于如何设计业务逻辑层并没有标准和最佳实践,绝大多数项目(我自己经历过的项目以及我有机会深入了解的项目)中大家都是想当然的按照业务领域对象来设计;

例如:领域实体对象有Account、Order、Delivery、Campaign。于是业务逻辑层就设计出AccountService、OrderService、DeliveryService、CampaignService

这种做法在项目简单是没什么问题,事实上项目简单时你随便怎么设计都问题不大。

但是当项目变大和复杂以后,就会出现问题了:

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

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

代码重复or逻辑纠缠的两难选择:当遇到一个业务逻辑,其中的某个环节在另一个业务逻辑API中已经实现,这时如果不想忍受重复实现和代码,就只能去调用那个API。但这样就造成了业务逻辑组件之间的耦合与依赖,这种耦合与依赖很快会扩散——新的API又会被其它业务逻辑依赖,最终形成蜘蛛网一样的复杂依赖甚至循环依赖;

复用代码、减少重复虽然是好的,但是复杂耦合依赖的害处也很大——赶走一只狼引来了一只虎。两杯毒酒给你选!

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

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

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

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

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

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

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

复用当然是好的,但那应该有个前提条件:不增加系统复杂度的情况下的复用,才是好的。

什么样的复用会增加系统复杂性、是不好的呢?前面提到的,一个业务逻辑API被另一个业务逻辑API复用——就是不好的:

损害了稳定性:因为业务逻辑本身是跟现实世界的业务挂钩的,而业务会发生变化;当你复用一个会发生变化的API,相当于在沙子上建高楼——地基是松动的;

增加了复杂性:这样的依赖还造成代码可读性降低——在一个本就复杂的业务逻辑代码中,包含了对另一个复杂业务逻辑的调用,复杂度会急剧增加,而且会不断泛滥和传递;

内聚性被破坏:由于业务逻辑被打散在了多个组件的方法内,变得支离破碎,无法在一个地方看清整体逻辑脉络和实现步骤——内聚性被破坏,同时也意味着,这个调用链条上涉及的所有组件之间存在高耦合。

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

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

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

当然具体到某一个组件实例,可能不会包括上述全部5类业务逻辑,但是也可能每一类业务逻辑存在多个。

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

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

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

@Override4.6药方3:控制逻辑分离——业务模板PatternofNestedBusinessTemplate解决“逻辑纠缠”最关键是要找到一种隔离机制,把两个Level的逻辑分开——控制逻辑分离,分离的好处很多:

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

publicclassXyzService{@SuppressWarnings({"unchecked","rawtypes"})如果你熟悉经典的GOF23种设计模式,很容易发现上面的代码示例其实就是TemplateMethod设计模式的运用,没什么新鲜的。

没错,我这个方案没有提出和创造任何新东西,我只是在实践中偶然发现TemplateMethod设计模式真的非常适合解决广泛存在的逻辑纠缠问题,而且也发现很少有程序员能主动运用这个设计模式;一部分原因可能是意识到“逻辑纠缠”问题的人本就不多,同时熟悉这个设计模式并能自如运用的人也不算多,两者的交集自然就是少得可怜;不管是什么原因,结果就是这个问题广泛存在成了通病。

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

把LowLevel逻辑提取成privatefunction,被HighLevel代码所在的function直接调用;

问题1硬连接不灵活:首先,这样虽然起到了一定的隔离效果,但是两个level之间是静态的硬关联,LowLevel无法被简单的替换,替换时还是需要修改和影响到HighLevel部分;

问题2组件内可见性造成混乱:提取出来的privatefunction在当前组件内是全局可见的——对其它无关的HighLevelfunction也是可见的,各个模块之间仍然存在逻辑纠缠。这在很多项目中的热点代码中很常见,问题也很突出:试想一个包含几十个API的组件,每个API的function存在一两个关联的privatefunction,那这个组件内部的混乱程度、维护难度是难以承受的。

把LowLevel逻辑抽取到新的组件中,供HighLevel代码所在的组件依赖和调用;更有经验的程序员可能会增加一层接口并且借助Spring依赖注入;

问题1API泛滥:提取出新的组件似乎避免了“结构化编程”的局限性,但是带来了新的问题——API泛滥:因为组件之间调用只能走public方法,而这个API其实没有太多复用机会根本没必要做成public这种最高可见性。

问题2同层组件依赖失控:组件和API泛滥后必然导致组件之间互相依赖成为常态,慢慢变得失控以后最终变成所有组件都依赖其它大部分组件,甚至出现循环依赖;比如那个拥有130个import和40个Spring依赖组件的ContractService。

下面介绍一下TemplateMethod设计模式的运用,简单归纳就是:

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

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

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

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

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

首先我来解释一下为什么ifelse这个看上去人畜无害的东西是有害的、是需要严格管控的:

if("3".equals(object.getString("type"))){if("3".equals(object.getString("type")))

显然这里的"3"是一个magicnumber,没人知道3是什么含义,只能推测;

但是仅仅将“3”重构成常量ABC_XYZ并不会改善多少,因为if(ABC_XYZ.equals(object.getString("type")))仍然是面向过程的编程风格,无法扩展;

到处被引用的常量ABC_XYZ并没有比到处被hardcoding的magicnumber好多少,只不过有了含义而已;

把常量升级成Enum枚举类型呢,也没有好多少,当需要判断的类型增加了或判断的规则改变了,还是需要到处修改——ShotgunSurgery(霰弹式修改)

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

如果if判断的变量状态只有两种可能性(比如boolean、比如null判断)时,是无伤大雅的;

反之,如果if判断的变量存在多种状态,而且将来可能会增加新的状态,那么这就是个问题;

switch判断语句无疑是有害的,因为使用switch的地方往往存在很多种状态。

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

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

enumNOTIFY_TYPE{email,sms,wechat;}//先定义一个enum——一个只定义了值不包含任何行为的“贫血”的枚举类型实现多种系统通知方式,充血枚举类型——RichEnumType模式:enumNOTIFY_TYPE{//1、定义一个包含通知实现机制的“充血”的枚举类型充血枚举类型——RichEnumType模式的优势:

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

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

当需要修改某个通知机制的实现细节,只需修改NotifyMechanismInterface中对应的策略实现;

无论新增还是修改通知机制,调用方完全不受影响,仍然是NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);

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

每个策略实现需单独定义成一个class;

还需要一个Context类来做初始化——用Map把类型与对应的策略实现做映射;

使用时从Context获取具体的策略;

RichEnumType的进一步的充血:

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

例如有些场景下,只是要对枚举值做个简单的计算获得某种flag标记,那就没必要把计算逻辑抽象成NotifyMechanismInterface那样的接口,杀鸡用了牛刀;

这时就可以在枚举类型中增加staticfunction封装简单的计算逻辑;

策略实现的进一步抽象:

当各个策略实现(byEmailbySmsbyWechat)存在共性部分、重复逻辑时,可以将其抽取成一个抽象父类;

然后就像前一章节——业务模板PatternofNestedBusinessTemplate那样,在各个子类之间实现优雅的逻辑分离和复用。

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

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

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

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

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

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

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

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

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

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

THE END
1.去美国留学多久可以拿到体检报告?都有哪些体检项目?这份指南值得...那么,留美要求哪些体检项目?一般打什么疫苗? 一般来说,如果学校没有特殊的疫苗要求,所有的留学生只需要去国际旅行健康中心做健康体检,体检项目包括:X 光检查、心电图、血液检查、全身检查和免疫注射(默认打两针,MMR 和白喉破伤风)。体检完成后国际旅行健康中心根据体检情况出具健康证书(俗称的小红本),里面记录了您的...https://www.idp.cn/jianada/zhuanyedingwei/124958.html
2....地方事业动态带您了解国家基本公共卫生服务项目三是健康管理的服务过程是一个环形运转循环。健康管理的实施环节为通过健康体检和相应的实验室检测指标来监测健康状态变化、进行健康评估和实施健康干预。整个服务过程,通过这三个环节不断循环运行,以减少或降低健康危险因素和程度,维护健康水平。 2. 老年人的健康管理值得做吗? http://www.china-zibo.gov.cn/art/2020/5/22/art_4751_2071017.html
3.我研究了一个礼拜的体检,才发现自己花了好多冤枉钱之前我都在第三方的那种商业机构体检,今年准备换成大医院,拿着记的笔记,前前后后对比了北京三家医院的体检套餐后,我最终为小吴和我选了一套性价比最高的,非常满意。 所以我写这篇文章的目的只有一个,让看完的人也能辨别出哪些套餐值得买,哪些又没用又浪费钱。分享一些笔记给大家,高中毕业后就没这么认真做过笔...https://www.douban.com/note/733067707/
4.27款中老年女性体检套餐测评:近7000元高端套餐鸡肋项目多,胃肠镜...在这27款体检套餐中,大部分都标注有彩超项目,主要有腹部肝胆胰脾彩超、乳腺彩超、心脏彩超、甲状腺彩超、颈动脉彩超、子宫附件彩超等,基本能满足中老年女性体检的需求。值得注意的是,慈铭体检的4款千元不到的套餐中,有3款价格稍低的相比其他机构高端套餐另含有盆腔彩超,对于生育过的女性盆底肌功能检测具有重要作用,可...https://news.southcn.com/node_17a07e5926/8bfd3cf8e0.shtml
5.给项目管理做个全面体检工程管理给项目管理做个全面体检假如把项目比方成一个有机的整体,那么,项目肌体的健康有赖于项目管理各个组成部分的健康。建立健康肌体的第一步,就是对整个肌体的各个部分进行有效的检查。一次基本的“体格检查”将可以关心你发觉项目潜在的致命缺陷。下面将从六个方面争论项目管理的状况检查。-范围管理 -范围管理(Scope Manag...https://m.renrendoc.com/paper/204696957.html
6.体检项目越多越好?未必!这5个检查白花钱美国预防服务工作组(USPSTF)建议,如果没有心血管疾病的症状并且患心血管疾病的风险不高,则心电图、 颈动脉筛查、腹主动脉瘤筛查没必要做。没有医生要求,全身CT扫描、冠状动脉CT钙化积分没必要做。 随着收入水平的提高和体检中心的普及,通过体检来发现健康隐患的观念已经深入人心。近年来,人们接触到的体检项目随着体检...https://www.chunyuyisheng.com/pc/article/112437
7.做全身体检有哪些项目有问必答病情描述(发病时间、主要症状、症状变化等):做全身体检有哪些项目想得到怎样的帮助:近来两天起床会头晕,是什么原因呢_有问必答_寻医问药网https://3g.club.xywy.com/wenda/42912588.htm
8.广东高考体检项目有的学生直到高考体检后,才知道自己的身体出了问题,这个时候再来治疗已经晚了,没办法体检合格,使得报考受限,与自己心仪的高校和专业失之交臂。 体检中的一个主要项目就是查肝功能,这个项目的检查中每年都会有出现转氨酶异常的考生。 体检前1天考生应该清淡饮食,不要喝可乐、吃火锅等,若感冒了,体检时一定要告诉医生...https://www.wenshubang.com/shenghuo/404658.html
9.体检的必做项目有哪些体检的必做项目有哪些 体检的必做项目因个人年龄、性别、身体状况和需求而异,但一般包括以下几个基本项目: 1. 血常规:通过检查红细胞、白细胞、血小板等成分,可以评估贫血、感染等情况。这个项目可以提供有关血液状况的信息,有助于发现血液病变或感染。 2. 基本体格检查:包括身高、体重、血压、脉搏等指标,以了解...https://mip.3zhijk.com/doctor/mip/mip_article/2e33c0135e8d12935d6a42f2f50d1f9a.html