书名:代码精进之路
副标题:从码农到工匠
作者:张建飞
豆瓣评分:7.9
内容简介:这是一本为专业程序员而写的书,写好代码、追求卓越和工匠精神是每个程序员都应该具备的优秀品质。
本书共有13章内容,主要分为技艺部分、思想部分和实践部分。技艺部分详细介绍了编程技巧和方法论,并配以详尽的代码案例,有助于读者提高编写代码的能力,优化代码质量。思想部分主要包括抽象能力、分治思想,以及程序员应该具备的素养等内容。实践部分主要介绍了常见的应用架构模式,以及COLA架构的设计原理。

读完日期:20200726

前言

开发人员被夹在中间,像一只困兽,向左走,还是向右走?方向在哪里?我倍感困惑

Robert C. Martin说的:“不管你们有多敬业,加多少班,在面对烂系统时,你仍然会寸步难行,因为你大部分的精力不是在应对开发需求,而是在应对混乱

《人月神话》的作者Frederick P.Brooks.Jr曾说:“软件的复杂性是一个基本特征,而不是偶然如此。

教条和妥协。我们可能不得已在不恰当的场景使用了不恰当的解决方案,造成了不必要的复杂性。我们向自己妥协、向产品经理妥协、向工期妥协、向技术债妥协,总有很多借口把设计糟糕、混乱丑陋的代码发布上线。

切实可行的控制复杂度的办法,并沉淀了整洁面向对象分层架构(Clean Object-oriented and Layered Architecture,COLA)

这样就能避免很多不必要的焦虑,少做有缺陷的设计,少写丑陋的代码了

因为我相信对代码的极致追求是每个技术人员的基本动力和诉求。

我们都知道“写出好代码”是比“写出代码”要难得多的要求,一个程序员的“美德”就在于他是否能为后人留下一段看得懂、可维护性好的代码。

系统化的工程,不是看几本书、写几年代码就能轻松习得的,而需要我们对自己的思维习惯、学习方法和工程实践进行彻底的反省和重构。

一个普通码农如何通过认知升级、知识重构、持续学习,继而转向工匠的过程。

以期给同样在路上的你带来一些启发,缩短你“从码农到工匠”的探索路径。

灵活性和没有银弹(Silver Bullet),也是软件行业的有趣之处。

知识储备、判断力和思辨力是软件行业给我们提出的更高

软件设计不仅是“技术”(Technique),更是一门“技艺”(Craftsmanship)。

坚持自己的技术梦想和技术信仰,怀有一颗“匠人”之心

技艺部分、思想部分和实践部分。

掌握这些方法论可以有效地提高我们的编程素养,培养更好的编程习惯,写出更好的程序。

命名。好的命名可以极大地提升代码可读性和可理解性,

规范。在Google的代码审查(Code Review)实践中,代码是否符合规范(Norms)是最重要的检查项

了解必需的规范、如何制定规范,以及如何贯彻实施规范。

函数。有时即使你不采用任何面向对象(Object Oriented,OO)技术,只把函数写好,代码也会呈现完全不一样的风貌

设计原则。本章介绍了很多前人总结的优秀设计原则,包括最著名的SOLID,它为我们提供了非常好的OO设计指导原则

设计模式。好的设计模式能够使代码具有恰到好处的灵活性和优雅性,工程师之间的沟通也会变得简单

DDD的精髓。领域建模是面向对象技术的精髓,本章的主要思想都来自于领域驱动设计(Domain Driven Design,DDD)

思想是比技艺更高层次的能力要求,如果说技艺是“术”,那么思想就是“道”,领悟这些道理,对我们的职业发展会大有裨益。

抽象。抽象能力是工程师需要的核心能力之一。

分治。分治思想的伟大之处在于,我们可以将一个很复杂的问题域分解成多个相对独立的子问题,再各个击破。

技术人的素养。做一个优秀的工程师不容易,然而还是有一些特质是值得我们学习的。

技术Leader的修养。一个优秀的工程师不一定是一个好的技术Leader,一个技术Leader在很大程度上决定了团队的技术味道和技术追求。

“Talk is cheap, show me the code”,一本没有实战的技术书是难以服众的。如果说思想是务虚的最高境界,那么实践就是务实的最低要求。

COLA架构。本章主要介绍了什么是架构,重点介绍COLA架构及其背后的设计理念和设计原理。

工匠平台。本章通过COLA架构在工匠平台实际业务场景中的落地,介绍如何使用COLA来搭建一个完整的应用架构,以及如何通过领域建模来实现业务逻辑。

“虚实结合”——既重视思想,又兼顾实践。

所谓思想,是我们分析和解决问题所需要的底层能力。

抽象、批判思维、辩证思维,以及程序员的素养

思想是我们构建技术大厦的底层基石,是我们必须要掌握的底层能力,它超越了软件行业的范畴,是一种哲学和世界观。

实践即COLA架构

开源框架COLA的技术指导手册

看完本书,相信你会对COLA,以及如何应用COLA进行应用架构和复杂性治理有一个全面的了解。

COLA是面向业务应用的框架
适用于具有一定工作经验、从事业务开发的读者。

新程序员:如果你是在校生或初入职场的新人,在追求技术宽度的同时,请一定要养成“写好代码”的习惯。

资深程序员:职场的资深人士能够选择本书,说明你和我一样,还怀有一颗“不安分”的心

架构师:熟悉我的人都知道,我不赞成在业务团队设置专门的架构岗位,因为我认为架构是一种能力,而不是职位。

如果恰巧,你就在这样的岗位上,那么请一定不要画完架构图就算完成工作,要深入代码细节中去,这样才能发现设计中存在的问题,赢得程序员的尊重。

技术团队管理者:管理者的一个很重要的使命就是帮助团队成长,包括制定规范和技术传承。

写书的过程比我预想的要难得多,记得最初的两个月,我常常坐在电脑前很长时间,却不知从何写起,大纲也被反复调整了很多次。正是家人一直在我背后默默地付出和陪伴

第一部分 技艺

名为万物之始,万物始于无名,道生一,一生二,二生三,三生万物。 ——《易经》

一个名字虽然并不影响程序的执行,但是却对代码的表达力和可读性有着重要的影响。

好的命名能够让代码的概念清晰,增加代码的表达力;词不达意的命名会破坏我们思考的连贯性,分散有限的注意力。

1.1 命名的力量

无论是对于人名,还是企业名、产品名,命名都有着巨大的力量。

POJO 这个名字加速了EJB的消亡。

1.2 命名其实很难

命名为什么难呢?

  • 因为命名的过程本身就是一个抽象和思考的过程,在工作中,当我们不能给一个模块、一个对象、一个函数,甚至一个变量找到合适的名称的时候,往往说明我们对问题的理解还不够透彻,需要重新去挖掘问题的本质,对问题域进行重新分析和抽象,有时还要调整设计和重构代码。因此,好的命名是我们写出好代码的基础。

像Stack Overflow的创始人Joel Spolsky所说的:“起一个好名字应该很难,因为一个好名字需要把要义浓缩在一到两个词中。

Martin Fowler也表示过,他最喜欢的一句谚语是:“在计算机科学中有两件难事:缓存失效和命名。

1.3 有意义的命名

通常,如果你无法想出一个合适的名字,很可能意味着代码“坏味道”、设计有问题

这时可以思考一下:是不是一个方法里实现了太多的功能?或者类的封装内聚性不够?又或者是你对问题的理解还不够透彻,需要获取更多的信息?

变量名应该是名词,能够正确地描述业务,有表达力。

命名应该提升抽象层次、体现业务语义。

类是面向对象中最重要的概念之一,是一组数据和操作的封装。对于一个应用系统,我们可以将类分为两大类:实体类和辅助类。

实体类承载了核心业务数据和核心业务逻辑,其命名要充分体现业务语义,并在团队内达成共识,如Customer、Bank和Employee等

辅助类是辅佐实体类一起完成业务逻辑的,其命名要能够通过后缀来体现功能。

对于辅助类,尽量不要用Helper、Util之类的后缀,因为其含义太过笼统,容易破坏SRP(单一职责原则)。比如对于处理CSV,可以这样写: CSVHelper.parse(String) CSVHelper.create(int[])但是我更建议将CSVHelper拆开: CSVParser.parse(String) CSVBuilder.create(int[])

包(Package)代表了一组有关系的类的集合,起到分类组合和命名空间的作用。

这里说的模块(Module)主要是指Maven中的Module,相对于包来说,模块的粒度更大,通常一个模块中包含了多个包。

在Maven中,模块名就是一个坐标:

在COLA架构中,模块代表着架构层次,因此,对任何应该遵循COLA规范的应用都有着xxx-controller、xxx-app、xxx-domain和xxx-Infrastructure这4个标准模块。

1.4 保持一致性

保持命名的一致性,可以提高代码的可读性,从而简化复杂度。因此,我们要小心选择命名,一旦选中,就要持续遵循,保证名称始终一致。

每个概念对应一个词,并且一以贯之。例如,fetch、retrieve、get、find和query都可以表示查询的意思

[插图]

常见的对仗词组:·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(平均支出)。

为了避免Num带来的麻烦,我建议用Count或者Total来表示总数,用Id表示序号。这样,customerCount表示客户的总数,customerId表示客户的编号。

在业务语义和文档、代码之间出现了一条无形的鸿沟。

统一语言就是要确保团队在内部的所有交流、模型、代码和文档中都要使用同一种编程语言

这些通用技术语言包括DO、DAO、DTO、ServiceI、ServiceImpl、Component和Repository等

◆ 1.5 自明的代码

好的代码是最好的文档

可读性和自明性。所谓自明性,就是在不借助其他辅助手段的情况下,代码本身就能向读者清晰地传达自身的含义。

我们可以通过添加中间变量让代码变得更加自明,即将计算过程打散成多个步骤,并用有意义的变量名来命名中间变量,从而把隐藏的计算过程以显性化的方式表达出来。

使用设计模式语言也是代码自明的重要手段之一,在技术人员之间共享和使用设计模式语言,可以极大地提升沟通的效率

例如,Spring里面的ApplicationListener就充分体现了它的设计和用处。通过这个命名,我们知道它使用了观察者模式

FilterChain这个名字非常恰当地表达出了作者的意图,Chain表示用的是责任链模式,Filter表示用来进行过滤。

如果注释是为了阐述代码背后的意图,那么这个注释是有用的;如果注释是为了复述代码功能,那么就要小心了,这样的注释往往意味着“坏味道”

在写注释时,你应该自省自己是否在表达能力上存在不足,真正的高手是尽量不写注释。

◆ 1.6 命名工具

他山之石,可以攻玉”,当你不知道如何优雅地给变量命名时,可以使用命名工具

我们可以在IDE中安装一个搜索插件,便于搜索海量的互联网上的开源代码

OnlineSearch的插件,插件里自带了像SearchCode这样的代码搜索工具,也可以自己配置像Codelf这样的代码搜索工具。

◆ 1.7 本章小结

命名在软件设计中有着举足轻重的作用,命名的力量就是语言的力量,好的命名可以保证代码不仅是被机器执行的指令,更是人和人之间沟通的桥梁。

有意义的命名更能够引导我们更加深入地理解问题域,理清关键业务概念,进行合理的业务抽象,从而设计出更加符合业务语义、易于理解的系统。

了解如何给软件制品(Artifact,包括Module、Package、Class、Function和Variable)命名,如何写注释,如何让代码自明地表达自己,以及如何保持命名风格的一致性。

◆ 第2章 规范

信息熵来进行复杂性度量的方法。所谓信息熵,就是一条信息的信息量大小和它的不确定性之间的关系

事物的复杂程度在很大程度上取决于其有序程度,减少无序能在一定程度上降低复杂度,这正是规范的价值所在。

◆ 2.1 认知成本

所谓认知,是指人们获得知识或应用知识的过程。

获得知识是要学习的,在学习过程中,我们要交的学费叫作认知成本。那什么是知识呢?知识是人类对经验范围内的感觉进行总结归纳之后发现的规律

◆ 2.2 混乱的代价

知道你是否有找不到衣服的痛苦经历,比如我每次找衣服都要花不少时间

问题的根源就在于“混乱”。后来,我痛定思痛,对衣服进行分门别类,收纳整理,找起来会容易得多

随心所欲”与“遵守规范”

混乱是有代价的,我们有必要使用规范和约定来使大脑从记忆不同的代码段的随意性、偶然性差异中解脱出来。

◆ 2.3 代码规范

代码格式关系到代码的可读性,因此需要遵从一定的规范,包括缩进、水平对齐、注释格式等

在我第一次领会到空行在概念区隔起到的作用时,其结果让我大吃一惊。

对于同样的内容,由空行分出小段落比大段文字具有更好的可读性

这种极其简单的规则极大地影响代码的视觉外观,每个空白行都是一条线索,提示你下一组代码表示的是不同的概念或功能。

当前的主流编程语言有50种左右,分为两大阵营——面向对象和面向过程;按照变量定义和赋值的要求,又可分为强类型语言和弱类型语言。

在Java中,我们通常使用如下命名约定。 ·类名采用“大驼峰”形式,

类名采用“大驼峰”形式,

方法名采用“小驼峰”形式,即首字母小写的驼峰,方法名一般为动词,与参数组成动宾结构,

常量命名的字母全部大写,单词之间用下划线连接,

枚举类以Enum或Type结尾,枚举类成员名称需要全大写,单词间用下划线连接

1.ERROR级别ERROR表示不能自己恢复的错误,需要立即被关注和解决。

但是切忌把INFO当成DEBUG使用,这样会导致记录的数据过多,一方面影响系统性能,日志文件增长过快,消耗不必要的存储资源;另一方面也不利于阅读日志文件。

DEBUG是输出调试信息,如request/response的对象内容。

为了防止日志量过大,我们可以采用分布式配置工具来实现基于requestId判断的日志过滤,从而只打印我们所需请求的DEBUG日志。

针对以上问题,我建议在业务系统中设定两个异常,分别是BizException(业务异常)和SysException(系统异常),而且这两个异常都应该是Unchecked Exception。

例如,我们可以将错误码定义成3个部分:类型+场景+自定义标识。

对于错误类型,我们可以做一个约定:P代表参数异常(ParamException)、B代表业务异常(BizException)、S代表系统异常(SystemException)。

◆ 2.4 埋点规范

有一句话叫“业务数据化、数据业务化”,即业务要沉淀数据、数据要反哺业务。

[插图]

在阿里巴巴有一个超级位置模型(Super Position Model,SPM)的埋点规范,用于统计分析各种场景的用户行为数据。

◆ 2.5 架构规范

规范对于架构来说至关重要。从某种意义上来说,架构就是一组约束,遵从了这些约束,才能符合架构要求;反之,架构将失去意义。

我们要求使用COLA架构的应用都遵循相同的分层原则、类似的模块化思想和分包机制

◆ 2.6 防止破窗

破窗效应,第一扇窗打破引发。一次等于无数次环境中的不良现象如果被放任存在,就会诱使人们仿效,甚至变本加厉。以一幢有少许破窗的建筑为例,如果破窗不被修理好,可能将会有破坏者破坏更多的窗户。最终,他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦而没有被清洗掉,那么很快,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的“破窗效应”。

◆ 2.7 本章小结

留给公司一个方便维护、整洁优雅的代码库,是我们技术人员的最高技术使命,也是我们对公司做出的最大技术贡献。

◆ 第3章 函数

如果将数据比作一道菜,那么函数就是菜谱,程序员就是厨师。相同的菜,有不同的做法,由不同的厨师做出来,味道会截然不同。

◆ 3.1 什么是函数

函数(function)作为数学概念,最早由我国清朝数学家李善兰翻译,出自其著作《代数学》。之所以这么翻译,他给出的理由是“凡此变数中函彼变数者,则此为彼之函数”,即函数指一个量随着另一个量的变化而变化,或者说一个量中包含另一个量。

◆ 3.5 短小的函数

Robert C. Martin有一个信条:函数的第一规则是要短小,第二规则是要更短小。

如果是Java语言,我建议一个方法不要超过20行代码,当我把这个规定作为团队代码审查的硬性指标后,发现代码质量得到了显著的改善。

◆ 3.6 职责单一

一个方法只做一件事情,也就是函数级别的单一职责原则(Single Responsibility Principle,SRP)。

因为职责越单一,功能越内聚,就越有可能被复用,这和代码的行数没有直接的关联性,但是有间接的关联性。

◆ 3.7 精简辅助代码

使用Java 8的这个新特性和新语法,我们可以用Optional来代替冗长的null检查:String isocode = Optional.ofNullable(user) .flatMap(User::getAddress) .flatMap(Address::getCountry) .map(Country::getIsocode) .orElse(“default”);

实际上,我们完全可以自研一个缓存框架,使用注解(Annotation)来代替这些铅板代码(Boilerplate Code)。

MultiCacheable(cacheNames =”product”) public List getProducts(@CacheKeyList List productIds, @CacheNotHit List notExistIds) { return notExistIds.stream().map(productId -> getProductsById (productId)). collect(Collectors.toList()); }

缓存这个技术细节的辅助代码被从业务逻辑中剥离出去,并进行统一维护,既减少了重复,又避免了和具体缓存实现的耦合,可谓是一举多得。

利用Hystrix提供的API,我们可以使用注解的方式定义降级服务,从而不用在业务逻辑里面使用try/catch来做异常情况下的服务降级。

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.8 组合函数模式

组合函数要求所有的公有函数(入口函数)读起来像一系列执行步骤的概要,而这些步骤的真正实现细节是在私有函数里面。组合函数有助于代码保持精炼并易于复用。阅读这样的代码就像在看一本书,入口函数是目录,目录的内容指向各自的私有函数,而具体的内容是在私有函数里实现的。

就像Kent Beck说的:“我不是一个伟大的程序员,只是习惯比较好而已。”只有养成精益求精、追求卓越的习惯,才能保持精进,写出好的代码。

◆ 3.9 SLAP

抽象层次一致性(Single Level of Abstration Principle,SLAP),是和组合函数密切相关的一个原则。

组合函数要求将一个大函数拆成多个子函数的组合,而SLAP要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,就会显得凌乱,难以理解。

如果继续有更多的需求加入,那么代码会进一步恶化,最后变成一个谁也看不懂且难以维护的逻辑迷宫。

满足SLAP实际上是构筑了代码结构的金字塔。金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。

在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次

在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。在这一点上,金字塔原理和SLAP是相通的,世界就是如此奇妙,很多道理在不同的领域同样适用。

◆ 3.10 函数式编程

函数式编程中最重要的特征之一,就是你可以把函数(你的代码)作为参数传递给另一个函数

。为什么这个功能很重要呢?主要有以下两个原因。·减少冗余代码,让代码更简洁、可读性更好。·函数是“无副作用”的,即没有对共享的可变数据操作,可以利用多核并行处理,而不用担心线程安全问题。

可以明显地看到,函数式编程的代码量更少,实现上更优雅、简洁。简洁也是控制复杂度的重要手段之一。

◆ 3.11 本章小结

在本章中,我们从函数参数、函数职责、函数写法(短小、优化判空和优化缓存等)、函数抽象(组合模式、SLAP),到函数式的代码风格,介绍了如何写好一个函数,怎样让函数更易于理解、更加简洁。

◆ 第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):依赖倒置原则。

这个新名字的确促进了SOLID思想的传播,再一次证明了命名的重要性。

开闭原则和里氏代换原则是设计目标;单一职责原则、接口分隔原则和依赖倒置原则是设计方法。

◆ 4.2 SRP

SRP要求每个软件模块职责要单一,衡量标准是模块是否只有一个被修改的原因。职责越单一,被修改的原因就越少,模块的内聚性(Cohesion)就越高,被复用的可能性就越大,也更容易被理解。

SRP要求每个软件模块职责要单一,衡量标准是模块是否只有一个被修改的原因。

◆ 4.3 OCP

软件实体应该对扩展开放,对修改关闭。

在面向对象设计中,我们通常通过继承和多态来实现OCP,即封装不变部分。

装饰者模式,可以在不改变被装饰对象的情况下,通过包装(Wrap)一个新类来扩展功能;策略模式,通过制定一个策略接口,让不同的策略实现成为可能;适配器模式,在不改变原有类的基础上,让其适配(Adapt)新的功能;观察者模式,可以灵活地添加或删除观察者(Listener)来扩展系统的功能。

当然,要想做到绝对地“不修改”是比较理想主义的。因为业务是不确定的,没有谁可以预测到所有的扩展点,因此这里需要一定的权衡,如果提前做过多的“大设计”,可能会犯YAGNI(You Ain’t Gonna Need It)的错误。

◆ 4.4 LSP

根据LSP的定义,如果在程序中出现使用instanceof、强制类型转换或者函数覆盖,很可能意味着是对LSP的破坏。

可以通过提升抽象层次来解决此问题,也就是将子类中的特有函数用一种更抽象、通用的方式在父类中进行声明。这样在使用父类的地方,就可以透明地使用子类进行替换了

例如,正方形是一个矩形,但是如果你把正方形设计成矩形的子类,就会出现一些意想不到的问题。以计算面积为例,矩形是a乘以b,而正方形是a的平方,它们在含义上是有区别的。

正方形-矩形问题(Square-rectangle Problem)

◆ 4.5 ISP

接口隔离原则认为不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。

满足ISP之后,最大的好处是可以将外部依赖减到最少。你只需要依赖你需要的东西,这样可以降低模块之间的耦合(Couple)。

◆ 4.6 DIP

模块之间交互应该依赖抽象,而非实现。DIP要求高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象

然而“面向接口编程”只是实现DIP的一个技法,DIP本身的意义要宽泛得多,它是一种思想,是一种软件设计的哲学。

这样,只要主板公司和显卡公司都依赖同一个抽象(显卡接口协议),就能实现互通了。

遵循DIP会大大提高系统的灵活性。如果类只关心它们用于支持的特定契约,而不是特定类型的组件,就可以快速而轻松地修改这些低级服务的功能,同时最大限度地降低对系统其余部分的影响。

依赖倒置,就是要反转依赖的方向,让原来紧耦合的依赖关系得以解耦,这样依赖方和被依赖方都有更高的灵活度。

[插图]图4-8 依赖倒置后的Logger依赖

所以我强烈建议所有的业务系统都应该有这样一个Logger抽象,来屏蔽对具体Logger框架的依赖。

除了组件级别的DIP,在架构层面,DIP同样有着重要的指导意义。

在COLA架构中,领域层不应该直接依赖基础设施层,它们之间的解耦就是通过DIP完成的

◆ 4.7 DRY

系统的每一个功能都应该有唯一的实现。

贯彻DRY可以让我们避免陷入“散弹式修改(Shotgun Surgery)”的麻烦,“散弹式修改”是Robert Martin在《重构》一书中列出的一个典型代码“坏味道”,由于代码重复而导致一个小小的改动,会牵扯很多地方。

◆ 4.8 YAGNI

YAGNI(You Ain’t Gonna Need It)的意思是“你不会需要它”,出自Ron Jeffries的Extreme Programming Installed一书。

YAGNI是针对“大设计”(Big Design)提出来的,是“极限编程”提倡的原则,是指你自以为有用的功能,实际上都是用不到的。因此,除了核心的功能之外,其他的功能一概不要提前设计,这样可以大大加快开发进程。它背后的指导思想就是尽可能快、尽可能简单地让软件运行起来。

◆ 4.9 Rule of Three

Rule of Three也被称为“三次原则”,是指当某个功能第三次出现时,就有必要进行“抽象化”了。这也是软件大师Martin Fowler在《重构》一书中提出的思想。

三次原则指导我们可以通过以下步骤来写代码。(1)第一次用到某个功能时,写一个特定的解决方法。(2)第二次又用到的时候,复制上一次的代码。(3)第三次出现的时候,才着手“抽象化”,写出通用的解决方法。这3个步骤是对DRY原则和YAGNI原则的折中,是代码冗余和开发成本的平衡点。

软件设计本身就是一个平衡的艺术,我们既反对过度设计(Over Design),也绝对不赞成无设计(No Design)。

◆ 4.10 KISS原则

KISS(Keep It Simple and Stupid)最早由Robert S. Kaplan在著名的平衡计分卡理论中提出。他认为把事情变复杂很简单,把事情变简单很复杂。好的目标不是越复杂越好,反而是越简洁越好。

我们一定要理解“简单”和“简陋”的区别。

真正的“简单”绝不是毫无设计感,上来就写代码,而是“宝剑锋从磨砺出”,亮剑的时候犹如一道华丽的闪电,背后却有着大量的艰辛和积累。真正的简单,不是不思考,而是先发散、再收敛。在纷繁复杂中,把握问题的核心。

◆ 4.11 POLA原则

POLA(Principle of least astonishment)是最小惊奇原则,写代码不是写侦探小说,要的是简单易懂,而不是时不时冒出个“Surprise”。在《复杂》一书的第7章“度量复杂性”中,就阐述了用“惊奇度”来度量复杂度的方法,“惊奇度”越高,复杂性越大,这也是侦探小说要比一般小说更“烧脑”的原因。

◆ 4.12 本章小结

设计原则能够指导我们编写出更好的代码。但还是那句话,不要教条,软件是一种平衡的艺术。要清楚一点,我们不是为了满足这些原则而工作的,原则只是背后的指导思想。我们的目的是构建可用的软件系统,并尽量减少系统的复杂度。在不能满足所有原则时,要懂得适当取舍。

◆ 第5章 设计模式

利用模式,我们可以让一个解决方案重复使用,而不是重复造轮子。(With patterns, you can use the solution a million times over, without ever doing it the same way twice.)——克里斯托佛·亚历山大

设计模式描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案,具有一定的普遍性,可以反复使用。其目的是提高代码的可重用性、可读性和可靠性。

正确使用设计模式,可以提高程序员的思维能力、编程能力和设计能力,使程序设计更加标准化、代码编制更加工程化,从而大大提高软件开发效率。

◆ 5.1 模式

所谓模式,就是得到很好的研究的范例。设计模式,就是设计范例。《孙子兵法》中充斥着各种模式,“置之死地而后生”是战争模式;“三十六计”条条都是模式,比如“走为上”和“空城计”都是战争模式。

模式不是框架(Framework),也不是过程。模式不是简单的“问题的解决方案”,必须是典型问题的解决方案,是可以让学习者举一反三的,是理论和实践之间的中介环节。模式具有一般性、简单性、重复性、结构性、稳定性和可操作性等特征。

模式不能套用,必须结合具体情况和上下文(Context)使用。

◆ 5.2 GoF

1995年,Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,书中收录了23个设计模式。

根据模式所完成的工作类型来划分,模式可分为创建型模式、结构型模式和行为型模式,如图5-2所示。[插图]

(1)创建型模式:用于描述“怎样创建对象”,主要特点是“将对象的创建与使用分离”。GoF中提供了单例、原型、工厂方法、抽象工厂、建造者5种创建型模式。(2)结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF中提供了代理、适配器、桥接、装饰、外观、享元、组合7种结构型模式。(3)行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器11种行为型模式。

(1)单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点,以便外部获取该实例,其拓展是有限多例模式。 (2)原型(Prototype)模式:将一个对象作为原型,通过对其进行复制操作而复制出多个和原型类似的新实例。 (3)工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。 (4)抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。 (5)建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同的需要分别创建它们,最后构建成该复杂对象。 (6)代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问,即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。 (7)适配器(Adapter)模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。 (8)桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。 (9)装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。 (10)外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。 (11)享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。 (12)组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。 (13)模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使子类可以在不改变该算法结构的情况下,重定义该算法的某些特定步骤。 (14)策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。 (15)命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。 (16)职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式可以去除对象之间的耦合。 (17)状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。 (18)观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。 (19)中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。 (20)迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。 (21)访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。

(22)备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。 (23)解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。

需要进一步学习的读者,建议去看“四人组”的著作《设计模式:可复用面向对象软件的基础》,此外,Head first Design Pattern和《设计模式解析》也值得阅读。

◆ 5.3 拦截器模式

拦截器模式(Interceptor Pattern),是指提供一种通用的扩展机制,可以在业务操作前后提供一些切面的(Cross-Cutting)的操作。这些切面操作通常是和业务无关的,比如日志记录、性能统计、安全控制、事务处理、异常处理和编码转换等。

相比于AOP中的代理实现(静态代理和动态代理),我更喜欢拦截器的实现方式,原因有二:一个其命名更能表达前置处理和后置处理的含义,二是拦截器的添加和删除会更加灵活,

 图5-3 拦截器的实现原理

拦截器模式中,主要包含以下角色。 ·TargetInvocation:包含了一组Interceptor和一个Target对象,确保在Target处理请求前后,按照定义顺序调用Interceptor做前置和后置处理。

Target:处理请求的目标接口。 ·TargetImpl:实现了Target接口的对象。 ·Interceptor:拦截器接口。 ·InterceptorImpl:拦截器实现,用来在Target处理请求前后做切面处理。

◆ 5.4 插件模式

插件模式 插件(plug-in)模式扩展方式和普通的对象扩展方式的不同之处在于,普通的扩展发生在软件内部,插件式扩展发生在软件外部。比如,我们在一个项目中使用了策略模式,当需要添加新的策略时,我们不得不重新编译代码、打包和部署,新的策略才能生效。 而插件式扩展是发生在软件外部的,新扩展以一个单独的组件(比如jar包)的方式加入软件中,软件本身不需要重新编译、打包。有些插件模式甚至可以做到热部署,即在运行时实现插件的加载或卸载,做到真正的即插即用(Pluggable)。

在一个插件框架中,通常会涉及以下概念。 ·ExtensionPoint:扩展点,用来标识可以扩展的功能点。 ·Extension:扩展,是对ExtensionPoint的扩展实现。 ·PluginDescriptor:插件描述,即描述插件的元数据,定义了包括对外暴露的扩展点,运行插件所需要的依赖等信息。一个PluginDescriptor对应一个Plugin.xml配置。 ·PluginRegistry:插件注册,用来进行插件注册和存储。 ·PluginManager:插件管理,用来装载和激活插件实例。 ·Plugin:插件实例,当PluginManager调用activate方法激活Plugin时,就会产生一个Plugin实例。

 图5-5 插件模式的概念类图

有关这些实现细节,我推荐一个开源项目JPF(Java Plug-in Framework),它受到了Eclipse的插件式启发,致力于打造一个通用的Java插件框架。

◆ 5.5 管道模式

管道模式 管道这个名字源于自来水厂的原水处理过程。原水要经过管道,一层层地过滤、沉淀、去杂质、消毒,到管道另一端形成纯净水。

链式管道 看过Tomcat源码或阿里巴巴开源的MVC框架WebX源码的读者,应该对其中的管道(Pipeline)和阀门(Valve)不会陌生。一个典型的管道模式,会涉及以下3个主要的角色。 (1)阀门:处理数据的节点。 (2)管道:组织各个阀门。 (3)客户端:构造管道并调用。

流处理 管道模式还有一个非常广泛的应用——流式处理,即把自来水厂的原水换成数据,形成数据流。管道模式适用于那些在一个数据流上要进行不同的数据计算场景,这种方式称为流处理,也称为流式计算。

鉴于流式计算在处理数据流上的优雅性,Java 8在引入函数式编程的同时,还提供了Stream API对集合流进行流式计算。

◆ 第6章 模型

建模的艺术就是去除实在中与问题无关的部分。 ——利普·沃伦·安德森(1977年诺贝尔物理学奖得主)

在软件工程中,有两个高阶工作分别是架构和建模。如果把写代码比喻成“施工”,那么架构和建模就是“设计图纸”。相比于编码,建模的确是对设计经验和抽象能力要求更高的一种技能。例如,在当前热门的人工智能和机器学习领域,建模就是其中非常重要的工作。

◆ 6.1 什么是模型

模型是对现实世界的简化抽象。建立模型有很多方法,并不意味着要用特定的符号、工具和流程。

无论使用何种建模工具和表示法(Notation),只要有助于我们对问题域的理解,均可认为是好的模型。

处理问题时,我们最好隐藏那些不必要的细节,只专注于重要的方面,抓住问题的本质。这也是建模和抽象的价值所在。

在不同的场景下,模型对相同的实体会有不同的表达方式。模型的作用就是表达不同概念的性质。根据使用场景的不同,模型大致可以分为物理模型、概念模型、数学模型和思维模型等。

物理模型 物理模型是拥有体积及重量的物理形态概念实体物件,是根据相似性理论制造的按原系统比例缩小(也可以是放大或与原系统尺寸一样)的实物。例如,风洞实验中的飞机模型、水力系统实验模型、建筑模型和船舶模型和汽车模型(如图6-1所示)等。

数学模型 数学模型是用数学语言描述的一类模型,可以是一个或一组代数方程、微分方程、差分方程、积分方程或统计学方程,也可以是某种适当的组合数学模型。利用这些方程可以定量地或定性地描述系统各变量之间的相互关系或因果关系,来描述系统的行为和特征,而不是系统的实际结构。

概念模型 概念模型是对真实世界中问题域内的事物的描述,是领域实体,而不是对软件设计的描述,它和技术无关。概念模型将现实世界抽象为信息世界,把现实世界中的客观对象抽象为某一种信息结构,这种信息结构并不依赖于具体的计算机系统。

思维模型 我们把用简单易懂的图形、符号或者结构化语言等表达人们思考和解决问题的形式,统称为思维模型。简单来说,就是我们可以总结出一些能够解决特定问题的“思维套路”,这些套路能帮助我们高效地解决问题。例如,8.5.3节中介绍的金字塔模型就是一种非常好的结构化思维模型

模型毕竟是模型,不能代替现实,就像类比不能代替问题本身一样。建模的过程与建模者的观察视角和对问题的认知有直接关系,所以我们要带着审视的眼光去看待模型。

模型要适时升级模型在软件开发中的作用也是一样的,我们也要用发展的眼光来看待模型,能解决当前问题的模型就是好模型,随着时间的推移,我们可能要像重构代码那样去重构模型,确保它能跟上我们对问题域的最新理解。

◆ 6.2 UML

UML 在软件领域,影响力最强的建模工具当属统一建模语言(Unified Modeling Language,UML)了。

UML提出了一套IT专业人员期待多年的统一的标准建模符号。

通过使用UML,用户能够阅读和交流系统架构和设计规划,就像建筑工人使用的建筑设计图一样。

UML分为结构型和行为型建模图形,具体分类如图6-4所示。  图6-4 UML分类

推荐阅读Grady Booch等人的《面向对象分析与设计》和Larman的《UML和模式应用》这两本书。

◆ 6.3 类图

类(Class)封装了数据和行为,是面向对象的重要组成部分,是具有相同属性、操作、关系的对象集合的总称。在系统中,每个类都具有一定的职责,职责指的是类要完成什么样的功能,要承担什么样的义务。

类图用于描述类以及它们的相互关系。在分析时,我们利用类图来说明实体共同的角色和责任,这些实体提供了系统的行为。

类的UML表示法 在UML中,类由包含类名、属性和操作3部分组成,这3部分使用分隔线分隔的矩形表示。

 图6-5 类图示例

在UML类图中,类一般由以下3个部分组成。 (1)类名(Name):每个类都必须有一个名字,类名是一个字符串。 (2)类的属性(Attributes):属性指类的性质,即类的成员变量。一个类可以有任意多个属性,也可以没有属性。 (3)类的操作(Operations):操作是类的任意一个实例对象都可以使用的行为,是类的成员方法。

属性规格说明格式是“可见性 属性名称:类型”,比如“- name : String”。操作规格说明格式是“可见性 操作名称(参数名称:类型):返回值类型”,比如“+ getName() : String”。

可见性:表示该属性对于类外的元素而言是否可见,包括公有(public)、私有(private)和受保护(protected),在类图中分别用符号+、-和#表示。 ·名称:按照惯例,类的名称以大写字母开头,单词之间使用驼峰隔开。属性和操作的名称以小写字母开头,后续单词使用驼峰。 ·类型:表示属性的数据类型,可以是基本数据类型,也可以是用户自定义类型。

关联(Association)关系是一种结构化关系,用于表示一类对象与另一类对象之间有联系,如汽车和轮胎、师傅和徒弟、班级和学生

在UML类图中,用实线连接有关联关系的对象所对应的类。在代码实现上,通常将一个类的对象作为另一个类的成员变量。

 图6-6 双向关联实例

三角形标注表示关联关系的阅读方向,是可选的。直线两边的数字代表关联的重数性(Multiplicity),也是可选的,表示两个关联对象在数量上的对应关系。

表6-1 多重性表示方式 

限定关联 限定关联(Qualified association)具有限定符(Qualifier),限定符的作用类似HashMap中的键(key),用于从一个集合中选择一个或多个对象。例如,一个用户(User)可以有多个角色(Role),但是在一个场景(Scenario)下,它只会是一种角色。

对于限定关联,有一点需要注意,即多重性的变化。例如,比较图6-7a和图6-7b,限定减少了在关联目标端的多重性,通常是由多变为一,因为限定关联通常是从较大集合中选择一个实例。

 图6-7 限定关联实例

类的关联关系也可以是单向的,单向关联用带箭头的实线表示。例如,顾客(Customer)拥有地址(Address),则Customer类与Address类具有单向关联关系

 图6-8 单向关联实

在系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系称为自关联。

一个节点类(Node)的成员又是节点Node类型的对象,如图6-9所示。  图6-9 自关联实例

设计模式中的装饰者模式也是一种自关联,都有类似如下的代码形式: public class Node { private Node subNode; }

聚合关系 聚合(Aggregation)关系表示整体与部分的关联关系。在聚合关系中,成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。

聚合关系用带空心菱形的直线表示。例如,汽车发动机(Engine)是汽车(Car)的组成部分,但是汽车发动机可以独立存在,因此汽车和发动机是聚合关系

 图6-10 聚合关系实

组合(Composition)关系也表示类之间整体和部分的关联关系,但是在组合关系中,整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在,成员对象与整体对象之间具有“同生共死”的关系。

在UML中,组合关系用带实心菱形的直线表示。例如,人的头部(Head)与嘴(Mouth),嘴是头部的组成部分,如果头部不存在,那么嘴也就不存在了,因此头部和嘴是组合关系

 图6-11 组合关系实

在用代码实现组合关系时,通常在整体类的构造方法中直接实例化成员类。成员对象域整体对象有同样的生命周期,也就是要“共生死”,这也是组合和聚合的主要区别。

public class Head { private Mouth mouth; public Head() { mouth = new Mouth(); //实例化成员类 } }public class Mouth { }

类的依赖关系 依赖(Dependency)关系是一种使用关系,特定事物的改变可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时,使用依赖关系。

依赖关系体现在某个类的方法使用另一个类的对象作为参数。在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方

。例如,教师(Teacher)上课时使用投影仪(Projector)进行演示

系统实施阶段,依赖关系通常通过3种方式来实现。 (1)第一种方式(也是常用的一种方式)是将一个类的对象作为另一个类中方法的参数

2)第二种方式是在一个类的方法中将另一个类的对象作为其局部变量。

3)第三种方式是在一个类的方法中调用另一个类的静态方法。

类的泛化关系 泛化(Generalization)关系也称为继承关系,用于描述父类与子类之间的关系。父类称为基类或超类,子类称为派生类。在UML中,泛化关系用带空心三角形的直线来表示。在代码实现时,我们使用面向对象的继承机制来实现泛化关系

 图6-13 泛化关系实

接口与实现关系

在UML中,类与接口之间的实现关系通常是用带空心三角形的虚线来表示。

UML 2中新定义的插座表示法(Socket notation),有助于表示“类X需要(使用)接口Y”。

我们有一个统计类(Statistics)要使用度量项进行统计,其插座表示法如图6-15所示。

 图6-15 接口和实现的插座表示法

◆ 6.4 领域模型

从本质上来说,软件开发过程就是问题空间到解决方案空间的一个映射转化,如图6-16所示。  图6-16 软件开发的本质

在问题空间中,我们主要是找出某个业务面临的挑战及其相关需求场景用例分析;而在解决方案空间中,则通过具体的技术工具手段来进行设计实现。

可以简单理解为一个领域就对应一个问题空间,是一个特定范围边界内的业务需求的总和。

“领域模型”就是“解决方案空间”,是针对特定领域里的关键事物及其关系的可视化表现

例如,请假系统解决的是人力工时的问题,属于人力资源领域,对口的是HR部门;费用报销系统解决的是员工和公司之间的财务问题,属于财务领域,对口的是财务部门;电商平台解决的是网上购物问题,属于电商领域。

每个软件系统本质上都解决了特定的问题,属于某一个特定领域,实现了同样的核心业务功能来解决该领域中核心的业务需求。

◆ 6.5 敏捷建模

敏捷建模 和开发模式一样,建模也可以用一套价值观、原则和实践来进行指导,以求尽可能地敏捷。敏捷建模方法的重点如下。

模型能用来沟通和理解。 ·力争用简单的工具创建简单的模型。 ·我们知道需求是变化的,因此创建模型时要拥抱变化。 ·重点是交付软件,而不是交付模型。模型能带来价值时,我们就使用;如果模型没有价值,不能加速软件的交付,就不创建它们。

理解受众,清楚目标。对于创建出来的所有模型,我们都知道它的读者是谁、要达成什么目标。如果我们还没理解目标,就不会创建模型。

◆ 6.6 广义模型

广义模型

C4模型 C4模型由Simon Brown提出。C4模型提出使用上下文(Context)、容器(Container)、组件(Component)和代码(Code)等一系列分层的图表,来描述不同缩放级别的软件架构

UI流程图 UI流程图使用页面之间的流转来描述系统交互流程。

 图6-18 UI流程图实例

业务模型 除描述技术以外,用户也可以用图形化的方式来描述业务。图形化的表达往往比文字更容易使人理解,也更加生动。

 图6-19 电商网站客户动线

关于O2O就医的流程,这张图非常巧妙地使用了线条(line),线条本身除了表达时间顺序外,还用来作为线上(online)和线下(offline)的区隔

 图6-20 基本就医的O2O流

好的图形表示法不仅需要我们对问题域有深入的理解,还要我们具备一定的想象力和创造力

◆ 6.7 本章小结

只要合理地使用线条、图形、箭头和颜色来描述我们想要表达的技术问题或者业务问题,就是好的“建模”。

建模并不意味着要用特定的符号、工具和流程。不管你用什么建模工具、什么表示法,只要有助于对问题域的理解,就是好的模型。

◆ 第7章 DDD的精髓

你可以,不代表你应该。 (Just because you can, doesn’t mean you should.) ——施莉琳·凯尼恩

本章将重点讲解领域驱动设计(Domain Driven Design,DDD),包括DDD的重要概念,以及如何进行领域建模。

◆ 7.1 什么是DDD

DDD是Eric Evans在2003年出版的《领域驱动设计:软件核心复杂性应对之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一书中提出的具有划时代意义的重要概念,是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。

◆ 7.2 初步体验DDD

在使用DDD之后,Account实体除账号属性之外,还包含了行为和业务逻辑,比如debit()和credit()方法。

◆ 7.3 数据驱动和领域驱动

 图7-1 数据驱动研发过程

以客户关系管理(Customer Relationship Management,CRM)为例,其中很重要的概念有销售、机会、客户、私海、公海,实体的定义分别如下。

·销售(Sales):公司的销售人员,一个销售可以拥有多个销售机会。

·机会(Opportunity):销售机会,每个机会包含至少一个客户信息,且归属于一个销售人员。

客户(Customer):客户,也就是销售的对象。 ·私海(Private sea):专属于某个销售人员的领地(Territory),私海里面的客户,其他销售人员不能触碰。 ·公海(Public sea):公共的领地,所有销售人员都可以从公海里捡入客户到其私海。

领域驱动设计关心的是业务中的领域划分(战略设计)和领域建模(战术设计),其开发过程不再以数据模型为起点,而是以领域模型为出发点,

领域模型对应的是业务实体,在程序中主要表现为类、聚合根和值对象,它更加关注业务语义的显性化表达,而不是数据的存储和数据之间的关系。这是“领域驱动设计”和“数据驱动设计”之间显著的区别。

 图7-4 领域驱动研发过程

 图7-5 CRM的领域模型

领域模型的描述更加贴近业务,一些重要的业务术语和概念没有丢失,更完整地表达了业务语义。

通过DDD的战略设计和战术设计,我们可以为问题域划分出合适的子域,并对域中的业务进行建模。

 图7-6 CRM的领域划分

对象关系映射(Object Relationship Mapping,ORM)

 图7-7 对象关系映射

仍以上述的CRM案例为例,在数据模型中根本就没有私海和公海这两个实体,工具是无法映射的。因此,Hibernate和JPA的衰落是可以预见的。现在使用最多的是MyBatis,它很简单,完全不理会复杂的关系和对象之间的复杂关系映射,只做数据库表和DO之间的简单映射。

◆ 7.4 DDD的优势

如果一开始不能接受DDD也没有关系,我其实也经历过“排斥—接受—使用”的过程。

统一语言 统一语言(Ubiquitous Language)的主要思想是让应用能和业务相匹配,这是通过在业务与代码中的技术之间采用共同的语言达成的。

目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言。代码、类、方法、属性和模块的命名必须和统一语言相匹配,必要的时候需要对代码进行重构!

DDD的核心是领域模型,这一方法论可以通俗地理解为先找到业务中的领域模型,以领域模型为中心,驱动项目开发。领域模型的设计精髓在于面向对象分析、对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析的大师。

DDD鼓励我们接触到需求后第一步就是考虑领域模型,而不是将其切割成数据和行为,然后用数据库实现数据,用服务实现行为,最后造成需求的首尾分离

DDD会让你首先考虑业务语言,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。重点不同,导致编程世界观不同。

 图7-8 业务逻辑和技术细节分离的架构

技术细节和核心业务逻辑是两个维度的重要性,如果把软件比喻成一个人,那么核心业务逻辑是大脑,技术细节是身体,二者都很重要,分开处理主要是为了降低复杂度。

◆ 7.5 DDD的核心概念

 图7-9 现实世界与软件世界

假如现在你需要设计一个中介系统,一个典型的User Story是“小明去找工作,中介让他留个电话,有工作机会就会通知他”。我们要如何寻找该业务中的关键领域实体呢?一个简单的方式就是“找名词”,分析这些名词,不难得到以下可能成为实体的候选项。

聚合根(Aggregate Root)是DDD中的一个概念,是一种更大范围的封装,会把一组有相同生命周期、在业务上不可分割的实体和值对象放在一起,只有根实体可以对外暴露引用,这也是一种内聚性的表现。

Service往往是以一个活动来命名,而不是Entity来命名。

在银行转账的例子中,转账(transfer)这个行为是一个非常重要的领域概念,但是它发生在两个账号之间,归属于账号Entity并不合适,因为一个账号Entity没有必要去关联它需要转账的账号Entity。

识别领域服务,主要看它是否满足以下3个特征。 (1)服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。 (2)被执行的操作涉及领域中的其他对象。 (3)操作是无状态的。

领域事件(Domain Event)是在一个特定领域由一个用户动作触发的,是发生在过去的行为产生的事件,而这个事件是系统中的其他部分或者关联系统感兴趣的。

事件命名

事件是表示发生在过去的事情,所以在命名上推荐使用Domain Name + 动词的过去式 + Event,这样可以更准确地表达业务语义。例如,在

事件内容 事件内容在计算机术语中叫作payload,有以下两种形式。

(1)自恰(Enrichment):就是在事件的payload中尽量多放数据,这样consumer不需要回查就能处理消息,也就是自恰地处理消息。

2)回查(Query-Back):这种方式是只在payload放置id属性,然后consumer通过回调的形式获取更多数据。这种形式会加重系统的负载,可能会引起性能问题。

边界上下文 领域实体的意义是有上下文的,比如同样是Apple,在水果店和苹果手机专卖店中表达出的含义就完全不一样。边界上下文(Bounded Context)的作用是限定模型的应用范围,在同一个上下文中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。

那么不同上下文之间的业务实体要如何实现交互呢?

在DDD中,这种机制叫作上下文映射(Context Mapping),我们可以使用防腐层(Anti-Corruption)来完成映射的工作。

 图7-11 边界上下文示例

(1)虽然属性大部分一样,但二者的作用和行为在各自上下文中是不一样的。 (2)解耦影响,加入了防腐层之后,网站的会员变化就不会影响到CRM系统了。

◆ 7.6 领域建模方法

领域建模方法 7.6.1 用例分析法 1.方法介绍 用例分析法是进行领域建模中最简单可行的方式,其步骤如下。

1)获取用例描述 既然领域模型指的是问题域模型,那么建模也一定要从问题域入手。

2)寻找概念类 寻找概念类就是对获取的用例描述进行语言分析,识别名词和名词短语,将其作为候选的概念类。

3)添加关联 关联意味着两个模型之间存在语义联系,在用例中的表现通常为两个名词被动词连接起来,

 图7-12 语义分析示例

(4)添加属性

我们需要区分概念类和属性(当然名词列表也会有无用的词语)。例如,对于上文抽取到的名词列表,“品名”是“商品”的属性,“iTouch”为无用的词语。

5)模型精化 模型精化是可选的步骤,有时我们希望在领域模型中表达更多的信息,这时会利用一些新的手段来表达领域模型,包括泛化、组合和子域划分等

子领域划分是常见的拆解领域的方式,通常来说,我们会将更内聚的一组模型划分为一个子领域,形成更高一层的抽象,有利于系统的表达和分工。

接下来,我们按照用例分析法的步骤来建模。 (1)寻找概念类 首把所有名词标记出来,作为概念类的候选类:vendors, sales employees, companies, ba

2)添加关联 如图7-13所示,接下来为名词添加关联,连接这些名词的动词会出现在关联的线上

(3)添加属性 最后,为这些候选的概念类选择属性。

四色建模法 1.方法介绍 四色建模法源于Peter Coad的Java Modeling In Color With UML一书,它是一种模型的分析和设计方法,要把所有模型分为4种类型,用4种颜色表示

 图7-15 四色模型

(1)业务关键时刻(Moment-Interval)

这种对象表示那些在某个时间点存在或者会存在一段时间。这样的对象往往表示了一次外界的请求,比如一次询价(Quotation)、一次下单(Order)或者一次租赁(Rental)。

2)角色(Role) 这种对象表示一种角色,往往由人或者物来承担,会有相应的责任和权利。

一般,一个Moment-interval对象会关联多个Role。例如,一次下单涉及两个Role,分别是客户(Customer)和商品(Product)。

3)人-事-物(Party,Place or Thing) 这种对象往往表示一种客观存在的事物,例如人、组织、产品或者配件等,这些事物会在一种moment-interval中扮演某个Role。

4)描述(Description) 这种对象一般是用于分类或者描述性的对象,它的属性一般是这一类事物都有的属性,一般用蓝色来表示

 图7-16 在线电子书店的关键业务流

这些问题都需要业务系统捕捉到相应的足迹才能够回答,所以企业的业务系统的主要目的之一,就是记录这些足迹,并将这些足迹形成一条有效的追溯链。

发现这些业务关键时刻对象就是建模的起点。

 图7-17 在线电子书店的业务关键时刻对象

 图7-18 在线电子书店的人-事-物对象

 图7-19 在线电子书店的角色对

 图7-20 电子书店的描述对

◆ 7.7 模型演化

建模不是一次性的工作,也不可能是一次性的工作。业务在变化,我们对业务的理解在变化,因此模型也要随之变化。就像生产力和生产关系,当生产关系不能满足生产力发展时,一轮变革就在所难免了。

◆ 7.8 为什么DDD饱受争议

我就见过有团队花大力气去做DDD的转型,结果系统的复杂度不但没有降低,反而变得更加复杂,又不得不花大力气改回Service+DAO的贫血模式。

以我的经历来看,DDD项目失败的主要原因如下。 7.8.1 照搬概念

 图7-21 DDD的架构分层

Robert C. Martin的《架构整洁之道》一书,作者提出整洁的架构应该是“核心业务逻辑和技术细节相分离”的,才触发了我对Domain依赖Infrastructure合理性的重新思考,最终在COLA 2.0时,我们决定让Domain变得更加独立。

◆ 8.1 伟大的抽象

赫拉利在《人类简史》中说,“人类之所以成为人类,是因为人类能够想象”。这里的想象,我认为很大程度上是指抽象能力。

◆ 8.2 到底什么是抽象

抽象和具象是相对应的概念,“抽”就是抽离,“象”就是具象。从字面上理解抽象,就是从具体中抽离出来。英文的抽象abstract来自拉丁文abstractio,它的原意是排除、抽出。

按照维基百科上的解释,抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息

◆ 8.3 抽象是OO的基础

面向对象(Object Oriented,OO)的思考方式,就是万物皆对象。抽象帮助我们将现实世界的对象抽象成类,完成从现实世界的概念到计算机世界的模型的映射。

面向对象的思想主要包括3个方面:面向对象的分析(Object Oriented Analysis,OOA)、面向对象的设计(Object Oriented Design,OOD),以及我们经常提到的面向对象的编程(Object Oriented Programming,OOP)。

OOA是根据抽象关键问题域来分解系统。

OOD是一种提供符号设计系统的面向对象的实现过程,它用非常接近实际领域术语的方法把系统构造成“现实世界”的抽象。OOP可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反,传统的程序设计主张将程序看作一系列函数的集合,或者更直接些,就是一系列对计算机下达的指令。

◆ 8.4 抽象的层次性

内涵越小,外延越大;内涵越大,外延越小。不同层次的抽象有不同的用途。

这种抽象的层次性基本可以体现在任何事物上,以下是对一份报纸在多个层次上的抽象。 (1)第一层:一个出版物。 (2)第二层:一份报纸。 (3)第三层:《旧金山纪事报》。 (4)第四层:5月18日出版的《旧金山纪事报》。 (5)第五层:我拥有的5月18日出版的《旧金山纪事报》。

软件领域的任何问题,都可以通过增加一个间接的中间层来解决。

例如,我们的系统就是分层的。最早的程序直接运行在硬件上,开发成本非常高。然后慢慢开始有了操作系统,操作系统提供了资源管理、进程调度、输入输出等所有程序都需要的基础功能,开发程序时调用操作系统的接口就可以了。再后来发现操作系统也不够用,于是又有了各种运行环境(如JVM)。

◆ 8.5 如何进行抽象

如何进行抽象 8.5.1 寻找共性 简单来说,抽象的过程就是合并同类项、归并分类和寻找共性的过程。

 图8-3 抽象的层次性示例

 图8-4 提升抽象层次示例

因此,每当我们有强制类型转换,或者使用instanceof时,都值得停下来思考一下,是否需要做抽象层次的提升。

构筑金字塔 《金字塔原理》是一本教人如何进行结构化思考和表达的书,核心思想是通过归类分组搭建金字塔结构

要自下而上地思考,总结概括;自上而下地表达,结论先行。

抗墒增金字塔结构让我们通过抽象概括将混乱无序的信息形成不同的抽象层次,从而便于理解和记忆,这是一个非常实用的方法论。

◆ 8.6 如何提升抽象思维

如何提升抽象思维

多阅读 为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面时你的大脑会被铺满,较少需要抽象和想象。

多总结 小时候,我们可能不理解语文老师为什么总是要求我们总结段落大意、中心思想。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维的能力。

做总结最好的方式就是写文章,小到博文,大到一本书,都是锻炼自己抽象思维和结构化思维的机会。记录也是很好的总结习惯。以读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳,这样不仅可以加深理解,还可以提升自己的抽象思维能力。

领域建模训练 对于技术人员来说,还有一个非常好的提升抽象能力的手段——领域建模。

◆ 第9章 分治

试着以某种方式去组织问题,以便在一个时刻专注于一个特定的部分。这样做的目的是尽量降低在任意时间所要思考问题的复杂度。

◆ 9.1 分治算法

老师介绍的第一个算法思想就是分治算法,这是一种高效、简洁、优美的算法思想。分治算法主要包含两个步骤——分、治。

分治法解题的一般步骤如下。 (1)分解:将要解决的问题划分成若干规模较小的同类问题。 (2)求解:当子问题划分得足够小时,用较简单的方法解决。 (3)合并:按原问题的要求,将子问题的解逐层合并,构成原问题的解。

二分搜索又称为二分查找、折半查找,是一种效率较高的查找方法。

◆ 9.3 写代码的两次创造

以我的实践经验来看,优雅的代码很少是一次成形的,大部分情况下要经过两次创造:第一遍实现功能,第二遍重构优化。

◆ 9.5 分层设计

分层设计是架构体系设计中最常见和重要的一种结构。分层设计最大的好处是分离关注(Separation of concerns)

我们实际在Internet中使用的并不是七层模型,而是TCP/IP四层模型

 图9-3 七层模型和四层

分层架构的目的是通过分离关注点来降低系统的复杂度,同时满足单一职责、高内聚、低耦合、提高可复用性和降低维护成本,也是一种典型的分治思想。

 图9-4 开放的分层架构

◆ 9.6 横切和竖切

因为只有分治才能应对网络高并发,实现水平扩展

 图9-6 数据库横切

◆ 第10章 技术人的素养

未经审视的人生不值得过。 ——苏格拉底

◆ 10.1 不教条

不教条

在软件的世界里没有“银弹”,在技术人的众多素养中,“不教条”占有重要的地位。

教条的主要原因是我们还停留在有样学样的阶段,导致我们忘记了软件的第一性原理是“控制软件复杂度”。

同大多数学科一样,学习编程艺术首先要学会基本的规则,然后才能知道什么时候去打破规则

瀑布还是敏捷

迭代,还是敏捷,一般会经历下面的过程。 (1)需求:对于系统该做什么,建立并保持与客户和其他涉众的一致意见,定义系统的边界。 (2)分析与设计:将需求转化为系统设计,设计将作为在特定实现环境中的规格说明,包括逐渐形成一个健壮的系统架构,建立起系统不同元素必须用到的共同机制。 (3)实现:编码、单元测试以及对设计进行集成,得到一个可执行的系统。 (4)测试:对实现进行测试,确保它实现了需求,通过具体的展示来验证软件产品是否像预期的那样工作。 (5)部署:确保软件产品能被它的最终用户使用。

灰色部分代表一个阶段在当前迭代中所花时间的比重。

 图10-1 改良的迭代开发

简单来说,贫血模式提倡模型对象只包含数据,并提供简单的Getter和Setter;而充血模式提倡数据和行为放在一起,是一种更加面向对象的做法。

单体还是分布式

在业务发展早期,因为用户少、流量少,功能相对简单。如图10-2所示,基本上单体(Monolithic)应用架构就足以支撑业务的发展。

 图10-2 单体应用架构

然而,随着业务的发展和用户的增加,单体应用的局限性开始显现。具有水平扩展性(scale out)的分布式系统架构几乎已经变成互联网公司的标配

 图10-3 分布式应用架构

中台的目的就是要通过中台能力来赋能前线业务,提升对前线业务的支撑效率

 图10-4 中台架构

◆ 10.2 批判性思维

批判性思维

批判性思维(Critical Thinking)是一种谨慎运用推理去断定一个断言是否为真的能力。

批判性思维也是一项能够被习得,并且通过训练和运用来提高的能力。

后来我学习了一些批判性思维的知识,情况才有所改观,可以抓住对方的一些逻辑漏洞和推理谬误进行反驳,这使我在职场上拿回了不少话语主动权。

说的对 不过在家里,我依然是输多赢少,后来我才发现,原来家不是一个讲逻辑的地方。

不过在家里,我依然是输多赢少,后来我才发现,原来家不是一个讲逻辑的地方。

关于训练批判性思维的书有很多,我重点推荐两本,一本是尼尔·布朗写的被誉为批判性思维领域经典读物的《学会提问》,另一本是樊登读书会推荐的《思辨与立场:生活中无处不在的批判性思维工具》。我在读这两本书的时候,经常会有aha moment(顿悟时刻),真的很有收获。

◆ 10.3 成长型思维

成长型思维

研究发现,成长型思维(Growth Mindset)和固定型思维(Fixed Mindset)会极大地影响我们面对逆境的处理方式

她在《终身成长》中提醒我们:我们获得的成功并不是能力和天赋决定的,更多受到我们在追求目标的过程中展现的思维模式的影响。

 图10-5 成长型思维和固定型思维对比图

转变思维模式,会用更加理性的态度看待一时的成败得失,内心坚定地相信成长和学习的力量。

◆ 10.4 结构化思维

结构化思维

思维混乱是缺少结构化思维的典型表现。实际上,我们不仅在表达上要结构化,在分析问题时更要有结构化思维,这样才能分析得更全面、深刻。

什么是结构化思维呢?我给结构化思维的定义就是“逻辑+套路”。

1.逻辑 所谓逻辑,是指结构之间必须是有逻辑关系的

组织思想的逻辑只有4种。

(1)演绎顺序:比如“大前提、小前提、结论”的演绎推理方式就是演绎顺序的。 (2)时间(步骤)顺序:比如“第一、第二、第三”和“首先、再者、然后”等,大多数的时间顺序同时也是因果顺序。 (3)空间(结构)顺序:比如“前端、后端、数据”和“波士顿、纽约、华盛顿”等,化整为零(将整体分解为部分)等都是空间顺序。在做空间分解时,要注意满足“相互独立,完全穷尽”(Mutually Exclusive Collectively Exhaustive,MECE)原则。 (4)程度(重要性)顺序:比如“最重要、次重要、不重要”等。 只要我们的思想和表达在这4种逻辑顺序之内,就是有逻辑的,否则就是没有逻辑的。

2.套路 套路是指我们解决问题的方法论、路径和经验。

比如,5W2H分析法就是非常好的,是可以帮助我们分析问题的一个“套路”。

 图10-6 5W2H问题分析法

逻辑是一种能力,而套路是方法论、经验;逻辑属于道,而方法论属于术

如何落地新团队

要知道对一个企业来说,核心要素无外乎就是业务、技术和人。

 图10-7 落地新团队的策略

熟悉业务 (1)了解产品形态:任何一个团队都有自己要负责的产品,申请一个测试账号去用一下产品,是熟悉产品比较好的方式。 (2)了解业务流程:任何业务都有自己的业务流程,而业务流程中的核心是信息流。

3)走访客户:通过走访客户,我们可以获得业务的第一手资料,更加贴近业务和客户诉求。

熟悉技术 (1)了解系统架构

了解领域模型:查看关键的核心表结构和系统API,快速了解系统的领域模型。

了解代码结构:下载系统工程,熟悉整个工程结构和模块职责

熟悉人 (1)了解组织结构:查看公司的组织树,知道公司大概是如何运作的,以及哪些是关键人(Key Person,KP)

。比如,一个典型的电商公司会包括产品部、运营部、销售部、技术部、人力资源部、财务部和法务部等。

2)了解人员角色:了解公司都有哪些岗位,以及各岗位的职责范围。 (3)勤沟通:找出和自己工作息息相关的岗位,比如产品和运营,积极和这些同事沟通,向他们请教业务问题,多多交流。

如何做晋升述职

最清晰和实用的结构化表达是“提出问题,定义问题,分析问题,解决问题,最后展望未来”。

这也是麦肯锡常用的解决问题的框架。

另一个有用的思维框架是“zoom in/zoom out”。我们说事情时,应该像电影镜头一样,先从远拉近,再由近拉远。“zoom in”是先从宏观背景开始,首先让大家知道你的事情发生的背景,为什么这件事情很重要?然后讲到具体细节,怎么做成的?解决了什么问题?后端思考是什么?最后“zoom out”,从细节调回到整体,结果是什么?带来的客户价值是什么?你对未来的思考是什么?

◆ 10.5 工具化思维

工具化思维

适当的懒比低效的勤奋更具智慧,是更难得的美德。

偷懒也有高低之分,可以分为3个境界。 (1)最差的境界是“实在懒”,拖延不喜欢的任务。 (2)其次是“开明懒”,迅速做完不喜欢的任务,以摆脱之。 (3)最高的境界是“智慧懒”,编写某个工具来完成不喜欢的任务,以便再也不用做这样的事情了,从而一劳永逸。

人们容易混淆行动与进展、混淆忙碌与多产的概念。

在有效的工作中,最重要的是思考,而人在思考时通常看上去不会很忙。如果和我共事的程序员总是忙个不停,我会认为他并非优秀的程序员,因为他没用最有价值的工具——自己的大脑。

有人说程序员和其他行业的最大区别是不仅使用工具,还能创造工具。可不是吗?理发师虽然会理发,但是不会制造剪刀。软件工程师却可以自己创造工具,用来提效,帮助自己更好、更快地完成工作。

我经常在团队中说,每当你重复同样的工作3次以上,就应该停下来问问自己:我是不是可以通过自动化脚本、配置化,或者小工具来帮助自己提效?

一次3分钟不多,但是如果需要在本地频繁启动做测试,就会浪费很多时间。

为此,我写了一个TestContainer的小工具,再配合IDE的热部署功能,在大部分情况下都不用重启服务,这个小小的创新为我和团队带来了极大的便利,节省了很多时间。为此,我还获得了公司当年的“突出贡献奖”。

 图10-8 关于工作效率的漫画

◆ 10.6 好奇心

好奇心 学习的动力不应该来自于外界的强力,而应该来自于内在,来自于我们内心对知识的渴望、对世界的好奇心。

好奇心是创新的驱动力。首先,它使我们灵活思考,打破现有的思维局限

其次,机会总是留给有准备的人

再次,拥有好奇心的人常常是快乐的

你更会因为在工作中获取新知识、新技能、创造价值而快乐

好奇心能使我们在工作中不断学习、积累经验

好奇心是学习的起点

曾写过一篇文章,是关于阿里巴巴所有缩写的英文全称和中文解释的

但是没想到这篇文章到目前为止总共获得了超过35万的浏览数和2000多个赞

做技术这一行,应该没有比持续学习更重要的了。

◆ 10.7 记笔记

记笔记

主持人问一个副总裁:“你成功的秘籍是什么?”这个副总裁只说了一点:“好记性不如烂笔头。

后来在工作中,我每每遇到比较欣赏的人,都能发现他们有一个共同的习惯——勤做笔记。

其必然的因素。首先,做笔记的人基本都是持续学习的人;其次,记录本身也有很多好处。

1)知识内化:记笔记的过程是一个归纳整理、再理解、再吸收的过程,可以加深我们对知识的理解。

2)形成知识体系:零散的知识很容易被遗忘,而形成知识体系之后,知识之间就能有更强的连接。

3)方便回顾:笔记就像我们的硬盘,当缓存失效后,我们依然可以通过硬盘调回,保证知识不丢失。

有觉悟也不算太晚,在这短短两年中,我记了上千篇笔记——从哲学、工作到银行卡密码,可以说我现在的工作和生活已经完全不能脱离笔记了

好处是,我很明显地感受到自己归纳总结的能力和文笔都比以前好了很多。

此,我将自己有限的记笔记经验分享出来,希望这些细节能帮助你提高笔记质量。

1)使用云笔记:云笔记要能在多端使用,要有目录的层次结构、标签和搜索功能。如果有些场合只能用笔做记录,也没关系,回来之后再整理到云笔记上。

2)归类分组:要定期回顾笔记内容,尽量按照合理的方式对笔记进行重组,形成一个有逻辑关系的树形结构。

3)不要复制粘贴:好的笔记最好是自己消化后的总结,而不是简单的照抄。如果有引用和参考,建议把链接也放在笔记下面,方便溯源。

4)结构化表达:对于简短的内容要重点突出,粗体显示重点部分

对于篇幅较长的内容,最好有目录,这样可以更加结构化地呈现笔记内容。

◆ 10.8 有目标

例如,在我进入技术管理岗位之后,不知道后面的方向是什么,是继续在技术上专研呢?还是要研究管理之术呢?直到我再次翻开史蒂芬·柯维的《高效能人士的七个习惯》一书,仔细阅读才发现,“你要做一个什么样的人”并不是一个可有可无的次要问题,而是首先要回答的头等大问题,这时我才意识到目标的重要性。

在《高效能人士的七个习惯》中,柯维博士提到,“所有事物都要经过两次创造的原则,第一次为心智上的创造,第二次为实际的创造”。直观的表达就是:先想清楚目标,然后努力实现。

例如,你需要提高自己的思辨和逻辑能力,那么就应该制定一个学习计划,多去看一些批判思维、逻辑学和哲学的书。

在这个信息爆炸的时代,如果只是碎片化地接收各个公众号推送的文章,学习效果几乎可以忽略不计。

带着问题学习,带着目标学习这种学习模式的效果会比碎片化阅读好得多。

◆ 10.9 选择的自由

选择的自由自由并不是想做什么,就做什么。自由是一种价值观,是一种为自己过去、现在及未来的行为负责的价值观。自由是一种责任,是一种敢于做出选择,并愿意为自己的选择承担后果的责任。

责任感(Responsible),从构词法来说是“能够回应(Response—able)”的意思,即选择回应的能力。

积极主动的人有选择的自由,而消极被动的人往往是被动地接受影响,忘记了自己的主观能动性,忘记了在刺激和回应之间还有选择的自由(There is always a space between stimulus and response)。

[插图]图10-9 积极主动的选择模式

但凡成大事者,都能够“处乱世而不惊,临虚空而不惧,喜迎阴晴圆缺,笑傲雨雪风霜”。正因为他们是自己思维的主人,而不是被思维所控制,他们知道不管身处什么样的境地,都有“选择的自由”。

◆ 10.10 平和的心态

无为之心行有为之事我的座右铭是“动机至善,了无私心;用无为的心,做有为的事”。首先,我们做事情的出发点必须是善的。其次,“有为的事”是指要认真做事,认真生活;“无为的心”代表一种平和的心态,一种活在当下的智慧。也就是做事要积极,但是心态要放平。

。最主要的原因是“心”出了问题,是我太在乎他人的眼光,太在乎面子,太在乎外界的宠辱得失,导致心态失衡。当我放下得失心,让自己平静下来,整个人仿佛获得了重生,我第一次真正感受到什么叫自由,什么叫作生活的主人。

◆ 10.11 精进

精进精进就是你每天必须进步一点点!记住,慢就是快。如图10-10所示,千万不要忽视每天进步一点点的力量,也不要试图一口吃成胖子,真正的进步是滴水穿石的累积,这就是精进。

巴菲特说:“人生就像滚雪球,关键是要找到足够湿的雪,和足够长的坡。”

◆ 10.12 本章小结

好在,过去的磨难都是现在的财富。这些曾经的挫折帮我塑造了成长型思维;培养了我持续学习的习惯;磨炼了我的心智——知道如何放平心态,同时又积极热爱生活;最重要的是给了我自由,让我不再轻易受环境的影响,做了自己命运的主人。

◆ 第11章 技术Leader的修养

Leader,就是走在队伍的最前面,带领者,领路人。——金一南《胜者思维》

(1)什么是Leader?(2)Leader和Manager之间的区别是什么?(3)什么是技术Leader?(4)技术Leader和其他Leader有什么不同?

◆ 11.1 技术氛围

一个技术团队,不管大小,如果没有“技术味道”,那么技术Leader负有很大的责任。“技术味道”的缺失,是目前技术团队存在的最大问题。

代码好坏味道

在我们团队周会中,有一个固定的环节是“代码好坏味道”:当天的会议主持人(我们的周会是轮值主持的,每个团队成员轮流组织一期)要给大家分享3个代码好味道和3个代码坏味道,这些代码既可以是来自我们工作中的代码,也可以是来自开源软件的源码。

晒代码不是关键,关键是通过晒代码,我们可以互相分享写好代码的心得和经验,特别是一些来自开源软件的好味道,对我们写好代码有非常重要的指导意义

技术分享分享是倒逼我们去学习和总结的有效手段。在准备分享的过程中,我们要去阅读很多资料,要把原理弄清楚,还要用别人能听得懂的方式表述出来

CR周报代码审查(Code Review,CR)是保证代码质量和架构风格一致性的重要手段。我们试过很多CR的方式,有Peer Review(点对点地审查),也有Group Review(团队成员一起审查)。

CR周报就是要把CR的结果透明化,通过周报的形式展现团队在一周中的CR成果,包括团队成员的CR评论数排名、代码分支的质量情况,以及CR中的典型案例。

读书会在一个人的能力象限中,我非常看重学习能力。原因很简单,一个人一旦停止了学习,就停止了进步。读书虽然不是学习的唯一方式,但一定是不可或缺的方式。

关于读书会的运作,在此分享以下3点经验。(1)书的范围可以放宽一点,不要只局限在技术类书籍,毕竟除了技术,我们还有很多东西要学。例如,我们最近一次读书会选的书是《高效能人士的七个习惯》。

(2)读书的方式,可以是同读一本书,也可以拆书

(3)读书会的举办频率可以灵活一些

◆ 11.2 目标管理

目标管理目标管理应该是Leader管理事务中最重要的事情之一

一个好的Leader,应该是愿意花时间和下属一起讨论、制定目标的。在过程中,给予帮助和指导,及时对焦纠偏,确保目标的达成。

什么是OKR目标管理的常见手段有关键绩效指标(Key Performance Index,KPI)和目标与关键成果(Objectives Key Results,OKR)两种方法。相比较而言,一味地追求KPI,可能会导致短视;OKR更注重短期利益和长期战略之间的平衡。

OKR主要有如下两个特点。(1)OKR可以不和绩效挂钩,主要强调沟通和方向。(2)OKR比KPI多了一个层级的概念,O(Objective)是要有野心的、有一定的模糊性,但是KR(Key Results)需要是可量化的,并且KR一定要为O服务,不能偏离O的方向。

SMART原则不管是KPI的目标设定,还是OKR的KR设定,都需要满足SMART原则。如图11-3所示,S代表Specific,表示指标要具体;M代表Measurable,表示指标要可衡量;A代表Attainable,表示指标是有可能达成的;R代表Relevant,表示KR和O要有一定的相关性;最后,T代表Time bound,表示指标必须具有明确的截止期限。

[插图]图11-3 目标设定的SMART原则

摩尔定律——“当价格不变时,集成电路上可容纳的元器件的数目大约每隔18~24个月便会增加一倍,性能也将提升一倍”。这是一个堪称完美的SMART目标,引领着Intel半个多世纪的快速发展。

目标是否足够有野心也是区分OKR与KPI的一个标志,KPI拿100分的员工,OKR可能只有0.5分(OKR的得分是0~1分),这才是正常的结果,证明该员工的目标(O)比其他人的KPI要高很多。

[插图]

◆ 11.3 技术规划

技术规划技术规划和目标管理有一点共同之处,都是要在技术团队中理清接下来要做的事情。不同之处在于,技术规划更多的是从团队视角去看接下来要做的事情,而目标管理是要把规划要做的事情进行拆解,和个人目标对齐。对于技术Leader而言,做好技术规划是非常重要的事情,一个团队有没有价值,最终还是要通过做出来的事情来体现。

技术规划是一个大命题。对待这种大问题,我们要分而治之,将其分解成几个不同层次的、相对较小的问题来看。

[插图]图11-4 技术规划的4个层次

当前问题第一层问题解决是最直接的,主要看团队中现在有什么迫切、紧急的问题需要解决,有哪些坑要去填。例如,业务增长比较快、当前架构缺乏弹性、要做服务化拆分、加入分布式缓存、分表分库等。又如,因为代码质量(可读性、可维护性)差,要建立一个代码审查机制,提升代码质量。

技术领域技术领域要做的是在这些常规领域中,根据业务情况和团队情况选择一些领域和命题(比如稳定、性能、效率等),并在这些命题和方向中根据优先级做判断。比如,完善监控体系提升系统稳定性、使用CDN提升性能、通过测试自动化提升研发效率等。

业务领域让业务先赢是技术的首要使命,即使我们身处技术团队,也要充分理解业务、关注业务,要分析业务数据和发展趋势,和业务同事充分交流,总结和抽象出业务的发展对技术会提出什么诉求,需要技术做什么布局和建设以应对业务发展的需求。

团队特色作为技术团队,我们要对比团队内外技术的异同,最终圈定一个差异化区域。这块区域是团队的特色技术,是团队借外力之外要修内功的部分,是不依赖别人而主要靠自己突破的部分,是团队相比外面的差异化竞争力。这一层很重要,对团队的口碑、影响力和稳定性都有较大的影响;同时这一层又是最难的,很多技术团队在这一层次是空白的。

例如,在我的团队中,我们一直把攻克软件复杂度作为首要技术目标,所以在“工匠精神”方面,我们团队在阿里巴巴集团是有一定影响力和口碑的。

◆ 11.4 推理阶梯

推理阶梯在日常生活中,个人的判断大部分基于自身的主观认识而非事实,这会产生许多误会。在企业的日常运转中,管理者在对待员工时也会犯一些主观性的错误。

很多情况下,我们推理别人的“结论”让自己非常生气,但是后来发现事实并非如此,这源于我们习惯用自我推理而非沟通的方式来解决问题。这种推理也被称为“推理阶梯”

“推理阶梯”的发生会经历以下步骤。(1)收集数据:每个人每天都会接受来自外界的大量信息,这些是产生推理的基础。(2)选择性接收数据:尽管我们不愿意承认,但“选择性接收”才是大脑处理信息的固有方式。有句老话:“顺眼的人越看越顺眼,讨厌的人越看越讨厌。”“情人眼里出西施”,说的就是这个道理,没人能避免。

做出假设,得出结论,采取行动:在选择性接收数据之后,我们自然而然地就会想要赋予这些数据意义,从而做出种种假设,并得出相应的结论,然后采取行动,这就是大脑中“推理阶梯”的整个过程

在做决定之前,我们一定要问问自己:“此事是否可能只是我的推理,实际情况并非如此?”

◆ 11.5 Leader和Manager的区别

Leader和Manager的区别简单来说,Manager是管理事务,是控制和权威;而Leader是领导人心,是引领和激发。Leader要做一些Manager的管理事务,但是管理绝对不是Leader工作的全部。

“我们不需要这么多‘高高在上’‘指点江山’的技术Manager,而是需要更多能真正深入系统里面,深入代码细节,给团队带来改变的技术Leader。”

[插图]图11-6 Leader和Manager的区别

技术Leader是专业性非常强的工作。技术Leader区别于其他Leader之处是你不仅要“以德服人”,还要“以技服人”。要带好一个技术团队,技术Leader首先要对技术有热情,有一定的技术能力,并使用一些11.1节中介绍的管理手段,帮助团队成员提升自我,有所成长。

从阿里巴巴的组织角度来看,我们也在强调技术Leader要“重技术、轻管理”

◆ 11.6 视人为人

视人为人在阿里巴巴有句话:“一群有情有义的人,做一件有意义的事”。我很喜欢这句话,俗话说“做事先做人”,我们唯有尊重自己,尊重他人,视人为人,视己为人,对团队倾注感情,和团队成员建立信任关系,才有可能做一个好Leader。

只有和团队建立了情感链接和信任关系,才能更好地开展工作。我们在公司工作,实际上是在给两个账号存钱:一个是绩效货币(PerformanceCurrency),这是对事的;另一个是关系货币(RelationshipCurrency),这是对人的。所有的判断都有人的主观因素在里面,因此第二个货币也很重要。

:“动机至善,了无私心”。我们做事情的出发点必须要是正的、善的。在这个大前提下,我们可以积极地拓展自己的人脉关系和影响力。

视人为人不仅是和他人处好关系,也是一种原则和勇气,你不能视一部分人为“人”,视一部分人为“神”,视一部分人为“物”。

对待上级——有胆量。·对待平级——有肺腑。·对待下级——有心肝。

◆ 11.7 本章小结

做一个Leader不容易,因为你不仅要管好自己,还要成就他人。做一个技术Leader更不容易,因为技术的发展日新月异,你没有退路,如果不持续学习,你就会落伍;如果不深入技术细节,你就很难赢得下属的尊重。

我们很多人在还没有准备好的时候,就被推上了Leader的位置,我本人也是这么过来的。关键是我们要清楚地认识到自己想要什么,要成为什么样的Leader,路走对了,就不怕远。

◆ 第三部分 实践

软件的首要技术使命:管理复杂度。——史蒂夫·迈克康奈尔《代码大全(第2版)》

COLA不仅是一个架构思想,还提供了一整套可以落地实施的框架和工具。

◆ 12.1 软件架构

软件架构架构始于建筑,这是人类发展(原始人自给自足住在树上,也就不需要架构)分工协作的需要。将目标系统按某个原则进行切分,切分的原则是便于不同的角色进行并行工作。

为了更清楚地表述COLA在软件架构中的位置,以及应用开发人员应该关注什么,我特意将软件架构划分成业务架构、应用架构、系统架构、数据架构、物理架构和运维架构,如图12-1所示。[插图]图12-1 软件架构分类

业务架构:由业务架构师负责,也可以称为业务领域专家、行业专家。业务架构属于顶层设计,其对业务的定义和划分会影响组织结构和技术架构。例如,阿里巴巴在没有中台部门之前,每个业务部门的技术架构都是烟囱式的,淘宝、天猫、飞猪、1688等各有一套体系结构。成立了共享平台事业部后,打通了账号、商品、订单等体系,让商业基础实施复用成为可能。

应用架构:由应用架构师负责,他需要根据业务场景的需要,设计应用的拓扑结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速地支撑业务发展的同时,确保系统的可用性和可维护性。COLA架构是一个典型的应用架构,致力于应用复杂度的治理。

系统架构:根据业务情况综合考虑系统的非功能属性要求(包括性能、安全性、可用性、稳定性等),然后做出技术选型。对于流行的分布式架构系统,需要解决服务器负载、分布式服务的注册和发现、消息系统、缓存系统、分布式数据库等问题,同时解决如何在CAP(Consistency,Availability,Partition Tolerance)定理之间进行权衡的问题。

数据架构:对于规模大一些的公司,数据治理是一个很重要的课题。如何对数据收集、处理,提供统一的服务和标准,是数据架构需要关注的问题。其目的就是统一数据定义规范,标准化数据表达,形成有效易维护的数据资产,搭建统一的大数据处理平台,形成数据使用闭环。

物理架构:物理架构关注软件元件是如何放到硬件上的,包括机房搭建、网络拓扑结构、网络分流器、代理服务器、Web服务器、应用服务器、报表服务器、整合服务器、存储服务器和主机等。

运维架构:负责运维系统的规划、选型、部署上线,建立规范化的运维体系。要借助技术手段控制和优化成本,通过工具化及流程提升运维效率,注重运营效益。制定和优化运维解决方案,包括但不限于柔性容灾、智能调度、弹性扩容与防攻击、推动及开发高效的自动化运维和管理工具、提高运维的自动化程度和效率。

◆ 12.2 典型的应用架构

典型的应用架构

分层架构分层是一种常见的根据系统中的角色(职责拆分)和组织代码单元的常规实践。

分层架构大概经历了以下阶段。

还不需要分层,实际上也只有一层。

开始出现C/S两层架构模式。

20世纪90年代之后:互联网开始普及,随着用户的增长,以及应用复杂性和基础实施复杂性的增加,终于诞生了我们现在仍在使用的三层架构,也叫N层应用架构

 图12-2 N层应用架构

2000年之后:2003年,Eric Evans出版了他的标志性著作《领域驱动设计:软件核心复杂性应对之道》。从此,DDD的概念被人熟悉,以及基于DDD的一系列架构演变开始出现。

如果在我们的系统中出现以下情况,就可能有“千层面”的嫌疑。 ·热衷于创建完美的系统导致项目过度抽象。 ·层次太多,增加了整个系统的复杂性。 ·物理层次太多,不但增加了整个系统的复杂性,还降低了系统的性能。 ·严格的分层方法导致上层必须通过中间层次访问,而不是直接访问需要的层次。

CQRS 命令查询分离(Command Query Separation,CQS)最早是Betrand Meyer(Eiffel语言之父,OCP的提出者)提出的概念,其基本思想在于任何一个对象的方法可以分为以下两类。

命令(Command):不返回任何结果(void),但会改变对象的状态。 ·查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。

命令查询职责分离模式(Command Query Responsibility Segregation,CQRS)是对CQS模式的进一步改进而成的一种简单架构模式。

该模式从业务上分离修改(Command,增、删改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为,从而使得逻辑更加清晰,便于对不同部分进行有针对性的优化。

CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来

 图12-3 CQRS架构

cqrs 的好处,读写分离,代码清晰使用了CQRS之后,我们能够把读模型和写模型完全分开,从而可以优化读操作和写操作。除了性能提升,CQRS还让代码库更清晰简洁,更能体现出领域,易于维护。

使用了CQRS之后,我们能够把读模型和写模型完全分开,从而可以优化读操作和写操作。除了性能提升,CQRS还让代码库更清晰简洁,更能体现出领域,易于维护。

六边形架构 六边形架构由Alistair Cockburn在2005年提出,解决了传统的分层架构所带来的问题。实际上,六边形架构也是一种分层架构,只不过不是上下,而是内部和外部

 图12-4 六边形架构的内外分层

六边形架构又称端口-适配器架构,这个名字更容易理解。六边形架构将系统分为内部(内部六边形)和外部,内部代表应用的业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。

内部不关心外部如何使用端口,从一开始就要假定外部使用者是可替换的。

适配器的两种不同类型如图12-5所示,左侧代表UI的适配器被称为主动适配器(Driving Adapters),因为是它们发起了对应用的一些操作;右侧表示和后端工具链接的适配器被称为被动适配器(Driven Adapters),因为它们只会对主适配器的操作做出响应。

 图12-5 六边形架构的端口和适配器

端口和它的具体实现(用例)都在应用内部。

洋葱架构 2008年,Jeffrey Palermo提出了洋葱架构(Onion Architecture)。在我看来,洋葱架构在端口和适配器架构的基础上贯彻了将领域放在应用中心,将驱动机制(用户用例)和基础设施(ORM、搜索引擎、第三方API等)放在外围的思路。洋葱架构在六边形架构的基础上加入了内部层次。

通过编写适配器代码将应用核心从对基础设施的关注中解放出来,避免基础设施代码渗透到应用核心之中。这样应用使用的工具和传达机制都可以轻松地替换,在一定程度上避免技术、工具或者供应商锁定。

 图12-6 洋葱架构

在洋葱架构中,明确规定了依赖的方向。 ·外层依赖内层。 ·内层对外层无感知。

耦合的方向是从外层指向中心的。

洋葱架构提供了一个完全独立的对象模型(领域模型),该模型位于架构的核心,不依赖其他任何层次,我们可以在不影响内层的情况下改变外层的灵活性。洋葱架构在架构层面运用了依赖倒置原则。

准确地说,DDD不是架构,而是一种开发思想。就像敏捷不是Scrum,而是一种思想一样。

DDD带来的最大改变是让我们得以从“数据驱动”转向“领域驱动”,让我们知道领域是应用的核心,其他都是技术细节,随时可以被替换。

◆ 12.3 COLA架构设计

然而我们又不是对前人思想的简单结合,而是融入了很多我们自己的思考、判断和创新,比如扩展点设计和规范设计。

 图12-7 传统的分层架构与COLA分层架构

展现层(Presentation Layer):负责以Rest的格式接受Web请求,然后将请求路由给Application层执行,并返回视图模型(View Model),其载体通常是数据传输对象(Data Transfer Object,DTO)

(2)应用层(Application Layer):主要负责获取输入、组装上下文、做输入校验、调用领域层做业务处理,当需要时发送消息通知。当然,层次是开放的,若有需要,应用层也可以直接访问基础实施层。

(3)领域层(Domain Layer):主要封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Entities)的函数对外部提供业务逻辑的计算和处理。

(4)基础设施层(Infrastructure Layer):主要包含数据访问通道(Tunnel)、Config和Common。

使用Tunnel这个概念对所有的数据来源进行抽象,数据来源可以是数据库(MySQL、NoSQL)、搜索引擎、文件系统,也可以是SOA服务等;Config负责应用的配置;Common是通用的工具类。

“业务逻辑”可以被分层“核心业务逻辑”和“技术细节”

这正是六边形架构和洋葱架构中提倡的思想,即尽量保证内部核心领域的独立和无依赖,而外部的技术细节可以通过接口和适配器随时更换,从而增加系统的灵活性和可测性。

在扩展设计中,我们提炼出两个重要的概念,分别是业务身份和扩展点。

业务身份是指在系统唯一标识一个业务或者一个场景的标志。在具体实现中,我们使用BizCode来表示业务身份,采用类似Java包名命名空间的方式。例如,用“ali.tmall”表示阿里巴巴天猫业务,用“ali.tmall.car”表示阿里巴巴天猫的汽车业务,用“ali.tmall.car.aftermarket”表示阿里巴巴天猫的汽车业务的后市场场景。

扩展点的思想源自5.4节中介绍的插件模式。每个业务或者场景都可以实现一个或多个扩展点(ExtensionPoint)

这个业务身份和扩展点的组合,我们称为扩展坐标(ExtensionCoordinate)

 图12-8 业务身份和扩展点

任何事物都是规则性和随机性的组合。规范的意义在于可以将规则性的东西固化下来,尽量减少随心所欲带来的复杂度,一致性可以降低系统复杂度。从命名到架构皆是如此,架构本身就是一种规范和约束,破坏这个约束,也就破坏了架构。

1.组件规范 COLA规定一个应用至少要有3个组件:应用层、领域层和基础实施层。如果不是严格的前后端分离,也可以加入展现层的组件,但这是可选的。组件之间的依赖关系如图12-9所示。

 图12-9 COLA的组件规范

领域组件的实现方式有两种,一种是把领域组件设计成纯POJO,另一种是通过依赖倒置,将数据访问的接口放在领域组件里,让基础设施组件(Infrastructure Module)去做接口实现。

包规范 相比组件,包是更细粒度的代码组织单元。包的设计也要遵循高内聚、低耦合的原则。每个包都应该是一组功能类似的类的聚集,这种划分使得整个项目形成一个金字塔结构(参考8.5.3节)。

   图12-10 COLA的包规范

命名规范 在COLA架构中,我们制定了一系列的命名规范,以便通过名称就能知晓该类的作用和职责范围,从而极大地提升代码的可理解性,提升代码审查(Code Review)的效率。

表12-1 类名规范 

例如,我们要生成一个名字叫demo的应用,只需要执行下面的Maven命令: mvn archetype:generate -DgroupId=com.alibaba.sample //demo应用的groupId -DartifactId=demo //demo应用的artifactId -Dversion=1.0.0-SNAPSHOT //demo应用的版本号 -Dpackage=com.alibaba.sample //demo应用的package名 -DarchetypeArtifactId=cola-framework-archetype -DarchetypeGroupId=com.alibaba.cola -DarchetypeVersion=1.0.0-SNAPSHOT

 图12-11 demo的组件依赖

◆ 12.4 COLA测试

单元测试,是指对软件中的最小可测试单元进行检查和验证,应该具备以下特点。 ·粒度要小:其测试对象通常是一个函数,最大也不应该超过一个类。 ·速度要快:其运行速度要极快,应该都是在毫秒级完成。

集成测试是在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统,进行集成测试。 集成测试的特点如下。 ·集成测试是以模块和子系统为单元进行的测试,是黑盒测试。 ·集成测试主要测试接口层的测试空间,单元测试主要测试内部实现层的测试空间。

 图12-12 ColaMock的工作原理

◆ 12.5 COLA架构总览

在架构思想上,COLA主张像六边形架构那样,使用端口-适配器去解耦技术细节;主张像洋葱架构那样,以领域为核心,并通过依赖倒置反转领域层的依赖方向。最终形成图12-13的层次关系和依赖关系。

 图12-13 COLA层次关系和依赖关系图

从COLA应用处理响应一个请求的过程来看,COLA使用了CQRS来分离命令和查询的职责,使用扩展点和元数据来提供更高应用的可扩展性,使用ColaMock来提升测试效率。我们可以得到一张如图12-14所示的COLA纵向架构图。

 图12-14 COLA架构图

◆ 12.6 本章小结

倘若你的业务场景分支不复杂,那么就可以选择不使用扩展点功能。甚至面对简单的CRUD(增、删、改、查)情况,领域层也可以是一个可选项。

软件的世界里没有银弹,我们不一定要使用COLA提供的所有能力,可以根据实际情况裁剪使用COLA。

◆ 13.1 项目背景

让业务先赢应该是我们的底线,而不是全部。文章中还提到,我们要有工匠精神,鼓励大家通过学习、实践、分享不断提升编码能力和设计能力。用工匠精神写每一行代码,而不是简单地通过代码堆砌实现业务功能来交差。

工匠平台”会收集这些技术指标,并对其进行评分和统计,从而提供一个更加全面的技术人员画像,更加客观地反映技术人员的技术贡献。

◆ 13.2 整理需求

整理需求 工匠平台的核心是要构建一套对技术人员进行评测的度量体系,量化技术人员的技术贡献,从而激励技术人员在完成任务之余,投入更多的精力去写出更好的代码。

经过讨论,我们定义了4个大的维度作为技术人员的KPI指标,分别是应用质量、技术影响力、技术贡献和开发质量。

表13-1 技术度量表 

◆ 13.4 使用COLA

安装COLA (1)下载COLA。 COLA的开源地址是https://github.com/alibaba/COLA,可以使用git clone复制到本地。 (2)安装cola-framework。 进入cola-framework,运行maven install安装COLA框架到本地。 (3)安装cola-archetype。 进入cola-archetype,运行maven install安装COLA Archetype到本地。

搭建应用 我们可以使用Cola Archetype为工匠平台(cratfsman)创建一个后端应用,使用如下命令: mvn archetype:generate -DgroupId=com.alibaba.craftsman -DartifactId= craftsman -Dversion=1.0.0-SNAPSHOT -Dpackage= com. alibaba. craftsman -DarchetypeArtifactId=cola-framework-archetype -DarchetypeGroupId= com. alibaba. cola -DarchetypeVersion=1.0.0-SNAPSHOT

运行命令后,在生成的项目目录中可以看到如下的目录结构。 ·craftsman-controller:控制器,主要以REST的方式接受前端的HTTP请求,然后路由到对应的Service进行处理。 ·craftsman-client:给外部服务进行RPC调用的SDK。 ·craftsman-app:App层,用于接受request进行通用的逻辑处理,调用Domain进行业务处理。 ·craftsman-domain:核心业务逻辑实现。 ·craftsman-infrastructure:基础实施层,负责数据CRUD操作。 ·start:负责SpringBoot的启动和基于ColaMock的集成测试。

◆ 13.5 领域模型

领域建模 工匠平台的核心领域概念是员工、度量和分数。

其中,一个员工总是归属于一个团队,员工档案应该包含一组度量,每一个度量都能计算分数。

 图13-3 工匠平台的领域模型

 图13-4 工匠平台模型演化V1

需要注意的是,在工作中不一定都是先建模后写代码的过程,代码和模型的迭代是交替、螺旋式前进的。

图13-5 工匠平台模型演化V2

比如“技术影响力”这个度量,实际上是由“ATA文章”“分享”“专利”和“论文”4个子度量组成的。

因此,我们又引入了主度量(MainMetric)和子度量(SubMetric)的概念。

 图13-6 工匠平台领域模型最终版

领域词汇表 在业务讨论和领域建模的过程中,我们逐渐形成了一套描述该领域的词汇表。

team:团队,一个团队由一个或多个员工组成。

profile:员工信息,包含员工的所有度量信息。

metric:通用的度量概念。

mainMetric:主度量,最顶层的度量,比如“技术影响力”。

subMetric:子度量,主度量可以包含一到多个子度量,比如“技术影响力”下面的“ATA文章”。

metricItem:度量项,一个子度量可以包含一到多个度量项,比如每一篇具体的ATA文章就是一个度量项。

score:分数,所有的度量都可以计算出分数。

weight:权重,不同度量项所占的权重会不一样。

这套词汇表就是DDD中的“统一语言”。在后续讨论、设计,以及编码过程中,我们都应该遵从这套词汇表,做到“一个团队,一种语言”,这样会极大地提升代码的表达能力和可理解力。

◆ 13.6 核心业务逻辑

根据COLA的架构思想,核心业务逻辑是完全独立的,不依赖任何技术细节。也就是说,不管你的数据存储是使用MySQL还是MongoDB,对外API是使用REST还是RPC,都不会影响我编写核心业务代码。

 图13-7 度量项的类图

主度量中最重要的逻辑是加权求和各个子度量的分数,从而得到该主度量的分数:

读者可以在GitHub中获取有关工匠平台的完整代码,网址为https://github.com/alibaba/COLA/tree/master/sample/craftsman

◆ 13.7 实现技术细节

数据存储 到目前为止,我们还没有做任何与数据存储相关的事情,但这并不妨碍编写核心业务逻辑代码,这也是我们把数据库列为“技术细节”的原因

。在实现核心业务逻辑时,我们不需要关心数据是怎么存储的,无论是存在内存、数据库,还是文件系统中,都不应该影响到业务逻辑的实现。

领域模型关注的是业务抽象,采用面向对象分析和设计的方法论;而数据模型关心的是数据存储。

以工匠平台为例,其数据模型只需要user_profile和metric这两张表(见图13-9),要比领域模型简单得多。

 图13-9 工匠平台的数据模型

特别是在上述案例中,领域模型中的10余个对象,落到数据层面,只是两张数据表而已。

控制器是模型-视图-控制器(Model-View-Controller,MVC)模式中的重要概念,主要负责前端请求的路由和转发,是链接前端和后端的桥梁。

现在大部分公司已经采用前后端分离的架构模式,提倡将视图和控制器都交由前端负责。因此,控制器并不是后端服务的必选项,用NodeJs实现也是一个不错的选择。

对于工匠平台而言,使用SpringMVC的RestController来实现控制器会更加简单。就像奥卡姆说的“无有必要,勿增实体”,能简单做的事情,就不要复杂化。

(1)RestController相当于@Controller+@ResponseBody两个注解的结合,默认返回json。 (2)@GetMapping是一个组合注解,用于响应HTTP的GET请求,等价于@RequestMapping(method = RequestMethod.GET) 。 (3)@PostMapping是一个组合注解,用于响应HTTP的POST请求,等价于@RequestMapping(method = RequestMethod.POST) 。

◆ 13.8 测试

单元测试 单元测试必须要满足两个前提:测试范围要小,运行速度要快。

阅读业务逻辑代码的实现,我们可以看到代码是纯POJO的,除了JDK以外,没有任何其他的依赖。

12.4.2节中介绍过,业务系统中集成测试的主要痛点在于模拟(Mock)成本很高,因此我们自研了ColaMock

@ColaMockConfig(mocks={MetricTunnel.class})表示我们需要对MetricTunnel中的方法调用进行Mock。 ·添加@RunWith(ColaTestRunner.class)注解,这样在Run Test时就会自动使用录制的数据进行回放。

回归测试 我们花费大量的精力编写的单元测试和集成测试,除了满足TDD的需要,另一个主要价值体现在回归上。

只要回归测试全部通过,我们就能进行提测。如果测试覆盖率足够高,甚至可以跳过QA的测试,直接进行预发布验证。

◆ 13.9 本章小结

本章通过工匠平台的实践,介绍了一个业务项目是如何使用COLA进行业务开发的。作为一个框架,COLA可以帮我们快速搭建业务应用;作为规范,COLA给我们提供了一些好的实践和工作指导;作为一种以领域为核心的编程思想,COLA可以指导我们用更加面向对象的方式进行设计编程。

◆ 点评

本书作者有着丰富的阿里工作经验,经历了众多高复杂度的项目架构设计,再加上自身对代码质量的高要求,对好的技术味道的崇尚,经过一系列的自我思考,优秀前辈的借鉴以及亲身实践,逐步沉淀出一套适用于大多数场景的整洁架构的方法论和运行框架。 本书文笔精炼,讲述了整洁架构的道,术,器,专业知识输出的同时,体现了个人的思考路径和结果,也传达了众多普世的理念和方法论。值得软件工程师阅读,对硬软能力的提升都会有很大帮助。 这本书我给打四分,一分给精炼的表达,清晰的叙事脉络,容易让读者接受,一分给专业且新颖的知识和产品输出,对读者来说软硬实力都具有可操作性,一分给作者的分享精神,代码开源以及个人方法论的输出,最后一分作为对作者的支持,书是优秀的,希望作者未来可以产出更多优秀的内容。