抛开性能、并发、一致性等技术因素,好的业务代码应当如一篇显浅易懂的业务叙实文章,满足以下几个基本条件:
因此,好代码如同好文章,它应该是饱含业务语义(词要达意)、具有自明性和可读性(结构清晰),能够显性化表达业务意图(紧扣主题),让人赏心悦目。
好的代码,从好的命名开始,做到名副其实。
变量名是名词,要正确和清晰地描述业务语义,如果一个变量需要通过注释补充说明,那可能就是没取好变量名。
变量命名的关键点:
1、词要达意:避免无业务语义的命名,如:list、val、a…;
2、语境范围:避免小范围词套大范围数据,反之亦然,不使用过于宽泛的名词。
3、名词复数:统一风格,加s或List尾缀,变量名建议使用s尾缀,函数名建议使用List尾缀。
4、后置限定词:限定词是对前面变量名的修饰,可以描述名词的作用范围属性,例如:
Bad case:
Good case:
函数命名要体现做什么,而不是怎么做,要清楚表达出操作意图和业务语义。
函数命名的关键点:
Bad Case:
Good Case:
类是面向对象中最重要的概念,是一组关联数据的相关操作的封装,通常可以把类分为两种:
函数命名的关键点:
包(package)是一组强关联(内聚)的类的集合,起分类收纳和命名空间的作用。
实际工程中,常见的分类维度主要是两种,按功能性或业务域分类。
同一层级的包,要严格保持分类维度的一致性,要么先按业务域分类,再按功能性分类;要么就先按功能性分类,再按业务域分类。
有时候,优雅的实现仅仅是一个函数,不是一个类,不是一个框架,只是一个函数。 —— John Carmack
遵循金字塔原则,把函数层层递进的调用,理解成结论先行,自上而下的表达过程。
同层函数是对上一层的支撑,同层间要符合MECE法则,应描述和处理同一逻辑范畴的事情,高层抽象和底层细节不能杂糅在一起,否则会变得凌乱和难以理解。
MECE是(Mutually Exclusive Collectively Exhaustive)的缩写,指的是“相互独立,完全穷尽”的分类原则。通过MECE方法对问题进行分类,能做到清晰准确,从而容易找到答案。
分包的建议:
例如:
软件设计的目标是高内聚、低耦合。如果代码是高耦合和低内聚的,就会出现修改一个逻辑,多处代码要修改,可能影响到多个业务链路,增加了出bug的业务风险,同时扩大了测试回归的范围,导致研发成本增加。
耦合和内聚,是我们常挂在嘴边的话,但是大家经常说不太清楚,讲不太明白,很难衡量:
耦合是描述模块(系统/模块/类/函数)之间相互联系(控制/调用/数据传递)紧密程度的一种度量。
如果两个模块之间没有直接关系,它们之间的联系完全是通过主模块控制调用来实现的,这就是非直接耦合,这种耦合的模块独立性最强。
如果一个模块访问另一个模块时,彼此之间是通过数据参数(不是控制参数、公共数据结构或外部变量)来交换输入、输出信息的,则称这种耦合为数据耦合,它是较好的耦合形式。
当模块之间使用复合数据结构进行通信时,就会发生印记耦合。
复合数据结构可以是数组、类、结构体、联合体等的引用,通过复合数据结构在模块之间传递的参数,可能会或不会被接收模块完全使用。
印记耦合优点:把模块A的引用一把传递给模块B,模块B只需要接受少量参数,接口说明简单。
印记耦合缺点:
印记耦合优化:增加入参数类型,进传入模块需要的必要数据,如下:
如果一个模块通过传送开关、标志等控制信息,明显地控制选择另一模块的功能,就是控制耦合。
外部耦合,是指多个模块同时依赖同一个外部因素(IO设备/文件/协议/DB等),如上图所示:
外部耦合与与外部设备的通信有关,而不是与公共数据或数据流有关。
一个模块对外部数据或通信协议所做的任何更改都会影响其他模块,可以通过增加中间模块隔离外部变化来降低耦合度,如下:
共用耦合是指不同的模块共享全局数据的信息(全局数据结构、共享的通信区、内存的公共覆盖区)。
共用耦合的问题:
内容耦合在低级语言(汇编)中出现,高级语言从设计上已避免出现内容耦合。
如果发生下列情形,两个模块之间就发生了内容耦合:
内聚,是描述一个模块内各元素彼此结合的紧密程度,是从功能角度来度量模块内的联系。
通常,解决了耦合的问题,就解决了内聚的问题,反之亦然。
偶然内聚,一个模块内的各元素之间没有任何联系,仅是恰好放在同一个模块内,业务的“Util/Helper”类有大量例子。
逻辑内聚,把几种相关的功能组合在一起,由调用方传入的参数来确定具体执行哪一种功能。
逻辑内聚是一种“低内聚”,某程度上对应了“控制耦合”,它把内部的逻辑处理暴露给了接口之外,当内部逻辑发生变更时,原本无辜的调用方也会受牵连改动。
时间内聚,指一个模块内的组件除了在同一时间都会被执行外,相互之间没有任何关联。
过程内聚,指一个模块内的组件以特定次序被执行,但相互之间没有数据传递。
通信内聚,指一个模块内的组件以特定次序被执行,且相互之间传递和操作相同的数据。
顺序内聚,指一个模块内的元素以特定次序被执行,且上一步的输出被下一元素所依赖。
功能内聚,指一个模块内所有组件属于一个整体,完成同一个不可切分的功能,彼此缺一不可。
设计原则,是指导我们如何设计出低耦合、高内聚的代码,让代码能够更好的应对变化,从而降本提效。
设计原则的关键,是从使用方的角度看提供方的设计,一句话概括就是:请不要要我知道太多,你可以改,但请不要影响我。
定义:一个函数/类只能因为一个理由被修改。
单一职责原则,是所有原则中看起来最容易理解的,但是真正做到并不简单。因为遵循这一原则最关键是职责的划分。
职责的划分至少要回答两个基本问题:
且不说写代码,工作中我们也会出现人人不管或相争的重叠地带,划分清楚职责看起容易,实际很难。
定义:对扩展开放,对修改关闭(不修改代码就可以增加新功能)。
要理解开闭原则,关键是要理解定义中隐含着的两个主语,“使用方”和“提供方”,即:
提供方可以修改,增加新的功能特性,但是使用方不需要被修改,即可享用新的功能特征。
开闭原则广泛的理解,可以指导类、模块、系统的设计,满足该原则的核心设计方法是:通过协议(接口)交互。
定义:所有引用父类的地方,必须能透明的使用它的子类对象,指导类继承的设计。
面向对象的继承特性,一方面,子类可以拥有父类的属性和方法,提高了代码的复用性;另一方面,继承是有入侵性的,父类对子类有约束,子类必须拥有父类全部的属性和方法,修改父类会影响子类,增加了耦合性。
里氏替换原则是对继承进行了约束,体现在以下方面:
定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,目的是降低层与层之间的耦合。
从倒置来看,该原则可以有更泛化的理解:
举个购物车的例子:
定义:客户端不应该被强迫去依赖它并不需要的接口。
理解接口隔离原则,需要拿单一职责的原则做对比。细品一下,如果一个接口满足了单一职责,是否就也就满足接口隔离原则?
简单来讲,接口隔离原则解决的问题是,当某些类本身或面向使用方不满足职责单一原则时,客户端不应该直接使用它们,而是通过增加接口类,通过它隐藏客户端不需要感知到的部分。
编程范式,本质是一种思维方式,而和具体语言没关系。
用C语言可以写出面向对象的程序,用Java语言可以写出面向过程的程序。而不争的现实是,我们大部分人是在用java写面向过程的代码。
例如下面代码,它是如何用面向过程语言实现封装、继承、多态的?
备注:以上代码来自开源libevent库
最早使用机器和汇编语言编程,是编排好一堆命令让机器逐条执行,为了控制一些跳跃的流程(如if/for/continue/break),就会用到类似goto的语句,让程序直接跳转到希望执行的指令位置,这样程序员就拥有了直接转移程序控制权的能力。goto的无条件转移,使得程序的控制流难于追踪,程序难以修改和维护。
后来大家出了一套流程结构化的定律:任何程序都可以用顺序、选择、循环三种基本控制结构来表示。
因此,结构化编程的本质,是对程序控制权的直接转移进行了规范和限制。
结构化编程思维,比较靠近机器运行的思维,当程序越来越复杂的时候,大家发现简单靠结构化思维编程,很难构建起一个庞大的应用。而在编码过程中,大家不知不觉的把一些数据和逻辑封装了起来,形成一个个可复用的组件。慢慢大家出了一套符合人类理解客观世界的编程范式:利用事物的信息建模概念,如实体、关系、属性等,同时运用封装、继承、多态等机制来构造模拟现实系统的方法。
封装、继承、多态是面向对象的三大特征,三者的关系是层层递进的,而多态实际是规范了程序控制权的间接转移,在面向对象编程之前,大家是通过函数指针来解耦不同组件的函数实现,这种方式需要工程师严格遵守约定初始化函数指针,是非常脆弱的。
因此,面向对象编程的本质,是规范了数据和行为的封装,同时限制了程序控制权的间接转移。
函数式思维,是一种数学思维,把一个问题分解为一系列函数。函数式编程有多种定义,但是从根本上来看,它的核心是“纯函数”和“引用透明”:
若要做到以上两点,就需要对赋值进行限制,即变量一旦初始化就不可以再修改。
因此,函数式编程的本质,是规范了函数(一等公民/高阶函数/声明式/闭包等),同时限制了赋值行为。
编程范式的本质,更多是告诉我们不能做什么,并且通过规范来约束我们的行为。
灵魂拷问一下:
我当前表浅的理解是:
三种编程范式没有好坏之分,核心是思维方式的区别,针对不同的问题和场景,如何选择适当的方式来思考和解决问题,才是我们理解它们的关键。
表模式关注的数据库的表,它先考虑数据库表需要管理,然后添加对数据增删改查的操作。封装是面向对象的关键特征之一,把数据和操作数据的行为绑定在一起,拥有一个标识符(类)来表示它两的集合,而表模式允许你把数据和行为放在一起,但是它没有一个标识符来标出它所代表的主体。
这种模式在PC时代很盛行,例如VB和.net等桌面应用开发框架上,但是在JAVA服务应用中也被我发现了,如下:
脚本,是指表演戏剧、拍摄电影等所依据的底本又或者书稿的底本。脚本可以说是故事的发展大纲,用以确定故事的发展方向。
事务脚本模式,关注点是事务的流程和步骤,是对事务流程和步骤的编排,是一种面向过程的组织和表达形式。
按照事务脚本模式编程,可以不需要任何面向对象的设计,其中任何逻辑都可以通过if/else/while等流程控制元素来表达。
事务脚本模式的优点是,门槛低容易上手,也符合人的直线直觉思维;它的缺点是,当业务逻辑复杂是,事务方法会快速膨胀,因为业务属性不明确和缺乏抽象,不好复用和扩展。该模式在服务端应用中很常见,从MVC时代开始,一般通过controller组织事务流程,常见的分层结构如下:
领域设计模式,是通过分析和发掘业务领域的概念,从中提炼和设计出具有数据和行为的对象模型(类),并建立模型之间的关系。
领域设计模式,需要建立一个完整的由对象模型组成的层,来对目标业务领域建模。业务是经常变化的,通常有会通过分层的模式,让领域模型和系统其他部分保持最小的依赖。
至此,你会发现领域设计是DDD的底层思想,是面向对象的实践,更多请查阅“对象建模”和“领域驱动设计(DDD)”相关的材料和数据,这里不做展开。
不同的应用范式,是随着软件复杂度逐步提升演进出来的,不同模式面对和解决不同复杂度的问题,相互之间没有好坏之分。当问题比较简单时,使用事务脚本模式足够应付,反倒使用领域设计就过度设计,增加了不必要的复杂度,适得其反。
任何一个学科的学习,都要从基本概念、基本原理、基本方法入手,才能把握住问题的实质。
所谓,招式套路可以千变万化,扎实深厚的内功却始终如一。内功是基础和本源的东西,例如耦合和内聚,我们都知道低耦合高内聚好,但如何衡量代码的耦合和内聚?再如编程范式,我们都在使用面向对象语言,为什么看到的大多数是面向过程的代码?究其根本,是我们容易忽视基础和本源的东西,比如更关注设计模式,更关注架构设计,但上层的设计理念大多数是来自基础和本源的思想指引。
套用道家的一句话:道以明向,法以立本,术以立策,器以成事。
从代码的角度来看:
从代码的角度来看它们的关系:
关于如何写好代码,描述如有不当之处,请大家帮忙指正。最后,用一句话与大家共勉:万丈高楼平地起,勿在浮沙筑高台。
《control-coupling》
《sequential-cohesion》