无论新手还是资深开发者都会经常问一个问题,“怎么写好的代码?”,要知道怎么写好代码,首先我们要知道怎么样才是好的代码。要有明确的目标,才能知道如何达成目标。在《程序员修炼之道》中提到的“ETC Principle” -- 易于改编原则
。这个原则看似简单,但是我们越是深入思考越是觉得“简约而不简单”。
这篇文章里会详细解刨在实际产品研发中“易于改编”的原因和怎么做到“易于改编”, 从而让我们编写出更好的代码。
程序为何需要“易于改编”
为何代码必须要易于改编?因为一个系统是会随着一个产品的发展,每日有用户增长就会有一直做不完的需求。只要公司一直在运营着这个产品,需求就会随着公司的发展而改变。只要我们开发者一直与时并进专研新技术,我们就需要一直升级优化。
只有了解清楚一个系统在一个生命周期中,具体什么会推动我们程序改变,从中我们才会更深刻明白为什么我们的代码需要”易于改编“。
无论我们是研发任何系统,产品需求都是会一直变的。这个是永恒不变的命运。为什么呢?
产品方向
--- 随着产品的营销,运营,发展会推动产品需求一直新增,修改,优化。使用量
--- 随着产品的用户量级,数据量级,并发量级也会推动程序的架构和策略上的变动。技术升级优化
--- 甚至是我们使用的语言,框架,依赖包等升级也会引起我们的代码需要适应。技术债
--- 可能是因为时间的限制,之前的代码重于实现而质量不佳。所以我们的代码会随着岁月的流逝一直在迭代升级优化。
近几年很多技术团队启用了敏捷迭代开发
模式。什么是敏捷迭代呢?
敏捷迭代就是把开发周期缩短到1-4周。小步快跑的迅速迭代交付功能上线。敏捷迭代的流程分别如下:
你想想如果一个APP或者系统,几个月甚至一年才更新一次功能和升级。我们用起来其实很枯燥的,甚至我们会发现很多问题,还有很多功能可以便捷或者提升我们的使用体验。但是这么久才更新一次,我们还会对这个产品抱有希望吗?(除了微信这种已经很成熟的应用,但是就算是微信也是有持续更新的)。
所以一个好的产品,是需要快速迭代,小步快跑的迅速迭代交付功能上线的。也是因为这样,功能就需要持续更新、升级和优化。自然我们研发的代码就需要一直随着产品的变化而改编。而且还是每1-4周就会升级优化一次。
?小总结一下:
如何做到“易于改编
我们深刻懂得为什么系统会一直在改变,那我们就要知道怎么写代码才能让一个程序“易于改编”,然而在敏捷迭代中才能快速的响应需求的变化。如果想让我们编写的程序更容易的响应需求改变、业务改变和逻辑改变等,我们就要充分的给我们的程序解刨逻辑。
说到逻辑与业务的分解,首先要根据需求和功能深入思考分析,然后对其进行一个架构的设计。最常用的方式就是把系统模块化,组件化等的系统架构设计。
模块设计,就是以功能块为单位进行程序设计,实现其求解算法的方法称为模块化。模块化的目的是为了降低程序复杂度,使程序设计、调试和维护等操作简单化。
不论是前端开发还是后端开发,我们都有模块化和组件设计模式。使用模块设计来分解我们的功能和逻辑,目的是为了降低程序的复杂度、利于调试、维护、修改和新增功能。
比如现在我们要做个CMS(内容管理系统),我们一起来尝试使用模块设计来分解这个系统的功能。
首先我们要理解一个内容管理系统有哪些功能,然后把每个功能划入各个模块里。但是很多童鞋一开始接触一个系统,然后开始瓜分模块会觉得无从入手,可能花了半天坐在电脑前思考?,但是半天都吐不出一个所以然来。接下来让我们一起来学习一套逻辑思维,让我们以后更轻松架构一套模块设计吧!
一开始先思考这个系统的目的和使用场景,这个系统是用来做什么的?
一个内容管理系统,一般来说都是用来发发文章,新闻,或者是一个官方网站的内容管理。那必定就有文章。那管理文章内容,需要什么功能呢?
文章模块 「Article 模块」
文章子模块 --- 分类 「Article Category 模块」
那这些与文章相关的功能是不是可以统一放在“Article”
模块中统一管理,然后文章的模块中还有一个文章分类的子模块
叫做“Category”
。
有文章了必定就需要有作者,那作者在系统中其实是一个用户。那我们就需要有用户模块了。
加上一个管理系统,必定就有管理员,作者,甚至是会员。走一波这个逻辑我们就发现应该要有以下的功能点。
用户模块 「User 模块」
这么一来我们就可以建立一个单独的User模块
。这个模块主要是管理用户相关的信息和功能。
看到这里我们应该对一个系统的模块构思有一点的概念了。这个时候产品经理过来给我们提了一个需求,“我们现在要在这个系统添加一个标签体系,专门用来管理文章标签的。”。
那童鞋们,你们觉得这个需求应该放入那个模块呢?? ....
你们答对了!?这个是属于文章的一个子模块,Tag模块
--- 专门管理文章的标签,然后和每一篇文章有多对多关系的。所以标签模块
归纳入文章模块中。如果我们的内容管理系统做的很大,里面有视频内容,图文文章等等。我们可以在一开始就把这些统一归纳入“内容模块”,也就是Content
模块中。
说到了这里前端的童鞋估计要举手咯?♂️,前端的我们求关注呀!“前端是以页面和交互为单位,不可能和后端一样按功能逻辑来分解模块吧?” --- 这个童鞋说的在理哈。其实前端和后端的设计上是有稍微的不一样的。
后端会以业务逻辑来分解模块,但是前端有页面和数据逻辑两块的代码。所以前端相对比后端就要分开两种模块分解思路了。
组件设计
来分解了。Vue
的状态管理Vuex
的时候,需要用到模块管理
来分解逻辑,使后面维护和修改更容易。解耦,就是把复杂繁琐的逻辑拆分成更小的逻辑块。从而让复杂的逻辑分解成小的逻辑处理,使得逻辑变得更简化,更易于调试和维护。
在一个功能众多、业务复杂和系统模块繁多的系统中,每一个模块里面的代码也会开始变得臃肿,越来越难调试、维护和管理。其实模块化和解耦是一致的。模块化也是为了解耦你的程序。这里我们重点讲的是模块之间和逻辑之间的解耦(Decouping)。
我分享一个经历让大家深刻认知到解耦的重要性。我遇到过最夸张的有一段逻辑处理写了上5000行代码的童鞋,然而更可怕的是,在相同功能的地方那5000行代码被复制粘贴过来了。?我滴乖乖,这位童鞋在研发小组中有个花名叫“复制兄”。不过得到大家的帮忙和提点下,后面他也成为了这个小组中的一名优秀的程序员。
如果我们不懂得解耦代码,编写的代码会给我们后面带来很重的“技术债”。假设一下,你的5000行处理逻辑,在上数十个地方使用了。我们要改一下这段逻辑就难过登天了。就算是这段逻辑没有复用性,但当你需要回头去修改这段逻辑也是会让你头皮发麻,无从入手。修改一点这个逻辑都可能会导致出现10个bug的后果。
我们深刻知道解耦的重要性,那么我们应该怎么去高效解耦代码呢?
在《程序员修炼之道》中的 Design by Contract
里提到我们编写“害羞”的代码是很有益处的。“害羞”有两个含义:“不要把自己暴露给别人”和“不要与过多的人相互影响”。 这个是什么意思?我们用书中的例子来理解一下。
在一个庞大的间谍组织中,特工们会分到各个小组,每个小组内部的特工基本都互相认识,但是各个小组之间的特工就都互不相识。假设某个特工被俘虏了,一个小组可能会被摧毁,但是其他小组的特工是不会被暴露被影响的。因为各个小组之间的关系都是绝对隔离的。但是在任务中,各个小组之间都是会有合作和互相帮助,但是都互不相识。所以这么庞大的间谍组织才能长期安全存活下来。
这个种隔离模式用在编程中是非常好的。把我们的代码解耦到相对独立的模块和方法中,让它们之间的关联性和影响性降到最低。如果一个模块或者逻辑方法出了问题,我们可以独立重构或者修复,而不会给其他模块带来巨大的影响。只要最终的结果是一致的,就可以完美优化升级或者修复了。
在程序中,我们需要一个Service (服务)
给我们处理一个Object(对象)
,或者请求一个服务获得一个Object
,我们希望这个服务给到我们需要的结果,但是不需要我们去操心它是怎么处理与获得这个Object
的。这个服务或者方法是独立运行的,里面的逻辑和代码是与我们写的代码绝对隔离的。我们只需要在获得结果的时候验证这个结果的可用性就可以了,如果结果与我们需要的不一致,那我们就可以抛出错误。只要这个服务做对应的修正,就可以继续运行了。
理论我们解说的差不多了,现在我们来个实战例子吧:
案例:
假设现在我们需要写一个获取天气预报数据的类,获取天气预报数据首先你需要提供Geolocation 定位信息
参数。Geolocation
对象中含有一个地址对象。里面有经纬度,省市区等数据。我们需要获取到地址中的经纬度才能得到精准定点的天气预告信息。我们的代码会这么写:
/**
* 获取天气方法
*/
public function getWeather(Geolocation $geolocation) {
// 假设我们已经封装了一个获取定位的天气的方法叫getWeatherByGeo()
return $this->getWeatherByGeo($geolocation->getLocation()->getLat());
}
getLocation
方法获取到定位对象里面的地址对象getLat()
方法获取到定位地址的经纬度信息以上例子中,因为我们需要在geolocation
对象中取到经纬度,所以我们需要先经过获取地址对象,然后再通过这个对象获取到经纬度。其实这里面有不需要的关联关系。无论是写服务,还是写对象方法,我们都不要让使用这个服务/对象的开发者去过度的理解和使用你关联性很强的内部方法。这样会导致如果我们那天改变了这个关联性,多处都需要修改代码。
如果那天刘某改了Geolocation
对象,里面不再含有Location
对象,而且也没有了getLocation()
方法,经纬度可以直接在Geolocation
对象中直接取得。这个时候所有之前运用这个对象的其他人都需要修改代码了。很多时候开发者很难修改代码,或者一改动就会伤筋动骨的,其实就是因为这种过多过度的关联性关系导致而为的。
所以作为Geolocation
对象的封装者,我们应该直接给到一个方法getLat()
,让调用这个对象的开发者直接能拿到所需要的信息:
/**
* 获取天气方法
*/
public function getWeather(Geolocation $geolocation) {
// 假设我们已经封装了一个获取定位的天气的方法叫getWeatherByGeo()
return $this->getWeatherByGeo($geolocation->getLat());
}
这样就剪断了刚刚对象中的强关联关系的缺陷。
服务定义:
角色
:服务是系统架构里面的业务处理层。作用
:主要是为了高度解耦和封装不同场景的业务和功能到对应的服务,然而达到高度中心化的业务代码。
人
是一个控制器
,现在拿到了一个衣服对象
的参数
,然后人拥有一个洗衣服
的方法
洗衣机服务
给到人去使用洗衣机
这个服务里面有很多不同洗衣服的方法
,但是其实具体洗衣机里面的每一个清洗方法人是不知道怎么实现的,人都是直接按照提供的功能直接使用。这样说是不是很好理解了?所以最简单的理解就是:
服务是用来封装业务逻辑代码,是一个独立的逻辑层,高度封装解耦后提供给控制器或者其他需要用到这个服务的地方使用的。
❌ 错误例子
把所有洗衣机的方法提供给人使用,那就等同于让人来决定所有洗衣机的参数和清洗步骤。当人放衣服到洗衣机后,要选择先加水,加多少水,然后清洗开始,清洗多久,再甩干等等。
光想想,洗个衣服还那么多的选项,还要想怎么样的洗衣顺序才是正确的!我太难了!洗个鸡腿哦!(ノ`□ ´)ノ⌒┻━┻
⭕️ 正确例子
洗衣机服务实现了很多不同的常用洗衣服的
模式
, 比如快速清洗,毛衣清洗,地毯清洗,风干,甩干等等。都是一些常用的功能。每个功能方法里面其实调用了很多洗衣机封装好的流程和方法。所以当人使用洗衣机时,根本就不需要知道这些功能是怎么实现的,只要知道自己要干嘛,洗衣机刚好也有这个模式,直接用就完事儿了。
(✧ᗜ✧)?哇!介么人性化的么!这种洗衣机给我来一打谢谢!
我写过一篇详细关于编写服务的文章《你真的懂怎么写服务层吗?》,有兴趣的童鞋可以前往查看哦。这里我就不详细解说了。
学懂编程第一法则助你写出更好的代码
这篇文章已经到达尾声了,到了这里我们已经深刻知道何为易于改编
原则,更懂得如何编写易于改编
的代码。其实在开发的过程中,我们还是需要先思考,后设计,再编写。根据所拿到的的功能需求,做好程序的架构设计,从而写出易于改编的程序。只有这样我们编写的代码才能越来越好,走上技术巅峰!
推荐阅读以下几篇文章,可以让助你成为出色的开发者。