代码精进之路:从码农到工匠
张建飞
前言
种一棵树最好的时间是十年前,其次是现在
管理者的一个很重要的使命就是帮助团队成长,包括制定规范和技术传承。
1.2 命名其实很难
命名为什么难呢?因为命名的过程本身就是一个抽象和思考的过程,在工作中,当我们不能给一个模块、一个对象、一个函数,甚至一个变量找到合适的名称的时候,往往说明我们对问题的理解还不够透彻,需要重新去挖掘问题的本质,对问题域进行重新分析和抽象,有时还要调整设计和重构代码。因此,好的命名是我们写出好代码的基础。
1.3 有意义的命名
变量名应该是名词,能够正确地描述业务,有表达力。
函数命名要具体,空泛的命名没有意义。
函数的命名要体现做什么,而不是怎么做。
命名应该提升抽象层次、体现业务语义。
1.4 保持一致性
[插图]
面列出一些常见的对仗词组:·add/remove·increment/decrement·open/close·begin/end·insert/delete·show/hide·create/destroy·lock/unlock·source/target·first/last·min/max·start/stop·get/set·next/previous·up/down·old/new
如果贯彻限定词后置的原则,我们就能收获一组非常优雅、具有对称性的变量命名,例如revenueTotal(总收入)、expenseTotal(总支出)、revenueAverage(平均收入)和expenseAverage(平均支出)。
统一语言就是要确保团队在内部的所有交流、模型、代码和文档中都要使用同一种编程语言。实际上,统一语言(Ubiquitous Language)也是领域驱动设计(Domain Driven Design,DDD)中的重要概念
有些技术语言是通用的,业内人士都能理解,我们应该尽量使用这些术语来进行命名。
1.5 自明的代码
“好的代码是最好的文档”。也就是说,代码若要具备文档的功能,前提必须是其本身要具备很好的可读性和自明性。所谓自明性,就是在不借助其他辅助手段的情况下,代码本身就能向读者清晰地传达自身的含义。
在进行EDM(邮件营销)时要根据一些规则过滤掉一些客户,比如没有邮箱地址的客户、没有订阅关系不能发送邮件的客户、3天内不能重复发送邮件的客户等。
小心注释
如果注释是为了阐述代码背后的意图,那么这个注释是有用的;如果注释是为了复述代码功能,那么就要小心了,这样的注释往往意味着“坏味道”(在Martin Fowler的《重构:改善既有代码的设计》一书中,注释就是“坏味道”之一),是为了弥补我们代码表达能力的不足。就像Brian W.Kernighan说的那样:“别给糟糕的代码加注释——重新写吧。”
1.不要复述功能
2.要解释背后意图
第2章 规范
复杂系统的前沿科学家Mitchell Waldrop在《复杂》一书中,提出一种用信息熵来进行复杂性度量的方法。所谓信息熵,就是一条信息的信息量大小和它的不确定性之间的关系。举个例子,假设消息由符号A、C、G和T组成,如果序列高度有序,例如“A A A A A A A … A”,则熵为零。而完全随机的序列,例如“G A T A C G A … A”,熵值达到最大。由此可见,事物的复杂程度在很大程度上取决于其有序程度,减少无序能在一定程度上降低复杂度,这正是规范的价值所在。通过规范,把无序的混沌控制在一个能够理解的范围内,从而帮助我们减少认知成本,降低对事物认知的复杂度。
2.3 代码规范
对于同样的内容,由空行分出小段落比大段文字具有更好的可读性。这让我不禁想起《道德经》中的“三十幅共一毂,当其无,有车之用”,意思是说正是因为有了车轮毂和车轴之间的空白,车轮能够转起来,这正是“无”的价值啊。
一个简单的原则就是将概念相关的代码放在一起:相关性越强,彼此之间的距离应该越短。
Java中,我们通常使用如下命名约定。·类名采用“大驼峰”形式,即首字母大写的驼峰,例如Object、StringBuffer、FileInputStream。·方法名采用“小驼峰”形式,即首字母小写的驼峰,方法名一般为动词,与参数组成动宾结构,例如Thread的sleep(long millis)、StringBuffer的append(String str)。·常量命名的字母全部大写,单词之间用下划线连接,例如TOTAL_COUNT、PAGE_SIZE等。·枚举类以Enum或Type结尾,枚举类成员名称需要全大写,单词间用下划线连接,例如SexEnum.MALE、SexEnum.FEMALE。·抽象类名使用Abstract开头;异常类使用Exception结尾;实现类以impl结尾;测试类以它要测试的类名开始,以Test结尾。·包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词,包名统一使用单数形式。通常以com或org开头,加上公司名,再加上组件或者功能模块名,例如org.springframework.beans。
1.ERROR级别ERROR表示不能自己恢复的错误,需要立即被关注和解决。
2.WARN级别对于可预知的业务问题,最好不要用ERROR输出日志,以免污染报警系统。例如,参数校验不通过、没有访问权限等业务异常,就不应该用ERROR输出。需要注意的是,在短时间内产生过多的WARN日志,也是一种系统不健康的表现。因此,我们有必要为WARN配置一个适当阈值的报警,比如访问受限WARN超过100次/分,则发出报警。这样在WARN日志过于频繁时,我们能及时收到系统报警,去跟进用户问题。例如,如果是产品设计上有缺陷导致用户频繁出现操作卡点,可以考虑做一下流程或者产品上的优化。3.INFO级别INFO用于记录系统的基本运行过程和运行状态。
try { //业务处理 Response res = process(request); } catch (BizException e) { //业务异常使用WARN级别 logger.warn("BizException with error code:{},error message:{}", e.getErrorCode(), e.getErrorMsg()); } catch (SysException ex) { //系统异常使用ERROR级别 log.error("System error" + ex.getMessage(), ex); } catch (Exception ex) { //兜底 log.error("System error" + ex.getMessage(), ex); }
千万不要在业务处理内部到处使用try/catch打印错误日志,这样会使功能代码和业务代码缠绕在一起,让代码显得很凌乱,并且影响代码的可读性。
显性化的错误码具有更强的灵活性,适合敏捷开发。例如,我们可以将错误码定义成3个部分:类型+场景+自定义标识。每个部分之间用下划线连接,内容以大驼峰的方式书写。
对于错误类型,我们可以做一个约定:P代表参数异常(ParamException)、B代表业务异常(BizException)、S代表系统异常(SystemException)。
2.4 埋点规范
“业务数据化、数据业务化”,即业务要沉淀数据、数据要反哺业务。
[插图]图2-3 数据处理的5个阶段
在阿里巴巴有一个超级位置模型(Super Position Model,SPM)的埋点规范,用于统计分析各种场景的用户行为数据。
spm=2014.123456789.1.2叫作SPM编码,是用于跟踪页面模块位置的编码,标准SPM编码由4段组成,采用a.b.c.d的格式。·a代表站点类型,对于xTao合作伙伴(外站),a为固定值,a=2014。·b代表外站ID(即外站所使用的TOP appkey),比如你的站点使用的TOP appkey=123456789,则b=123456789。·c代表b站点上的频道ID,比如外站某个团购频道、某个逛街频道、某个试用频道等。·d代表c频道上的页面ID,比如某个团购详情页、某个宝贝详情页、某个试用详情页等。
2.6 防止破窗
环境中的不良现象如果被放任存在,就会诱使人们仿效,甚至变本加厉。以一幢有少许破窗的建筑为例,如果破窗不被修理好,可能将会有破坏者破坏更多的窗户。最终,他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦而没有被清洗掉,那么很快,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的“破窗效应”。
“第一扇破窗”常常是事情恶化的起点。从“破窗效应”中我们可以得到这样一个道理:任何一种已存在的不良现象都在传递着一种信息,会导致不良现象无限扩展,同时必须高度警觉那些看起来是偶然的、个别的、轻微的“过错”,如果对“过错”不闻不问、熟视无睹、反应迟钝或纠正不力,就会纵容更多的人“去打烂更多的窗户”,极有可能演变成“千里之堤,溃于蚁穴”的恶果。
2.7 本章小结
代码审查(Code Review)机制
第3章 函数
把简单的事情做到极致,功到自然成,最终“止于至善”。——秋山利辉《匠人精神》
3.5 短小的函数
函数的第一规则是要短小,第二规则是要更短小。维护过遗留系统、受过超长函数折磨的读者应该深有体会,相比于3000行代码的“庞然大物”,肯定是更短小的函数更易于理解和维护。
有时保持代码的逻辑不变,只是把长方法改成多个短方法,代码的可读性就能提高很多。超长方法是典型的代码“坏味道”,对超长方法的结构化分解是提升代码可读性最有效的方式之一。
3.6 职责单一
一个方法只做一件事情,也就是函数级别的单一职责原则(Single Responsibility Principle,SRP)。
遵循SRP不仅可以提升代码的可读性,还能提升代码的可复用性。因为职责越单一,功能越内聚,就越有可能被复用,这和代码的行数没有直接的关联性,但是有间接的关联性。
3.7 精简辅助代码
所谓的辅助代码(Assistant Code),是程序运行中必不可少的代码,但又不是处理业务逻辑的核心代码,比如判空、打印日志、鉴权、降级和缓存检查等。这些代码往往会在多个函数中重复冗余,减少辅助代码可以让代码显得更加干净整洁,易于维护。
利用Hystrix提供的API,我们可以使用注解的方式定义降级服务,从而不用在业务逻辑里面使用try/catch来做异常情况下的服务降级。一个典型的Hystrix的服务降级代码如下所示:public class UserService { @Autowired private RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "defaultUser") public User getUserById(Long id){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class, id); } //在远程服务不可用时,使用降级方法:defaultUser public User defaultUser(){ return new User(); }}
3.9 SLAP
抽象层次一致性(Single Level of Abstration Principle,SLAP),是和组合函数密切相关的一个原则。组合函数要求将一个大函数拆成多个子函数的组合,而SLAP要求函数体中的内容必须在同一个抽象层次上。
3.10 函数式编程
函数式编程中最重要的特征之一,就是你可以把函数(你的代码)作为参数传递给另一个函数。为什么这个功能很重要呢?主要有以下两个原因。·减少冗余代码,让代码更简洁、可读性更好。·函数是“无副作用”的,即没有对共享的可变数据操作,可以利用多核并行处理,而不用担心线程安全问题。
3.11 本章小结
一个系统容易腐化的部分正是函数,不解决函数的复杂性,就很难解决系统的复杂性。
第4章 设计原则
每个人都有义务捍卫、遵守或完善原则。原则可以修正,但是不能肆意妄为。——瑞·达利欧《原则》
4.1 SOLID概览
SOLID是5个设计原则开头字母的缩写,其本身就有“稳定的”的意思,寓意是“遵从SOLID原则可以建立稳定、灵活、健壮的系统”。5个原则分别如下。·Single Responsibility Principle(SRP):单一职责原则。·Open Close Principle(OCP):开闭原则。·Liskov Substitution Principle(LSP):里氏替换原则。·Interface Segregation Principle(ISP):接口隔离原则。·Dependency Inversion Principle(DIP):依赖倒置原则。
4.3 OCP
OCP软件实体应该对扩展开放,对修改关闭。
在面向对象设计中,我们通常通过继承和多态来实现OCP,即封装不变部分。对于需要变化的部分,通过接口继承实现的方式来实现“开放”。因此,区别面向过程语言和面向对象语言最重要的标志就是看它是否支持多态。
装饰者模式,可以在不改变被装饰对象的情况下,通过包装(Wrap)一个新类来扩展功能;策略模式,通过制定一个策略接口,让不同的策略实现成为可能;适配器模式,在不改变原有类的基础上,让其适配(Adapt)新的功能;观察者模式,可以灵活地添加或删除观察者(Listener)来扩展系统的功能。
当然,要想做到绝对地“不修改”是比较理想主义的。因为业务是不确定的,没有谁可以预测到所有的扩展点,因此这里需要一定的权衡,如果提前做过多的“大设计”,可能会犯YAGNI(You Ain’t Gonna Need It)的错误。
4.5 ISP
ISP多个特定客户端接口要好于一个宽泛用途的接口。接口隔离原则认为不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。
4.6 DIP
模块之间交互应该依赖抽象,而非实现。DIP要求高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象。
依赖倒置,就是要反转依赖的方向,让原来紧耦合的依赖关系得以解耦,这样依赖方和被依赖方都有更高的灵活度。
4.7 DRY
DRY是Don’t Repeat Yourself的缩写,DRY原则特指在程序设计和计算中避免重复代码,因为这样会降低代码的灵活性和简洁性,并且可能导致代码之间的矛盾。
系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能。
4.8 YAGNI
YAGNI(You Ain’t Gonna Need It)的意思是“你不会需要它”
YAGNI是针对“大设计”(Big Design)提出来的,是“极限编程”提倡的原则,是指你自以为有用的功能,实际上都是用不到的。因此,除了核心的功能之外,其他的功能一概不要提前设计,这样可以大大加快开发进程。它背后的指导思想就是尽可能快、尽可能简单地让软件运行起来。
你会发现DRY原则和YAGNI原则是不兼容的。前者追求“抽象化”,要求找到通用的解决方法;后者追求“快和省”,意味着不要把精力放在抽象化上面,因为很可能“你不会需要它”。因此,就有了Rule of Three原则。
4.9 Rule of Three
Rule of Three也被称为“三次原则”,是指当某个功能第三次出现时,就有必要进行“抽象化”了。
三次原则指导我们可以通过以下步骤来写代码。(1)第一次用到某个功能时,写一个特定的解决方法。(2)第二次又用到的时候,复制上一次的代码。(3)第三次出现的时候,才着手“抽象化”,写出通用的解决方法。
软件设计本身就是一个平衡的艺术,我们既反对过度设计(Over Design),也绝对不赞成无设计(No Design)。
4.10 KISS原则
KISS(Keep It Simple and Stupid)
把事情变复杂很简单,把事情变简单很复杂。好的目标不是越复杂越好,反而是越简洁越好。
4.11 POLA原则
POLA(Principle of least astonishment)是最小惊奇原则,写代码不是写侦探小说,要的是简单易懂,而不是时不时冒出个“Surprise”。
5.2 GoF
[插图]
5.3 拦截器模式
拦截器模式(Interceptor Pattern),是指提供一种通用的扩展机制,可以在业务操作前后提供一些切面的(Cross-Cutting)的操作。这些切面操作通常是和业务无关的,比如日志记录、性能统计、安全控制、事务处理、异常处理和编码转换等。
[插图]
5.4 插件模式
插件(plug-in)模式扩展方式和普通的对象扩展方式的不同之处在于,普通的扩展发生在软件内部,插件式扩展发生在软件外部。
而插件式扩展是发生在软件外部的,新扩展以一个单独的组件(比如jar包)的方式加入软件中,软件本身不需要重新编译、打包。
5.5 管道模式
管道这个名字源于自来水厂的原水处理过程。原水要经过管道,一层层地过滤、沉淀、去杂质、消毒,到管道另一端形成纯净水。我们不应该把所有原水的过滤都放在一个管道中去提纯,而应该把处理过程进行划分,把不同的处理分配在不同的阀门上,第一道阀门调节什么,第二道调节什么……最后组合起来形成过滤纯净水的管道。
一个典型的管道模式,会涉及以下3个主要的角色。(1)阀门:处理数据的节点。(2)管道:组织各个阀门。(3)客户端:构造管道并调用。
[插图]图5-6 基于链表的管道
管道模式还有一个非常广泛的应用——流式处理,即把自来水厂的原水换成数据,形成数据流。管道模式适用于那些在一个数据流上要进行不同的数据计算场景,这种方式称为流处理,也称为流式计算。流是一系列数据项,一次只生成一项。程序可以从输入流中逐个读取数据项,然后以同样的方式将数据项写入数据流。一个程序的输出流很有可能是另一个程序的输入流。
第6章 模型
建模的艺术就是去除实在中与问题无关的部分。
6.1 什么是模型
。在处理问题时,我们最好隐藏那些不必要的细节,只专注于重要的方面,抓住问题的本质。这也是建模和抽象的价值所在。
6.4 领域模型
[插图]图6-16 软件开发的本质
领域模型在软件开发中的主要起到如下作用。·帮助分析理解复杂业务领域问题,描述业务中涉及的实体及其相互之间的关系,是需求分析的产物,与问题域相关。
·是需求分析人员与用户交流的有力工具,是彼此交流的语言。·分析如何满足系统功能性需求,指导项目后续的系统设计。
6.6 广义模型
UI流程图使用页面之间的流转来描述系统交互流程。用户可以通过UI流程图进行业务分析和检查,UI流程图也可以作为系统文档向新人介绍业务。
[插图]
[插图]图6-19 电商网站客户动线
7.3 数据驱动和领域驱动
数据驱动以数据库为中心,其中最重要的设计是数据模型,但随着业务的增长和项目的推进,软件开发和维护的难度会急剧增加。
[插图]图7-1 数据驱动研发过程
领域驱动设计关心的是业务中的领域划分(战略设计)和领域建模(战术设计),其开发过程不再以数据模型为起点,而是以领域模型为出发点
领域模型对应的是业务实体,在程序中主要表现为类、聚合根和值对象,它更加关注业务语义的显性化表达,而不是数据的存储和数据之间的关系。这是“领域驱动设计”和“数据驱动设计”之间显著的区别。
[插图]图7-4 领域驱动研发过程
7.4 DDD的优势
貌似对于新事物的出现,都会出现这种现象
排斥—接受—使用
排斥—接受—使用
从业务语言定义角度,关联人涉及到的不只是产品,还应有业务,开发,UI等相关人员
统一语言(Ubiquitous Language)的主要思想是让应用能和业务相匹配,这是通过在业务与代码中的技术之间采用共同的语言达成的。业务语言起源于公司的业务侧,业务侧拥有需要实现的概念。业务语言中的术语由公司的的业务侧和技术侧通过协商来定义(意味着业务侧也不能总是选到最好的命名),目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言。代码、类、方法、属性和模块的命名必须和统一语言相匹配,必要的时候需要对代码进行重构!
业务语言,在不统一、具有二义性等情况下,无形中加重了沟通成本,降低了效率
统一语言(Ubiquitous Language)的主要思想是让应用能和业务相匹配,这是通过在业务与代码中的技术之间采用共同的语言达成的。业务语言起源于公司的业务侧,业务侧拥有需要实现的概念。业务语言中的术语由公司的的业务侧和技术侧通过协商来定义(意味着业务侧也不能总是选到最好的命名),目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言。代码、类、方法、属性和模块的命名必须和统一语言相匹配,必要的时候需要对代码进行重构!
统一语言(Ubiquitous Language)的主要思想是让应用能和业务相匹配,这是通过在业务与代码中的技术之间采用共同的语言达成的。业务语言起源于公司的业务侧,业务侧拥有需要实现的概念。业务语言中的术语由公司的的业务侧和技术侧通过协商来定义(意味着业务侧也不能总是选到最好的命名),目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言。代码、类、方法、属性和模块的命名必须和统一语言相匹配,必要的时候需要对代码进行重构!
DDD的核心是领域模型,这一方法论可以通俗地理解为先找到业务中的领域模型,以领域模型为中心,驱动项目开发。领域模型的设计精髓在于面向对象分析、对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析的大师。DDD鼓励我们接触到需求后第一步就是考虑领域模型,而不是将其切割成数据和行为,然后用数据库实现数据,用服务实现行为,最后造成需求的首尾分离。DDD会让你首先考虑业务语言,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。重点不同,导致编程世界观不同。
,核心业务逻辑对技术细节没有任何依赖,依赖都是由外向内的,即使有由内向外的依赖,也应该通过依赖倒置来反转依赖的方向。
[插图]图7-8 业务逻辑和技术细节分离的架构
UI:UI只是一种I/O设备的呈现,Web、WAP和Wireless都是不同的I/O,我们的核心业务逻辑应该与如何呈现解耦,以及针对不同的端可以使用不同的适配器(Adaptor)去做适配。·框架:不要让框架侵入我们的核心业务代码,以Spring为例,最好不要在业务对象中到处写@autowired注解。业务对象不应该依赖框架。
7.5 DDD的核心概念
[插图]图7-9 现实世界与软件世界
聚合根(Aggregate Root)是DDD中的一个概念,是一种更大范围的封装,会把一组有相同生命周期、在业务上不可分割的实体和值对象放在一起,只有根实体可以对外暴露引用,这也是一种内聚性的表现。
事件是表示发生在过去的事情,所以在命名上推荐使用Domain Name +动词的过去式+ Event,这样可以更准确地表达业务语义。
事件内容在计算机术语中叫作payload,有以下两种形式。(1)自恰(Enrichment):就是在事件的payload中尽量多放数据,这样consumer不需要回查就能处理消息,也就是自恰地处理消息。(2)回查(Query-Back):这种方式是只在payload放置id属性,然后consumer通过回调的形式获取更多数据。这种形式会加重系统的负载,可能会引起性能问题。
7.6 领域建模方法
[插图]图7-15 四色模型
任何的业务事件都会以某种数据的形式留下足迹。我们对于事件的追溯可以通过对数据的追溯来完成。
为什么这些业务数据具备可追溯性(Tracibility)呢?因为这些数据都是关键业务流程执行的结果。
[插图]图7-16 在线电子书店的关键业务流程
这些问题都需要业务系统捕捉到相应的足迹才能够回答,所以企业的业务系统的主要目的之一,就是记录这些足迹,并将这些足迹形成一条有效的追溯链。
足迹通常都具有一个有意思的特性,即它们是Moment-interval(要么是“时间时刻”,要么是“时间段”)的。发现这些业务关键时刻对象就是建模的起点。对这些对象稍加整理,我们就能得到图7-17所示的整个领域模型的骨干。[插图]图7-17 在线电子书店的业务关键时刻对象
7.8 为什么DDD饱受争议
核心业务逻辑和技术细节相分离
第二部分 思想
若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。
8.2 到底什么是抽象
抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。
抽象就是简化事物,抓住事物本质的过程。
[插图]图8-1 实物牛和抽象牛的对比
8.4 抽象的层次性
对同一个对象的抽象是有不同层次的。层次越往上,抽象程度越高,它所包含的东西就越多,其含义越宽泛,忽略的细节也越多;层次越往下,抽象程度越低,它所包含的东西越少,细节越多。也就是我们常说的,内涵越小,外延越大;内涵越大,外延越小。不同层次的抽象有不同的用途。
“软件领域的任何问题,都可以通过增加一个间接的中间层来解决。”这种分层在某种意义上也是抽象的分层,每一层的抽象只关注本层相关的信息,对上层屏蔽复杂性,从而简化整个系统的设计。
8.5 如何进行抽象
抽象的过程就是合并同类项、归并分类和寻找共性的过程。也就是将有内在逻辑关系的事物放在一起,然后给这个分类进行命名,这个名字就代表了这组分类的抽象。
当我们发现有些东西无法归到一个类别中时,该怎么办呢?此时,我们可以通过上升一个抽象层次的方式,让它们在更高的抽象层次上产生逻辑关系。
在开发工作中,很多时候就需要通过抽象层次的提升来提高代码的可读性和通用性。
《金字塔原理》是一本教人如何进行结构化思考和表达的书,核心思想是通过归类分组搭建金字塔结构,这是一种非常有用的思维框架,让我们更加全面地思考,在表达观点时更加清晰。书中提到,要自下而上地思考,总结概括;自上而下地表达,结论先行。其中,自下而上总结概括的过程就是抽象的过程,构建金字塔的过程就是寻找逻辑关系、抽象概括的过程。经常锻炼用结构化的方式去处理问题,搭建自己的金字塔,可以帮助我们理清问题的脉络,提升抽象能力。金字塔结构让我们通过抽象概括将混乱无序的信息形成不同的抽象层次,从而便于理解和记忆,这是一个非常实用的方法论。
8.6 如何提升抽象思维
如何提升抽象思维
多阅读
多总结
小时候,我们可能不理解语文老师为什么总是要求我们总结段落大意、中心思想。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维的能力。做总结最好的方式就是写文章,小到博文,大到一本书,都是锻炼自己抽象思维和结构化思维的机会。记录也是很好的总结习惯。以读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳,这样不仅可以加深理解,还可以提升自己的抽象思维能力。
第9章 分治
分治的价值在于,我们不应该试着在同一时间把整个问题域都塞进自己的大脑,而应该试着以某种方式去组织问题,以便在一个时刻专注于一个特定的部分。这样做的目的是尽量降低在任意时间所要思考问题的复杂度。
让分治的理念融入我们的潜意识中,使我们在开发工作中灵活地使用分层、分场景和分步骤等解决办法,化解软件设计中的复杂问题,从而写出可读性更好的代码,开发出可维护性、可扩展性更好的系统。
9.1 分治算法
“分”就是递归地将原问题分解成小问题;“治”则是在解决了各个小问题(各个击破)之后合并小问题的解,从而得到整个问题的解。分治法解题的一般步骤如下。(1)分解:将要解决的问题划分成若干规模较小的同类问题。(2)求解:当子问题划分得足够小时,用较简单的方法解决。(3)合并:按原问题的要求,将子问题的解逐层合并,构成原问题的解。
9.2 函数分解
关于函数分解,在此强调以下两点。(1)函数长短是职责单一的充分不必要条件,也就是长函数往往意味着职责不单一,但是短函数也不一定就意味着职责单一。(2)在使用组合函数模式时,要注意抽象层次一致性原则(Single Level of Abstration Principle,SLAP),不同抽象层次的内容放在一起会给人凌乱、逻辑不协调的感觉。
9.3 写代码的两次创造
我们不仅要写实现功能的代码,还要写容易理解的好代码。“写出好代码”除了需要好的技艺之外,还要有好的方法论。以我的实践经验来看,优雅的代码很少是一次成形的,大部分情况下要经过两次创造:第一遍实现功能,第二遍重构优化。
最好的优化肯定不是等系统上线后再去做,因为这样往往就等于“再也不会去做”(later equals never)。优化工作本应该是我们编码工作的一部分,拆成两步,主要对编码效率上的考量。
9.5 分层设计
分层设计最大的好处是分离关注(Separation of concerns),这样我们就可以通过分层隔离简化一个复杂的问题,让每一层只对上一层负责,从而使每一层的职责变得相对简单。
9.6 横切和竖切
[插图]图9-5 数据库竖切
[插图]图9-6 数据库横切
10.1 不教条
软件的第一性原理是“控制软件复杂度”。
[插图]图10-1 改良的迭代开发
[插图]图10-4 中台架构
10.3 成长型思维
[插图]
10.4 结构化思维
什么是结构化思维呢?我给结构化思维的定义就是“逻辑+套路”。1.逻辑所谓逻辑,是指结构之间必须是有逻辑关系的。例如,你说话时用“第一、第二、第三”这个逻辑顺序是合理的,而如果用“第一、第二、第四”就会显得很奇怪。实际上,组织思想的逻辑只有4种。(1)演绎顺序:比如“大前提、小前提、结论”的演绎推理方式就是演绎顺序的。(2)时间(步骤)顺序:比如“第一、第二、第三”和“首先、再者、然后”等,大多数的时间顺序同时也是因果顺序。(3)空间(结构)顺序:比如“前端、后端、数据”和“波士顿、纽约、华盛顿”等,化整为零(将整体分解为部分)等都是空间顺序。在做空间分解时,要注意满足“相互独立,完全穷尽”(Mutually Exclusive Collectively Exhaustive,MECE)原则。(4)程度(重要性)顺序:比如“最重要、次重要、不重要”等。只要我们的思想和表达在这4种逻辑顺序之内,就是有逻辑的,否则就是没有逻辑的。2.套路套路是指我们解决问题的方法论、路径和经验。比如,5W2H分析法就是非常好的,是可以帮助我们分析问题的一个“套路”。试想一下,面对任何一个问题,你如果都能从“Why、Who、When、Where、What、How和How much”(如图10-6所示)这7个方面去思考,是不是比不知道这个方法论的人用点状模式思考要全面得多呢?[插图]图10-6 5W2H问题分析法
[插图]图10-7 落地新团队的策略
最清晰和实用的结构化表达是“提出问题,定义问题,分析问题,解决问题,最后展望未来”。如果按照这个逻辑顺序去阐述一件事情,会比不知道这个“套路”的效果好得多。这也是麦肯锡常用的解决问题的框架。
另一个有用的思维框架是“zoom in/zoom out”。我们说事情时,应该像电影镜头一样,先从远拉近,再由近拉远。“zoom in”是先从宏观背景开始,首先让大家知道你的事情发生的背景,为什么这件事情很重要?然后讲到具体细节,怎么做成的?解决了什么问题?后端思考是什么?最后“zoom out”,从细节调回到整体,结果是什么?带来的客户价值是什么?你对未来的思考是什么?
10.5 工具化思维
(1)最差的境界是“实在懒”,拖延不喜欢的任务。(2)其次是“开明懒”,迅速做完不喜欢的任务,以摆脱之。(3)最高的境界是“智慧懒”,编写某个工具来完成不喜欢的任务,以便再也不用做这样的事情了,从而一劳永逸。
在有效的工作中,最重要的是思考,而人在思考时通常看上去不会很忙。如果和我共事的程序员总是忙个不停,我会认为他并非优秀的程序员,因为他没用最有价值的工具——自己的大脑。
我经常在团队中说,每当你重复同样的工作3次以上,就应该停下来问问自己:我是不是可以通过自动化脚本、配置化,或者小工具来帮助自己提效?
[插图]图10-8 关于工作效率的漫画
10.8 有目标
“所有事物都要经过两次创造的原则,第一次为心智上的创造,第二次为实际的创造”。直观的表达就是:先想清楚目标,然后努力实现。
如果我们能够带着问题与目标去搜集相关的资料并学习,就会事半功倍。这种学习模式的效果会比碎片化阅读好得多。
10.9 选择的自由
[插图]图10-9 积极主动的选择模式
“处乱世而不惊,临虚空而不惧,喜迎阴晴圆缺,笑傲雨雪风霜”
11.1 技术氛围
特别是做业务开发的技术团队,如果管理者完全不关心技术细节,绩效完全和业务KPI绑定,就会导致工程师们整天只会写if-else的业务代码,得不到技术上的成长。在这样的技术团队,团队的战斗力和凝聚力都会每况愈下。
11.1.1 代码好坏味道
11.1.2 技术分享
11.1.3 CR周报代码审查(Code Review,CR)是保证代码质量和架构风格一致性的重要手段。
有Peer Review(点对点地审查),也有Group Review(团队成员一起审查)
11.1.4 读书会
关于读书会的运作,在此分享以下3点经验。(1)书的范围可以放宽一点,不要只局限在技术类书籍,毕竟除了技术,我们还有很多东西要学。例如,我们最近一次读书会选的书是《高效能人士的七个习惯》。(2)读书的方式,可以是同读一本书,也可以拆书,就是每个人分别读书的一章或者几章,然后互相分享书中的内容和读后感。拆书的效率更高,比较适合工具类的书。(3)读书会的举办频率可以灵活一些,工作任务紧的时候,频率适当放低,甚至暂停。
11.2 目标管理
一个好的Leader,应该是愿意花时间和下属一起讨论、制定目标的。在过程中,给予帮助和指导,及时对焦纠偏,确保目标的达成。这样做是对下属负责,也对自己负责,至少不至于在谈绩效时造成意外,出现管理事故。
目标管理的常见手段有关键绩效指标(Key Performance Index,KPI)和目标与关键成果(Objectives Key Results,OKR)两种方法。相比较而言,一味地追求KPI,可能会导致短视;OKR更注重短期利益和长期战略之间的平衡。
OKR主要有如下两个特点。(1)OKR可以不和绩效挂钩,主要强调沟通和方向。(2)OKR比KPI多了一个层级的概念,O(Objective)是要有野心的、有一定的模糊性,但是KR(Key Results)需要是可量化的,并且KR一定要为O服务,不能偏离O的方向。
S代表Specific,表示指标要具体;M代表Measurable,表示指标要可衡量;A代表Attainable,表示指标是有可能达成的;R代表Relevant,表示KR和O要有一定的相关性;最后,T代表Time bound,表示指标必须具有明确的截止期限。[插图]图11-3 目标设定的SMART原则
OKR中的目标必须是有野心的。因为只有高远的目标,才能最大程度地激发人的潜能。目标是否足够有野心也是区分OKR与KPI的一个标志,KPI拿100分的员工,OKR可能只有0.5分(OKR的得分是0~1分),这才是正常的结果,证明该员工的目标(O)比其他人的KPI要高很多。
OKR示例[插图]
11.3 技术规划
技术规划和目标管理有一点共同之处,都是要在技术团队中理清接下来要做的事情。不同之处在于,技术规划更多的是从团队视角去看接下来要做的事情,而目标管理是要把规划要做的事情进行拆解,和个人目标对齐。对于技术Leader而言,做好技术规划是非常重要的事情,一个团队有没有价值,最终还是要通过做出来的事情来体现。
技术规划是一个大命题。对待这种大问题,我们要分而治之,将其分解成几个不同层次的、相对较小的问题来看。如图11-4所示,我们可以从时间和重要性的维度,将其拆解成当前问题、技术领域、业务领域和团队特色4个层次的问题,然后分别定义问题、制定策略,这样就会清晰很多。[插图]图11-4 技术规划的4个层次
11.3.1 当前问题第一层问题解决是最直接的,主要看团队中现在有什么迫切、紧急的问题需要解决,有哪些坑要去填。例如,业务增长比较快、当前架构缺乏弹性、要做服务化拆分、加入分布式缓存、分表分库等。又如,因为代码质量(可读性、可维护性)差,要建立一个代码审查机制,提升代码质量。
11.3.2 技术领域技术领域要做的是在这些常规领域中,根据业务情况和团队情况选择一些领域和命题(比如稳定、性能、效率等),并在这些命题和方向中根据优先级做判断。比如,完善监控体系提升系统稳定性、使用CDN提升性能、通过测试自动化提升研发效率等。
11.3.3 业务领域让业务先赢是技术的首要使命,即使我们身处技术团队,也要充分理解业务、关注业务,要分析业务数据和发展趋势,和业务同事充分交流,总结和抽象出业务的发展对技术会提出什么诉求,需要技术做什么布局和建设以应对业务发展的需求。
11.3.4 团队特色作为技术团队,我们要对比团队内外技术的异同,最终圈定一个差异化区域。这块区域是团队的特色技术,是团队借外力之外要修内功的部分,是不依赖别人而主要靠自己突破的部分,是团队相比外面的差异化竞争力。这一层很重要,对团队的口碑、影响力和稳定性都有较大的影响;同时这一层又是最难的,很多技术团队在这一层次是空白的。
11.4 推理阶梯
在企业的日常运转中,管理者在对待员工时也会犯一些主观性的错误。例如,管理者要批评一个员工,前提是管理者认为员工做的事情是错误的,但是有没有可能管理者本身的认知就是错误的?因此,作为管理者,一定不要轻易对员工做推理,一些错误的推理如果不能及时被澄清,会激起员工的反感。要实事求是,尊重事实。
很多情况下,我们推理别人的“结论”让自己非常生气,但是后来发现事实并非如此,这源于我们习惯用自我推理而非沟通的方式来解决问题。这种推理也被称为“推理阶梯”
[插图]图11-5 推理阶梯
我们根本不是理性的人,很多决定都是在稀里糊涂的状态下做出的感性决定,崇尚理性思维的博弈论很少在实际生活中得到应用。
11.5 Leader和Manager的区别
Manager是管理事务,是控制和权威;而Leader是领导人心,是引领和激发。Leader要做一些Manager的管理事务,但是管理绝对不是Leader工作的全部。
我们不需要这么多‘高高在上’‘指点江山’的技术Manager,而是需要更多能真正深入系统里面,深入代码细节,给团队带来改变的技术Leader。
[插图]图11-6 Leader和Manager的区别
从阿里巴巴的组织角度来看,我们也在强调技术Leader要“重技术、轻管理”。比如,以前在技术栈是有P线和M线[插图]的,当你从个人贡献者(Individual Contributor,IC)晋升到管理岗时,可以选择M线。但是在2018年,组织上做了一个调整,在技术岗位取消M线,也就是不论你是不是带团队,都必须要在专业技术上过硬。
11.6 视人为人
我们在公司工作,实际上是在给两个账号存钱:一个是绩效货币(Performance Currency),这是对事的;另一个是关系货币(Relationship Currency),这是对人的。
·对待上级——有胆量。·对待平级——有肺腑。·对待下级——有心肝。
第三部分 实践
软件的首要技术使命:管理复杂度。
12.1 软件架构
架构始于建筑,这是人类发展(原始人自给自足住在树上,也就不需要架构)分工协作的需要。将目标系统按某个原则进行切分,切分的原则是便于不同的角色进行并行工作。
·数据架构:对于规模大一些的公司,数据治理是一个很重要的课题。如何对数据收集、处理,提供统一的服务和标准,是数据架构需要关注的问题。其目的就是统一数据定义规范,标准化数据表达,形成有效易维护的数据资产,搭建统一的大数据处理平台,形成数据使用闭环。
12.2 典型的应用架构
[插图]图12-2 N层应用架构
12.3 COLA架构设计
[插图]
[插图]
[插图]
12.4 COLA测试
[插图]
13.2 整理需求
技术度量表[插图]
13.4 使用COLA
https://github.com/alibaba/COLA,
13.6 核心业务逻辑
读者可以在GitHub中获取有关工匠平台的完整代码,网址为https://github.com/alibaba/COLA/tree/master/sample/craftsman。
点评