程序设计二三事 - 如何从点滴做起开发高质量项目

 在UC浏览器的开发过程中,笔者曾参与或主导过多次重构和设计工作。那么如何才能避免将来大规模重构,如何写出稳健、易维护、易扩展、生命力长久的程序呢?现将一些注意事项、踩坑记录写下来,供参考。

单向依赖

 单向依赖原则是我在任何团队都会着重强调的原则,千万不能随便违反。防微杜渐,从良好习惯开始,这些原则性的东西需要刻到骨子里去,不要有丝毫妥协。循环依赖,就更为恐怖了,你不仔细阅读代码根本就察觉不了,一旦出现问题,那将是致命的。修改成本很高,不是简单优化能解决的,基本上就是要重构了。参考ADP(The Acyclic Dependencies Principle,无环依赖原则)。

 我曾经在一个C++项目中遇到一个场景,A依赖B,B依赖C,C依赖D,最后D又会依赖A,问题的关键在于,A是在构造函数中使用B的,D会去调用A的方法。然而A还没构造完毕,其方法是不能随便调用的,因此出现崩溃。这就形成了先有鸡还是先有蛋的问题,排查这种问题很浪费时间。

避免太抽象太宽泛的工具库类

 千万不要建立Common/General/Utility这样的文件或类,因为一旦开了先例,将来会有一大堆的东西往里面塞,直到无法承受,且根本无法控制其坏味道蔓延。

 这个观点,我也是从别的文章看来的,算是拿来主义吧。当时看到这个说法的时候,觉得深有同感,试问我们是不是经常都在走这样的道路呢?

 如若不信,可以现在就去自己的项目中搜索文件名:Common / Util / Utils / Tools。

 我所经历过的项目,几乎也都干过这样的事情吧,通常就是觉得有一些基础工具,感觉命名比较难,很难想到一个准确优雅的名称,然后呢,这又是大家都需要使用的公共工具,那么就叫Common吧,这名字还不错,不失优雅,而且还有海纳百川的范儿!然后我们的项目中就出现了Common.h,Utilities.java之类的文件。一开始还好,里面都是一些基础的简单的工具函数,但是随着时间的推移,你会发现慢慢的也就变成了大杂烩,团队开发的时候,凡是一时难以分类的,或者觉得反正也是公共的东西,那就往这里面放吧。

 这样演变的结果,大家肯定不难想象,也肯定都见识过这样的案例。我们在做信息流重构的时候就遇到一个InfoFlowUtils类,虽然吧,它的命名已经有了InfoFlow这个约束词了,但是对于信息流产品来说,这个约束词也是太强大了些,几乎所有的工具都可以往上蹭吧。所以其结果就是,里面包含了各种工具,例如图形的、颜色的、字符串的、日期的、dispatcher操作的、MD5加密的、16进制运算的,真是应有尽有啊。当你移植代码的时候,发现你的代码需要依赖这些的时候,那滋味,那酸爽。。。

 为了避免这类现象的再次出现,唯一的办法就是不要再建立这样命名的库类。就算是对Utility这个命名情有独钟的,那么命名上也请严格遵循SRP单一职责原则)吧,比如UrlUtil,MathUtils,ColorUtility等。也就是在文件名前加上具体的约束,参考后面的,工具需要小而美。

 防微杜渐,大家共勉。   

低耦合、高内聚

 这里不阐述耦合和内聚的定义。
 耦合越高,复用越难,代码移植也就越困难。

 代码级的低耦合,也就是说,在能完成功能的前提下,你所需要依赖的代码或库越少越好,要敢于做减法。如果你真的觉得一个需求价值不大,由于逻辑怪异而对设计、对代码又有很严重的破坏,也需要敢于去跟产品PK。

 低耦合不光是代码级别要求低耦合,在业务逻辑上也要尽量做到低耦合。例如我们的项目里面,有换肤功能,其中内置了两种皮肤,日间模式和夜间模式皮肤,同时技术上可以支持多种自定义皮肤。在我们的业务代码中,很多地方都会去检测当前是否夜间模式,如果是的话,就去添加一个遮罩、或者做颜色变换,甚至有做图像变换的,我并不知道图像变换的效率如何,会不会影响到界面卡顿,反正凡是图像处理,潜意识都是低效率的、耗电的东西,除非这个变换刚好能使用上硬件加速,可是安卓机型那么多,能保证都能硬件加速吗?

 那么对于夜间模式的特殊性来说,正确的做法是什么呢?首先需要将处理模式从逻辑上统一化、一致化。也就是说无论日间模式、夜间模式,对业务层来说应该是透明的,业务层根本不应该关心当前是什么皮肤模式,只应该按统一的方案来处理,需要抹平不同模式之间的差异。比如一张图片,夜间模式需要变得暗淡一点,就不应该去修改图像的像素色彩值,完全可以换一种方式来实现。例如无论什么模式,我们都在图像控件上添加一层遮罩,这个遮罩的颜色由资源管理器(ResManager)来提供,而这个颜色是配置到皮肤资源XML文件里面的,既方便修改调整,也无需业务层去耦合那么多换肤逻辑,只需要在夜间模式XML文件里面配置一个半透明黑色,在日间模式资源里面配置纯透明色即可。

慎用继承

 抽象不是万能的。
 能不用继承也能很好解决问题的时候,就不要用继承,简单说:继承能不用就不用。

 虽然在很多场合,使用继承有它的合理性,而且继承也的确是一个非常有用的东东,然而一旦使用不当,就会成为灾难的根源,这就是我们常说要慎用继承的原因。
 关于继承的复杂性,可能面临的坑,请参考面向对象设计原则之:里氏替换原则(LSP)

 另外,有一种说法叫少用继承,多用组合(聚合)

 继承具有侵入性
 假如你需要编写一个ClassC,继承自ClassA,那么你的C就被A侵入了,你必须得遵循A的方式来设计接口,C跟A的关系,必须在编译时期就确定下来,当然就不具备运行时期再进行变化的灵活性,如果需要在某种条件下,不需要使用A的那一套特性,就会比较麻烦了。而且,就算不考虑动态变化,如果哪天产品经理说我们换一套花样,你发现新特性跟A没有半毛钱关系,那么你的ClassC的代码得全部重写,根本没法移植复用。

 多用组合的意思,就是你不必非要从一个类继承新类,再添加新特性。而是全新定义一个新类,将原本想继承的类作为你的成员,然后还可以引入更多的其它类,一起来组装你的功能。你完全可以按新的场景来设计接口,而不是受制于原先的类,从而避免被侵入,增强了将来变更的灵活性。

 我见过太多由于过度抽象或滥用继承而导致积重难返的案例,要重构只能重写。

 其实继承就是一种严重耦合,使用组合来代替继承也就是遵循了高内聚、低耦合的原则,很多设计原则都是想通的。

避免集中管理

 在我的记忆中,所经历过的项目都曾经有过集中管理,例如Message字符串或整形ID的集中定义,配置项Key的集中定义,JSBridge注册本地方法的集中注册等等,整个项目所有业务的某一个功能范围的一些常量,集中编写到一个定义文件里面,这种做法的好处是:

  • 代码整齐划一
  • 用常量别名定义具体的字符串或数字,避免使用中的笔误出错
  • 不容易产生重名冲突
  • 方便代码走查与监控

但是也有缺点:

  • 集中定义文件容易膨胀
  • 不易于代码复用

 好吧,缺点好像真不如优点多,这也许就是很多项目都乐此不疲的原因吧,而且就算有人觉得有问题,想要改变的化,压力也是巨大的,由于历史等种种原因几乎很难达成目标。甚至于有时候,集中管理成为了政治正确的事情,所以极少有人会去挑战传统。所以我也只有在全新的从零开始的九游iOS客户端项目中,才彻底避免了集中管理的侵入。

 集中管理的案例,在真实的项目中,我们一个项目往往由十几个乃至几十个业务模块构成,这些模块都会使用消息、都会使用首选项、都会使用动态配置、部分模块会使用JSBridge。那么这些大多数模块都会使用的东西,我们往往就会将它集中定义在一个地方,例如Message.h、JSConst.x、ConfigKeyDef.java等等。这些每一类别的业务场景资源都是统一集中管理的,大家都往里面添加自己的常量。这带来了一个问题,当有一天,我们想将某一个业务单元做成独立业务模块,做成SDK从整个项目中抽离出去复用,才发现异常痛苦,之前的代码根本不具备复用性,很难从容抽身出去,因为已经耦合严重。虽然复用性差并非集中管理一个原因造成,但是也是原因之一了。而且一个项目的集中管理场景根本就不只一个,上面就提到了三个,改造成本可想而知。

 那么问题怎么解决呢?
 只要我们要使用消息机制,就必须统一定义消息资源,否则会跟其它模块冲突导致程序异常。
 其实消息机制并非唯一的通信方案,我个人是不太喜欢使用消息来作为业务模块间的通信机制的,用路由来代替消息机制是现在比较常见的方案了,暂且不表。
 用一个简单的案例来说明消息定义臃肿耦合的改进办法,例如我们的换肤功能,以前也是在Message.h中新增一个ON_THEME_CHANGED消息,然后这个换肤模块就跟其它所有模块都产生了耦合。其实换肤模块并非必须要跟这个消息打交道,完全可以自己定义一个观察者接口,所有的UI模块都依赖这个换肤接口,注册皮肤变更观察者,当换肤事件发生时,这些UI模块都可以监听到这个事件即可。这样一来,就解除了换肤模块跟其它业务之间由于消息产生的耦合。
 当然这样一来,所有的UI业务模块就都得向换肤模块注册监听才行,而以前只需要注册一个消息回调,就能接受所有的消息了。这不是使用原方案的理由,有些事情本身就该业务层完成的事情,没必要回避。而且,这也并非没有办法解决,如果觉得每个UI模块都去注册换肤监听麻烦的话,完全可以在业务层作一个封装类来注册监听,UI业务层Controller继承这个封装类即可。所以慎用抽象并非不能用抽象,只要进行推敲、合理使用即可。

 另外,想在此强调一下,集中管理是严重违背 OCP(开放封闭原则) 的,所谓开放封闭原则,就是对扩展开放,对修改封闭。用通俗的话来说当你设计一个模块,别人可以在你的基础上添加新的代码来扩展你的功能,但是无需修改你的任何代码。做到这一点,你就是遵循开放封闭原则的。大家可以对标自己手上的项目代码,看有多少做到了,多少没有做到,需要怎么做才能做到。

去中心化

 曾看过一篇微信团队重构的分享文章,里面提到一个观点,核心模块容易中心化,就是一个比较重要的模块,跟大家都有交集,于是大家都往里面添加代码,最后就会出现该模块的膨胀、臃肿,直至出现严重问题。
 跟前面讲的集中管理的问题整体思路大致差不多,只是场景不同,不细说。

SDK化

 当初我们在一开始做浏览器的时候,可能觉得咱们就做一个浏览器项目,可能不会去做别的产品,所以就不会太在意复用性的问题,但是随着业务的扩展,会发现即便不做新产品,光产品内部也是完全可能需要复用的。如果我们能将一些业务单元,做成独立业务模块,甚至直接做成SDK,即便不是物理上的SDK,但是我们按SDK的接口标准、依赖合理化来设计这些模块,那么将来即便是需要重构改造,也是很容易的,通常只需要在某一模块内部进行重构即可。因为SDK的接口一定是经过推敲的,扩展性复用性是会很强的,不会由于业务扩展而做很大的改造。

 例如前面提到的,我们的很多业务单元本来应该是完全独立存在,不应该有任何关联的,那么这些能独立的业务单元,就应该按SDK的标准来设计,不要产生不合理的依赖。但是,我们当初的确是将他们需要通信的消息、需要注入的JS接口、以及其他的很多东西,都跟整个项目资源做了统一的集中管理,而那些集中管理模块就是根本无法复用的,分离改造成本都很高。因此,当哪一天项目变得积重难返,需要重构的时候,就发现重构成本异常的大。

 兄弟模块之间,不要有任何耦合。假如我们一开始就将那些模块分别设计成独立SDK,或者按照SDK的标准来要求,那么就不难理解那些不同业务的消息为什么不能集中定义了。

 但是,凡是不能走极端,即便是一个非常完美的东西,也可能是双刃剑,很难有那种放之四海而皆准的准则。比如一个业务模块内部,又是否可以集中定义的,我觉得是可以的。当里面如果有一个可以继续拆分独立出去的东西时,那么最好又不要跟整个业务集中处理了。

 所以,至于到底是不是绝对能不能集中管理,不能一概而论,需要看具体场景,需要看粒度是否合适,能带来什么好处,又可能面临什么问题,最终得出一个合乎当下的合理化方案,同时具备一定的前瞻性。这个过程就叫设计。

小工具零耦合

 工具、接口需要小而美。

 能独立提炼出来的东西,就尽可能设计成跟外部环境无依赖的独立模块,哪怕暂时是放在一起统一开发的,也就是一开始设计时就要将不合理依赖去除掉,俗称解耦。而不要等将来积重难返了,才想到重构。

 例如,在一些文字处理业务里面,我们要做文字排版与渲染功能,为了尽可能高效,可能会用到CoreText,但是,当去做的时候,发现CoreTex API的使用并没有那么简单,需要查资料,做实践,几乎就是要去预研一番,然后再在项目中写一个使用CoreText的类CoreTextRender,然而如果你没有低耦合的思想,为了方便,很可能在CoreTextRender中引入项目依赖,例如字体字号、文字颜色等,甚至你还可能在CoreTextRender中使用项目中另一个工具如StringUtility,而这个StringUtility本身又可能有很复杂的依赖,然后,你的CoreTextRender根本不具备可复用性,将来在其他地方想使用了,才发现无法复用。从某种意义上来说,CoreTextRender就是一个文字渲染的SDK,只是我们没有把它做成SDK的形式而已,但是从依赖关系上来说,需要按照SDK的标准来设计。

 当有一天,你发现另一个同学也在使用CoreText做东西,然后你对他说,我已经做过了,你拿去用就是了,结果他把你的代码拿过去,发现有一个编译错误,是由于你依赖了项目中一个东西,然后他又把那个东西拿过去,然后发现出现了N个编译错误。然后他说:算了,我还是自己重新写吧,是不是很尴尬。可以想想,在我们的项目中是否存在大量这样的现象。当你费尽心思写好一个独立模块后,突然一天发现不知道谁在你的代码里面引入了一个项目依赖,而他添加的功能对你这个模块来说,价值并不大。这个时候,你是否有砍人的冲动呢,这就是我前面一篇文章中提到的,修改一个模块前,首先找作者或模块负责人沟通讨论的原因。

 就算有时需要妥协,做不到零耦合,也要尽量做到低耦合、高内聚。

 如果我们将项目中的有复用价值的东西都按照SDK的标准来设计,或者真的将他们都做成SDK的话,那么将来需要重构的可能就会很小,即便需要,也很容易做,不会让人一提到重构就出现恐惧的感觉。将来要将这些类SDK重新组装成一个新的产品也会非常容易,那些类SDK的内部代码,几乎不需要修改。

原创文章,转载请注明出处

     

转载于:https://juejin.im/post/5cd652e7e51d45473d10fef1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值