代码精进之路:从码农到工匠
读书笔记
第一章技艺
代码命名规范的意义:
由此可见,事物的复杂程度在很大程度上取决于其有序程度,减少无序能在一定程度上降低复杂度,这正是规范的价值所在。通过规范,把无序的混沌控制在一个能够理解的范围内,从而帮助我们减少认知成本,降低对事物认知的复杂度。
第二章 代码规范
1、命名规范
当前的主流编程语言有50种左右,分为两大阵营——面向对象和面向过程;按照变量定义和赋值的要求,又可分为强类型语言和弱类型语言。每种语言都有自己独特的命名风格,有些语言在定义时提倡以前缀来区分局部变量、全局变量和变量类型。例如,JavaScript是弱类型语言,所以其中会有匈牙利命名法的习惯,用li_count表示local int局部整形变量,使用$给jQuery的变量命名。语言的命名风格多样,无可厚非,但是在同一种语言中,如果使用多种语言的命名风格,就会令其他开发工程师反感。在Java中,我们通常使用如下命名约定。
·类名采用“大驼峰”形式,即首字母大写的驼峰,例如Object、StringBuffer、FileInputStream。
·方法名采用“小驼峰”形式,即首字母小写的驼峰,方法名一般为动词,与参数组成动宾结构,例如Thread的sleep(long millis)、StringBuffer的append(String str)。
·常量命名的字母全部大写,单词之间用下划线连接,例如TOTALCOUNT、PAGESIZE等。
·枚举类以Enum或Type结尾,枚举类成员名称需要全大写,单词间用下划线连接,例如SexEnum.MALE、SexEnum.FEMALE。
·抽象类名使用Abstract开头;异常类使用Exception结尾;实现类以impl结尾;测试类以它要测试的类名开始,以Test结尾。
·包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词,包名统一使用单数形式。通常以com或org开头,加上公司名,再加上组件或者功能模块名,例如org.springframework.beans。
2、日志规范
日志的重要性很容易被开发人员忽视,写好程序的日志可以帮助我们大大减轻后期维护的压力。在实际工作中,开发人员往往迫于时间压力,认为写日志是一件非常烦琐的事情,往往没有足够的重视,导致日志文件管理混乱、日志输出格式不统一,结果在出现故障时影响工作效率。开发人员应在一开始就养成良好的撰写日志的习惯,并在实际的开发工作中为写日志预留足够的时间。在打印日志时,要特别注意日志输出级别,这是系统运维的需要。详细的日志输出级别分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定义的级别。我认为比较有用的4个级别依次是ERROR、WARN、INFO和DEBUG。通常这4个级别就能够很好地满足我们的需求了。
3、 异常规范
异常处理很多的应用系统因为没有统一的异常处理规范,增加了人为的复杂性,具体体现在以下两个方面。(1)代码中到处充斥着异常捕获的try/catch的代码,搞乱了代码结构,把错误处理与正常流程混为一谈,严重影响了代码的可读性。(2)异常处理不统一,有的场景对外直接抛出异常,有的场景对外返回错误码,这种不一致性让服务的调用方摸不着头脑,增加了服务的使用成本和沟通成本。针对以上问题,我建议在业务系统中设定两个异常,分别是BizException(业务异常)和SysException(系统异常),而且这两个异常都应该是Unchecked Exception。为什么不建议用Checked Exception呢?因为它破坏了开闭原则。如果你在一个方法中抛出了Checked Exception,而catch语句在3个层级之上,那么你就要在catch语句和抛出异常处理之间的每个方法签名中声明该异常。这意味着在软件中修改较低层级时,都将波及较高层级,修改好的模块必须重新构建、发布,即便它们自身所关注的任何东西都没有被改动过。这也是C#、Python和Ruby语言都不支持Checked Exception的原因,因为其依赖成本要高于显式声明带来的收益。最后,针对业务异常和系统异常要做统一的异常处理,类似于AOP,在应用处理请求的切面上进行异常处理收敛,其处理流程如下:
try{
//业务处理
Response res = process(request);
}
catch(BizException e){
//业务异常使用warn级别
Logger.warn(“EizException”,e.getErrorMsg());
}
catch (SysException ex){
//系统异常使用error级别
Logger.error(“System error”+ex.getMessage(),ex)
}
catch(Exception ex){
//兜底
Logger.error(“System error”+ex.getMessage(),ex);
}
第三章 函数
单一职责
函数要尽可能的短小,java语言最好不要超过20行。
职责单一:一个方法只做一件事。(作用:提高代码的可读性,职责越单一,功能越内聚,提升代码的可复用性。)
优化判空
抽象层次一致性(SLAP)
组合函数要求将一个大函数拆分成多个子函数组合,SLAP要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,就会显得凌乱,难以理解。
举个例子,假如有一个冲泡咖啡的原始需求,其制作咖啡的过程分为3步。
(1)倒入咖啡粉。
(2)加入沸水。
(3)搅拌。
public void makeCoffe(){
pourCoffeePower();
pourWater();
stir();
}
如果要加入新的需求,比如需要允许选择不同的咖啡粉,以及选择不同的风味,那么代码就会变成这样:
public void makeCoffe(Boolean isMikCoffee, Boolean isSweetTooth, CoffeeType type){
//选择咖啡粉
if(type==CAPPUCCTION{
pourCappuccionPowder();
}
else if(type == BLACK){
pourBlackPower();
}
……
//加入沸水
pourWater();
//选择口味
if(isMikCoffee){
pourMik();
}
if(isSweetTooth){
addSugar();
}
//搅拌
Stir();
}
按照组合函数和SLAP原则,我们要在入口函数中只显示业务处理 的主要步骤。具体的实现细节通过私有方法进行封装,并通过抽象层次 一致性来保证,一个函数中的抽象在同一个水平上,而不是高层抽象和 实现细节混杂在一起。
根据SLAP原则,我们可以将代码重构为:
public void makeCoffe(Boolean isMikCoffee, Boolean isSweetTooth, CoffeeType type){
//选择咖啡粉
pourCoffeePowder(type);
//加入沸水
pourWater();
//选择口味
flavor(isMilkCoffee,isSweetTooth);
//搅拌
Stir();
}
private void flavor(boolean isMilkCoffee, boolean isSweetTooth){
if(isMilkCoffee){
pourMilk();
}
if(isSweetTooth){
addSugar();
}
}
private void pourCoffeePowder(CoffeeType type){
//选择咖啡粉
if(type==CAPPUCCTION{
pourCappuccionPowder();
}
else if(type == BLACK){
pourBlackPower();
}
……
}
重构后的makeCoffee()又重新变得整洁如初了,满足SLAP实际上是构筑了代码结构的金字塔。金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。关于金字塔原理的更多内容,请参考8.5.3节。在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。在这一点上,金字塔原理和SLAP是相通的,世界就是如此奇妙,很多道理在不同的领域同样适用。
第四章 设计原则
原则:所谓原则,就是一套前人通过经验总结出来的,可以有效解决问题的指导思想和方法论。遵从原则,可以事半功倍;反之,则有可能带来麻烦。需要注意的是,和其他道理一样,原则并非是形而上学的静态客观真理,不是说每一个设计都要教条地遵守每一个原则,而是要根据具体情况进行权衡和取舍。
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)就越高,被复用的可能性就越大,也更容易被理解。
4.3 开闭原则OCP
为什么OCP这么重要?因为可扩展性是我们衡量软件质量的一个重要指标。在软件的生命周期内,更改是难免的,如果有一种方案既可以扩展软件功能,又可以不修改原代码,那是我们梦寐以求的。因为不修改就意味着不影响现有业务,新增的代码不会对既有业务产生影响,也就不会引发漏洞。在面向对象设计中,我们通常通过继承和多态来实现OCP,即封装不变部分。对于需要变化的部分,通过接口继承实现的方式来实现“开放”。因此,区别面向过程语言和面向对象语言最重要的标志就是看它是否支持多态。
实际上,很多的设计模式都以达到OCP目标为目的。例如,装饰者模式,可以在不改变被装饰对象的情况下,通过包装(Wrap)一个新类来扩展功能;策略模式,通过制定一个策略接口,让不同的策略实现成为可能;适配器模式,在不改变原有类的基础上,让其适配(Adapt)新的功能;观察者模式,可以灵活地添加或删除观察者(Listener)来扩展系统的功能。当然,要想做到绝对地“不修改”是比较理想主义的。因为业务是不确定的,没有谁可以预测到所有的扩展点,因此这里需要一定的权衡,如果提前做过多的“大设计”,可能会犯YAGNI(You Ain’t Gonna Need It)的错误。
4.4 里氏替换原则LSP
程序中的父类型都应该可以正确地被子类型替换。LSP认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”,即子类应该可以替换任何基类能够出现的地方,并且经过替换后,代码还能正常工作。根据LSP的定义,如果在程序中出现使用instanceof、强制类型转换或者函数覆盖,很可能意味着是对LSP的破坏。
4.4.1 警惕instanceof
如果我们发现代码中有需要通过强制类型转换才能使用子类函数的情况,或者要通过instanceof判断子类类型的地方,那么都有不满足LSP的嫌疑。出现这种情况的原因是子类使用的函数没有在父类中声明。在程序中,通常使用父类来进行定义,如果一个函数只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该函数。可以通过提升抽象层次来解决此问题,也就是将子类中的特有函数用一种更抽象、通用的方式在父类中进行声明。这样在使用父类的地方,就可以透明地使用子类进行替换了,具体做法请参考8.5.2节。
4.4.2 子类覆盖父类函数
子类方法覆盖(Override)了父类方法,并且改变了其含义。这样在做里氏替换时,就会出现意想不到的问题。
4.5 接口隔离原则ISP
多个特定客户端接口要好于一个宽泛用途的接口。接口隔离原则认为不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。
做接口拆分时,我们也要满足单一职责原则,让一个接口的职责尽量单一,而不是像图4-4中那样无所不包。满足ISP之后,最大的好处是可以将外部依赖减到最少。你只需要依赖你需要的东西,这样可以降低模块之间的耦合(Couple)。
4.6 依赖倒置原则DIP
模块之间交互应该依赖抽象,而非实现。DIP要求高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象。
类不是孤立的,一个类需要依赖于其他类来协作完成工作。但是这种依赖不应该是特定的具体实现,而应该依赖抽象。也就是我们通常所说的要“面向接口编程”。
依赖倒置,就是要反转依赖的方向,让原来紧耦合的依赖关系得以解耦,这样依赖方和被依赖方都有更高的灵活度。
4.7 Don’t Repeat Yourself
系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能。在8.5.1节中,我们通过创建缺失的抽象来消除重复代码,就是一个很好的DRY案例。
贯彻DRY可以让我们避免陷入“散弹式修改(Shotgun Surgery)”的麻烦,“散弹式修改”是Robert Martin在《重构》一书中列出的一个典型代码“坏味道”,由于代码重复而导致一个小小的改动,会牵扯很多地方。
4.8 You Ain’t Gonna Need It
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在著名的平衡计分卡理论中提出。他认为把事情变复杂很简单,把事情变简单很复杂。好的目标不是越复杂越好,反而是越简洁越好。KISS原则被运用到软件设计领域中,常常会被误解,这成了很多没有设计能力的工程人员的挡箭牌。在此,我们一定要理解“简单”和“简陋”的区别。真正的“简单”绝不是毫无设计感,上来就写代码,而是“宝剑锋从磨砺出”,亮剑的时候犹如一道华丽的闪电,背后却有着大量的艰辛和积累。真正的简单,不是不思考,而是先发散、再收敛。在纷繁复杂中,把握问题的核心。
4.11 POLA原则
POLA(Principle of least astonishment)是最小惊奇原则,写代码不是写侦探小说,要的是简单易懂,而不是时不时冒出个“Surprise”。在《复杂》一书的第7章“度量复杂性”中,就阐述了用“惊奇度”来度量复杂度的方法,“惊奇度”越高,复杂性越大,这也是侦探小说要比一般小说更“烧脑”的原因。
个人感觉与KISS 原则重复
第五章 设计模式
设计模式(Design Pattern)是一套代码设计经验的总结,并且该经验必须能被反复使用,被多数人认可和知晓。设计模式描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案,具有一定的普遍性,可以反复使用。其目的是提高代码的可重用性、可读性和可靠性。设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。正确使用设计模式,可以提高程序员的思维能力、编程能力和设计能力,使程序设计更加标准化、代码编制更加工程化,从而大大提高软件开发效率。
拦截器模式
拦截器模式(Interceptor Pattern),是指提供一种通用的扩展机制,可以在业务操作前后提供一些切面的(Cross-Cutting)的操作。这些切面操作通常是和业务无关的,比如日志记录、性能统计、安全控制、事务处理、异常处理和编码转换等。在功能上,拦截器模式和面向切面编程(Aspect Oriented Programming,AOP)的思想很相似。不过,相比于AOP中的代理实现(静态代理和动态代理),我更喜欢拦截器的实现方式,原因有二:一个其命名更能表达前置处理和后置处理的含义,二是拦截器的添加和删除会更加灵活,如图5-3所示。
插件模式
管道模式
管道这个名字源于自来水厂的原水处理过程。原水要经过管道,一层层地过滤、沉淀、去杂质、消毒,到管道另一端形成纯净水。我们不应该把所有原水的过滤都放在一个管道中去提纯,而应该把处理过程进行划分,把不同的处理分配在不同的阀门上,第一道阀门调节什么,第二道调节什么……最后组合起来形成过滤纯净水的管道。这种处理方式实际上体现了一种分治(Divid and Conquer)思想,这是一种古老且非常有效的思想。关于分治思想,将会在第9章中详细介绍。接下来,我们来看管道模式的实际应用。
5.5.1 链式管道
看过Tomcat源码或阿里巴巴开源的MVC框架WebX源码的读者,应该对其中的管道(Pipeline)和阀门(Valve)不会陌生。一个典型的管道模式,会涉及以下3个主要的角色。(1)阀门:处理数据的节点。(2)管道:组织各个阀门。(3)客户端:构造管道并调用。
对应现实生活中的管道,我们一般使用一个单向链表数据结构作为来实现。
5.5.2 流处理
管道模式还有一个非常广泛的应用——流式处理,即把自来水厂的原水换成数据,形成数据流。管道模式适用于那些在一个数据流上要进行不同的数据计算场景,这种方式称为流处理,也称为流式计算。流是一系列数据项,一次只生成一项。程序可以从输入流中逐个读取数据项,然后以同样的方式将数据项写入数据流。一个程序的输出流很有可能是另一个程序的输入流。
熟悉UNIX或Linux命令的读者对管道应该不会陌生,管道(|)是把一个程序的输出直接连接到另一个程序的输入命令符,这样就能方便快捷地进行流式数据处理,UNIX的cat命令会把两个文件连接起来创建流,tr会转化流中的字符,sort会对流中的行进行排序,而tail -3则给出流的最后3行。
第六章 模型
第七章 领域驱动设计 DDD
DDD 是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。
DDD的革命性在于领域驱动设计是面向对象分析的方法论,它可以利用面向对象的特性(封装、多态)有效地化解复杂性,而传统J2EE或Spring+Hibernate等事务性编程模型只关心数据。这些数据对象除了简单的setter/getter方法外,不包含任何业务逻辑,业务逻辑都是以过程式的代码写在Service中。这种方式极易上手,但随着业务的发展,系统也很容易变得混乱复杂。
7.4 DDD的优势
7.4.1 统一语言
统一语言(Ubiquitous Language)的主要思想是让应用能和业务相匹配,这是通过在业务与代码中的技术之间采用共同的语言达成的。业务语言起源于公司的业务侧,业务侧拥有需要实现的概念。业务语言中的术语由公司的的业务侧和技术侧通过协商来定义(意味着业务侧也不能总是选到最好的命名),目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言。代码、类、方法、属性和模块的命名必须和统一语言相匹配,必要的时候需要对代码进行重构!
7.4.2 面向对象
DDD的核心是领域模型,这一方法论可以通俗地理解为先找到业务中的领域模型,以领域模型为中心,驱动项目开发。领域模型的设计精髓在于面向对象分析、对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析的大师。
DDD鼓励我们接触到需求后第一步就是考虑领域模型,而不是将其切割成数据和行为,然后用数据库实现数据,用服务实现行为,最后造成需求的首尾分离。DDD会让你首先考虑业务语言,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。重点不同,导致编程世界观不同。
7.4.4 分离业务逻辑和技术细节
代码复杂度是由业务复杂度和技术复杂度共同组成的。实践DDD还有一个好处,是让我们有机会分离核心业务逻辑和技术细节,让两个维度的复杂度有机会被解开和分治。
为什么说数据库、框架和UI都是技术细节呢?
·数据库:业务逻辑不应该受限于存储方式,也就是不论你是使用关系型数据库还是NoSQL,都不应该影响业务逻辑的实现。数据本身很重要,但数据库技术仅仅是一个实现细节。
·UI:UI只是一种I/O设备的呈现,Web、WAP和Wireless都是不同的I/O,我们的核心业务逻辑应该与如何呈现解耦,以及针对不同的端可以使用不同的适配器(Adaptor)去做适配。
·框架:不要让框架侵入我们的核心业务代码,以Spring为例,最好不要在业务对象中到处写@autowired注解。业务对象不应该依赖框架。
7.6 领域建模方法
第八章 抽象
8.2抽象的概念
抽象和具象是相对应的概念,“抽”就是抽离,“象”就是具象。从字面上理解抽象,就是从具体中抽离出来。英文的抽象abstract来自拉丁文abstractio,它的原意是排除、抽出。按照维基百科上的解释,抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个“皮质的足球”,我们可以过滤它的质料等信息,得到更一般性的概念,也就是“球”。从另一个角度看,抽象就是简化事物,抓住事物本质的过程。
8.3 抽象是面向对象的基础
面向对象(Object Oriented,OO)的思考方式,就是万物皆对象。抽象帮助我们将现实世界的对象抽象成类,完成从现实世界的概念到计算机世界的模型的映射。例如,有一堆苹果,如果对其进行抽象,我们可以得到Apple这个类,通过这个类,我们可以实例化一个红色的苹果:new Apple("red")。此时,如果我们需要把香蕉、橘子等水果也纳入考虑范围,那么Apple的抽象层次就不够了,我们需要Fruit这个更高层次的抽象来表达“水果”的概念。面向对象的思想主要包括3个方面:面向对象的分析(Object OrientedAnalysis,OOA)、面向对象的设计(Object Oriented Design,OOD),以及我们经常提到的面向对象的编程(Object Oriented Programming,OOP)。
8.4抽象的层次性
对同一个对象的抽象是有不同层次的。层次越往上,抽象程度越高,它所包含的东西就越多,其含义越宽泛,忽略的细节也越多;层次越往下,抽象程度越低,它所包含的东西越少,细节越多。也就是我们常说的,内涵越小,外延越大;内涵越大,外延越小。不同层次的抽象有不同的用途。
分层抽象在软件的世界里随处可见,是软件架构的核心,也是我们构建当今信息帝国的基石。有一句名言,“软件领域的任何问题,都可以通过增加一个间接的中间层来解决。”这种分层在某种意义上也是抽象的分层,每一层的抽象只关注本层相关的信息,对上层屏蔽复杂性,从而简化整个系统的设计。例如,我们的系统就是分层的。最早的程序直接运行在硬件上,开发成本非常高。然后慢慢开始有了操作系统,操作系统提供了资源管理、进程调度、输入输出等所有程序都需要的基础功能,开发程序时调用操作系统的接口就可以了。再后来发现操作系统也不够用,于是又有了各种运行环境(如JVM)。编程语言也是一种分层的抽象。机器理解的其实是机器语言,即各种二进制的指令。但是使用二进制编程效率极低,所以我们用汇编语言抽象了二进制指令,用C语言抽象了汇编语言,而高级语言Java抽象了低级语言。
8.5如何抽象
8.5.1 寻找共性
简单来说,抽象的过程就是合并同类项、归并分类和寻找共性的过程。也就是将有内在逻辑关系的事物放在一起,然后给这个分类进行命名,这个名字就代表了这组分类的抽象。
我们的生活中无时无刻不在进行着这样的抽象,语言本身就是对现实世界的抽象符号表达。比如当你说“花”的时候,就使用了抽象概念,它包含了各种各样、万紫千红的花的本性。在我们写代码的过程中,如果遇到大量重复或者部分重复的代码,往往意味着抽象的缺失,可以通过合并归类来进行优化。合并同类项,找到共性,再给这个共性命名的过程就是一个简单的抽象过程。
8.5.2 提升抽象层次
当我们发现有些东西无法归到一个类别中时,该怎么办呢?此时,我们可以通过上升一个抽象层次的方式,让它们在更高的抽象层次上产生逻辑关系。例如,你可以合乎逻辑地将苹果和梨归类为水果,也可以将桌子和椅子归类为家具。但是怎样才能将苹果和椅子放在同一组中呢?仅仅提高一个抽象层次是不够的,因为上一个抽象层次是水果和家具的范畴。因此,你必须提高到更高的抽象层次,比如将其概括为“商品”。如果我们想把大肠杆菌也纳入其中,该怎么办呢?此时,“商品”这个抽象也不够用了,需要再提高一个抽象层次,比如叫“物质”(见图8-3)。但是这样的抽象太过于宽泛,难以说明思想之间的逻辑关系。类似于我们在Java中的顶层父类对象(Object),万物皆对象。
在开发工作中,很多时候就需要通过抽象层次的提升来提高代码的可读性和通用性。举个例子,现在有Apple和Watermelon两个类,都继承自Fruit。对于苹果来说,我们认为甜度大于60就是好的;对于西瓜,我们认为水分大于60就是好的。两个类的代码如下所示。
Apple类
public class Apple extends Fruit{
private int sweetDegree;
public boolean isSweet(){
return sweetDegree>60;
}
}
Watermelon类
public class Watermelon extends Fruit{
private int waterDegree;
public boolean isJuicy(){
return waterDegree > 60;
}
}
此时,我们需要把好的水果挑出来,不难写出一个FruitPicker(挑选水果的类),其代码如下:
public class FruitPicker{
public List<Fruit> pickGood(List <Fruit> fruits){
return fruits. stream(). filter(e->check(e)).
collect(Collectors. toList);
}
private boolean check(Fruit e){
if(e instanceof Apple){
if(((Apple)e).isSweet()){
return true;
}
}
if(e instanceof Watermelon){
if(((Watermelon)e).isJuicy(){
return true;
}
}
return false;
}
}
这里的代码有个问题,就是instanceof的使用。为了判断苹果和西瓜是否好,我们需要借用instanceof获得具体的对象才能完成。
通过提升抽象层次,我们有了新的Fruit类:
public abstract class Fruit{
//提升抽象层次,需要一个新方法来表达这个抽象
public abstract boolean isTasty();
}
再回头看一下FruitPicker,已经不再需要instanceof来做辅助判断了:
public class FruitPicker{
public List<Fruit> pickGood(List <Fruit> fruits){
return fruits. stream(). filter(e->check(e)).
collect(Collectors. toList);
}
//不在需要instanceof
private boolean check(Fruit e){
return e.isTasty();
}
通过上面的示例可以看到,提升了抽象层次的代码的通用性也随之提升,程序更好地满足了LSP(里式替换原则)。因此,每当我们有强制类型转换,或者使用instanceof时,都值得停下来思考一下,是否需要做抽象层次的提升。
8.5.3 构筑金字塔
《金字塔原理》是一本教人如何进行结构化思考和表达的书,核心思想是通过归类分组搭建金字塔结构,这是一种非常有用的思维框架,让我们更加全面地思考,在表达观点时更加清晰。书中提到,要自下而上地思考,总结概括;自上而下地表达,结论先行。其中,自下而上总结概括的过程就是抽象的过程,构建金字塔的过程就是寻找逻辑关系、抽象概括的过程。经常锻炼用结构化的方式去处理问题,搭建自己的金字塔,可以帮助我们理清问题的脉络,提升抽象能力。金字塔结构让我们通过抽象概括将混乱无序的信息形成不同的抽象层次,从而便于理解和记忆,这是一个非常实用的方法论。举个例子,你要出门买报纸,你妻子让你带点东西回来并列了一个清单,里面有葡萄、橘子、咸鸭蛋、土豆、鸡蛋、牛奶和胡萝卜。当你准备出门时,你妻子说,“顺便再带点苹果和酸奶吧”。如果不用纸笔写下来,你还能记住妻子让你买的9样东西吗?大部分人应该都不能,因为我们的大脑短期记忆无法一次容纳7个以上的记忆项目并超过5个小时,我们就会开始将其归类到不同的逻辑范畴,以便记忆。对于葡萄、橘子、牛奶、咸鸭蛋、土豆、鸡蛋、胡萝卜、苹果和酸奶,我们可以按照逻辑关系进行分类,搭建一个金字塔结构。
8.6 如何提升抽象思维
8.6.1多阅读
为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面时你的大脑会被铺满,较少需要抽象和想象。这也是我们不提倡小孩子过多地看电视或玩手机的原因,因为不利于锻炼其抽象思维。抽象思维的差别使孩子们的学习成绩从初中开始分化,许多不能适应这种抽象层面训练的孩子可能选择去读职业技校,因为这里比大学更加具象——车铣刨磨、零件部件等都是能够看得到、摸得到的。
8.6.2多总结
小时候,我们可能不理解语文老师为什么总是要求我们总结段落大意、中心思想。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维的能力。做总结最好的方式就是写文章,小到博文,大到一本书,都是锻炼自己抽象思维和结构化思维的机会。记录也是很好的总结习惯。以读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳,这样不仅可以加深理解,还可以提升自己的抽象思维能力。现实世界纷繁复杂,只有具备较强的抽象思维能力的人,才能够具备抓住事物本质的能力。
8.6.3领域建模训练
对于技术人员来说,还有一个非常好的提升抽象能力的手段——领域建模。当我们对问题域进行分析、整理和抽象时,或对领域进行划分和建模时,实际上都是在锻炼我们的抽象能力。
8.7本章小结
“抽象”作为名词,代表着一种思维方式,它的伟大之处在于可以让我们撇开细枝末节,去把握事物更本质、更一般的特性,从而更有效地对问题域进行分析设计。“抽象”作为动词,代表着一种能力,它是我们理解概念、理清概念之间逻辑关系的基础,也是我们面向对象分析设计所要求的底层能力。归纳总结,合并同类项是进行抽象活动时最有效的方法。同时,我们也要注意到抽象是有层次性的。当一个概念无法涵盖其外延的时候,我们有必要提升一个抽象层次来减少它的内涵,让其有更大的外延。
第九章 分治
9.1分治算法
分治算法主要包含两个步骤——分、治。“分”就是递归地将原问题分解成小问题;“治”则是在解决了各个小问题(各个击破)之后合并小问题的解,从而得到整个问题的解。
分治法解题的一般步骤如下。(1)分解:将要解决的问题划分成若干规模较小的同类问题。(2)求解:当子问题划分得足够小时,用较简单的方法解决。(3)合并:按原问题的要求,将子问题的解逐层合并,构成原问题的解。
例子:归并排序算法、 二分搜索、 K选择问题
K选择问题是指,给出N个数,找出其中第K小的元素。如果直接用穷举法,一共需要比较K ×N次,当K与N有关时,比如K是中位数(K=N / 2),那么时间复杂度为O (N2 );采用分治,则可把复杂度降低为O (N )。
首先,在N个数中选出一个枢轴元素,将比枢轴元素大的元素放到枢轴元素的右边,将比枢轴元素小的元素放到枢轴元素的左边。这样,N个数被分成了两部分,比枢轴大一部分记为S (1),比枢轴小的部分记为S (2),这就是分治的“分”。假设一种理想的情况,枢轴元素基本位于中间值,即它总是将原数组划分成两个大小基本相等的子数组S (1)和S (2)。要求解第K小的元素,有以下3种情况。(1)若K < |S (1)|,则说明第K小的元素位于S (1)子数组中。其中,|S (1)| 表示 S (1) 数组中元素的个数。(2)若K == |S (1)| + 1,则说明第K小的元素刚好是枢轴元素。(3)否则,第K小的元素位于S (2)子数组中。如果是情况1或者情况2,可以继续递归分解子数组。分解问题之后,将N个元素分成了两个N / 2个元素的子数组,只需要在其中一个子数组中进行查找即可,使用穷举查找,复杂度为O (N / 2)。递归表达式为T (N )=T (N / 2 )+O (N / 2),解为O (N ),这说明采用分治算法可以将K选择问题的时间复杂度降低为O (N )。
9.2函数分解
函数过大过长是典型的代码“坏味道”,意味着这个函数可能承载着过多的职责,我们有必要“分治”一下,将大函数分解成多个短小、易读、易维护的小函数。第3章中已经介绍了大量函数分解的技艺。关于函数分解,在此强调以下两点。
(1)函数长短是职责单一的充分不必要条件,也就是长函数往往意味着职责不单一,但是短函数也不一定就意味着职责单一。
(2)在使用组合函数模式时,要注意抽象层次一致性原则(Single Level of Abstration Principle,SLAP),不同抽象层次的内容放在一起会给人凌乱、逻辑不协调的感觉。
9.3 写代码的两次创造
本书一直在强调,我们不仅要写实现功能的代码,还要写容易理解的好代码。“写出好代码”除了需要好的技艺之外,还要有好的方法论。以我的实践经验来看,优雅的代码很少是一次成形的,大部分情况下要经过两次创造:第一遍实现功能,第二遍重构优化。
9.3.1 第一遍实现功能
不要试图一次就写出“完美的”代码,这样只会拖慢我们的节奏。就像写文章,第一遍可以写得粗糙一点,把大概意思写出来,然后再仔细打磨,斟酌推敲,直到达到理想的样子。写代码也是如此,第一遍以实现功能为主,可以允许一定的冗长和复杂,比如有过多的缩进和嵌套循环,有过长的参数列表,名称可以随意取,还会有部分的重复代码。第一遍主要是为了理清逻辑,为第二遍的重构优化做好准备。
9.3.2 第二遍重构优化
如果只是止步于功能实现,那么代码最多只是一个半成品。而实际情况是我们的代码库中有太多这样的半成品,导致系统的复杂度不断攀升,越来越难维护。因此,我们需要有第二次创造——重构优化,即在第一遍实现功能的基础上,看一看是否可以做得更好:命名合理吗?职责单一吗?满足开闭原则吗?函数是否过长?抽象是否合理?
第二次创造通常要比第一次创造更费精力、更耗时间,所以很少有人愿意去做第二遍的事情。比如,要你回答2加2等于几,你凭直觉就知道是4,但是如果把问题换成37×189,你可能都懒得去算,在心里想没事费这劲干嘛。这也是康纳曼·丹尼尔在《快思慢想》一书中提出的重要理论:系统一(感性)和系统二(理性)。动用系统二会耗费能量,而人类是从贫瘠的远古时代进化而来的,为了保存能量,一般默认的是使用系统一思考,不到万不得已,是不会启动系统二的。因此,最好的优化肯定不是等系统上线后再去做,因为这样往往就等于“再也不会去做”(laterequals never)。优化工作本应该是我们编码工作的一部分,拆成两步,主要对编码效率上的考量。
9.4 分治模式
很多的设计模式都用到了分治的思想。例如,第5章中介绍的管道模式,以及没有详细介绍的责任链模式和装饰者模式,其中都有分治的思想。就责任链模式来说,我们不会把处理一件事情需要的所有职责都放在一个组件中,而是放在多个组件中完成,形成一个链条。这样不仅增加了可扩展性,也使每个组件的职责变得更加单一,更容易维护。
9.5 分层设计
分层设计是架构体系设计中最常见和重要的一种结构。分层设计最大的好处是分离关注(Separationof concerns),这样我们就可以通过分层隔离简化一个复杂的问题,让每一层只对上一层负责,从而使每一层的职责变得相对简单。
9.5.1 分层网络模型
网络通信是互联网最重要的基础实施之一,它是一个很复杂的过程,包括TCP协议——在不可靠的网络上出现状况要怎么办,IP协议——把数据包传给谁。需要处理的事情有很多,我们可不可以在一个层次中都处理掉呢?当然是可以的,但显然不科学。因此,ISO制定了网络通信的七层参考模型,每一层只处理一件事情,低层为上层提供服务,直到应用层把HTTP和FTP等方便理解和使用的协议暴露给用户。但是,我们实际在Internet中使用的并不是七层模型,而是TCP/IP四层模型,如图9-3所示。因为七层参考模型过于理想化,过多的分层反而降低了效率,让问题变得更复杂。这里就涉及另一个问题:分层是不是越多越好?当然不是,分层有很多好处,但也有代价。在处理复杂问题时,不能不进行分层,但只分有必要的层。
9.5.2 分层架构
分层架构的目的是通过分离关注点来降低系统的复杂度,同时满足单一职责、高内聚、低耦合、提高可复用性和降低维护成本,也是一种典型的分治思想。
在分层架构中,分层的使用可以进行严格地限制——分层只知道直接的下层;或者可以宽松一些——分层可以访问它之下的任何分层。Martin Fowler的经验是在实际中使用第二种方式会更好,我的经验也可以验证这个说法,因为它避免了在中间分层创建代码方法(或者完整的代理类),也避免了退化成千层面的反模式。有时分层会安排领域层将数据源完全隐藏,不让展现层看到。但是更多时候,展现层会直接访问数据存储,这虽然不那么纯粹,但实际却工作得更好。这种灵活的分层机制实际上是一种开放的分层架构,如图9-4所示。这种不教条和12.2.2节中介绍的CQRS有着类似的作用,即领域层是可选的,允许应用层绕过领域层直接和基础设施层进行通信。关于架构的更多内容,将在第12章详细讨轮。
9.6 横切和竖切
随着互联网的发展,用户流量呈指数型增长,单体应用已经不能适应发展的需要,分布式架构正在变得越来越重要。如果你经常参加一些技术峰会,可以看到这样的分享:一个小企业从一台应用服务器、一个数据库慢慢壮大,发展成独角兽公司,其架构也随之演变成一个大型分布式系统。这不是偶然,而是企业架构演化的必然结果,因为只有分治才能应对网络高并发,实现水平扩展。以分布式数据库为例,我们把原来放在一个数据库中的几千亿数据通过竖切、横切(水平拆分和垂直拆分),切分成相对较小的几十个乃至几千个小数据库,以便满足性能和可用性的要求。所谓竖切,就是按照领域将单体数据库拆分成多个数据库。比如,原来电商数据都是放在一个库中,我们可以按照领域拆分成商品库、会员库、交易库等。
横切是通过一种数据路由算法对数据进行分片,从而减少一个数据库中的数据量。比如,我们要将会员的交易数据切分成10个库,可以用userId对10进行取模,如图9-6所示,这种水平扩展能力在理论上来说提供了无限扩展的可能。
第10章 技术人的素养
10.1不教条
10.1.1瀑布还是敏捷
选择软件开发过程绝不是要么瀑布(Waterfall),要么敏捷(Agile)这么简单。实际上,软件开发的生命周期风格类似一个连续光谱——有从瀑布式到敏捷,以及它们之间的多种可能性,敏捷模型没有方法论。
因此,我们需要在大设计和无设计之间找到一种平衡。一个软件从无到有,不管你是瀑布、迭代,还是敏捷,一般会经历下面的过程。
(1)需求:对于系统该做什么,建立并保持与客户和其他涉众的一致意见,定义系统的边界。
(2)分析与设计:将需求转化为系统设计,设计将作为在特定实现环境中的规格说明,包括逐渐形成一个健壮的系统架构,建立起系统不同元素必须用到的共同机制。
(3)实现:编码、单元测试以及对设计进行集成,得到一个可执行的系统。
(4)测试:对实现进行测试,确保它实现了需求,通过具体的展示来验证软件产品是否像预期的那样工作。
(5)部署:确保软件产品能被它的最终用户使用。
基于此,我推荐一种综合了瀑布模式、迭代模式和敏捷思想的软件开发过程。在此提倡根据迭代所处阶段的不同,在不同科目上花不同的时间,如图10-1所示,灰色部分代表一个阶段在当前迭代中所花时间的比重。
10.1.2贫血还是充血
简单来说,贫血模式提倡模型对象只包含数据,并提供简单的Getter和Setter;而充血模式提倡数据和行为放在一起,是一种更加面向对象的做法。两种模式都有自己的道理,也都有人支持。在我看来,这种争执是没有必要的,因为没有抓住问题的本质。问题的核心不在于行为和数据是否在一起,而在于你能否有效地控制复杂度。如果你有很好的面向对象思维,使用贫血也可以写出好的代码;没有面向对象思维,即使采用DDD,也会陷入复杂性的泥潭。行为和数据是否放在一起,只是表现形式的差别,并不是用来区分面向对象和面向过程的关键区别。
10.1.3单体还是分布式
在业务发展早期,因为用户少、流量少,功能相对简单。基本上单体(Monolithic)应用架构就足以支撑业务的发展。然而,随着业务的发展和用户的增加,单体应用的局限性开始显现。具有水平扩展性(scale out)的分布式系统架构几乎已经变成互联网公司的标配。
虽然面向服务的架构(Service Oriented Architecture,SOA)和微服务有一统天下之势,但是并不代表单体架构就会退出历史舞台,特别是在中台概念提出来以后。中台要求通过集中式的中台管控来提升软件系统的复用,避免趋同的业务重复造轮子的现象。中台的目的就是要通过中台能力来赋能前线业务,提升对前线业务的支撑效率,中台虽然也是对业务进行聚合,但并不是对单体架构的简单回归,而是综合了分布式理念之后升级版的“超级单体”。正是这种不教条和辩证的发展思维,才推动互联网架构不断地向前发展演进。
10.2批判性思维
推荐书:《学会提问》、《思辨与立场:生活中无处不在的批判性思维工具》
10.3成长型思维
把每一次失败都当作学习的机会。
10.4结构化思维
逻辑 + 套路
逻辑是一种能力,而套路是方法论、经验;逻辑属于道,而方法论属于术。二者都很重要,只有熟练地掌握二者,我们才能有更好的结构化思维。接下来,通过两个案例来介绍结构化思维在实际工作场景中的应用。
10.4.1如何落地新团队
这里就能用上结构化思维来帮助我们理清思路,从而有条不紊地开展工作。要知道对一个企业来说,核心要素无外乎就是业务、技术和人。我们所要做的就是如何去熟悉业务、熟悉技术、熟悉人,然而每一部分又可以进行进一步的结构化拆解。
10.4.2 如何做晋升述职
最清晰和实用的结构化表达是“提出问题,定义问题,分析问题,解决问题,最后展望未来”。如果按照这个逻辑顺序去阐述一件事情,会比不知道这个“套路”的效果好得多。这也是麦肯锡常用的解决问题的框架。另一个有用的思维框架是“zoom in/zoom out”。我们说事情时,应该像电影镜头一样,先从远拉近,再由近拉远。“zoom in”是先从宏观背景开始,首先让大家知道你的事情发生的背景,为什么这件事情很重要?然后讲到具体细节,怎么做成的?解决了什么问题?后端思考是什么?最后“zoom out”,从细节调回到整体,结果是什么?带来的客户价值是什么?你对未来的思考是什么?
10.5 工具化思维
我们提倡的“智慧懒”实际上是一种工具化思维,是“磨刀不误砍柴工”的智慧。
我经常在团队中说,每当你重复同样的工作3次以上,就应该停下来问问自己:我是不是可以通过自动化脚本、配置化,或者小工具来帮助自己提效?
10.6 好奇心
求知欲
10.7 记笔记
(1)知识内化:记笔记的过程是一个归纳整理、再理解、再吸收的过程,可以加深我们对知识的理解。
(2)形成知识体系:零散的知识很容易被遗忘,而形成知识体系之后,知识之间就能有更强的连接。
(3)方便回顾:笔记就像我们的硬盘,当缓存失效后,我们依然可以通过硬盘调回,保证知识不丢失。
tips:
(1)使用云笔记:云笔记要能在多端使用,要有目录的层次结构、标签和搜索功能。如果有些场合只能用笔做记录,也没关系,回来之后再整理到云笔记上。(2)归类分组:要定期回顾笔记内容,尽量按照合理的方式对笔记进行重组,形成一个有逻辑关系的树形结构。这样既方便记忆检索,也可以逐渐形成自己的知识体系。对于归于A组或B组都没错的笔记,可以使用标签来辅助分类。(3)不要复制粘贴:好的笔记最好是自己消化后的总结,而不是简单的照抄。如果有引用和参考,建议把链接也放在笔记下面,方便溯源。(4)结构化表达:对于简短的内容要重点突出,粗体显示重点部分;对于篇幅较长的内容,最好有目录,这样可以更加结构化地呈现笔记内容。
10.8有目标
在《高效能人士的七个习惯》中,柯维博士提到,“所有事物都要经过两次创造的原则,第一次为心智上的创造,第二次为实际的创造”。直观的表达就是:先想清楚目标,然后努力实现。不管是人生大问题,还是阶段性要完成的事情,都需要目标清晰、有的放矢。
10.9选择的自由
责任感(Responsible),从构词法来说是“能够回应(Response—able)”的意思,即选择回应的能力。所有积极主动的人都深谙其道,因此不会把自己的行为归咎于环境、外界条件或他人的影响。他们根据价值观有意识地选择待人接物的方式,不会因为外界因素或一时情绪而冲动行事。消极被动的人会受到“社会天气”的影响。别人以礼相待,他们就笑脸相迎,反之,则摆出一副自我守护的姿态。心情好坏全都取决于他人的言行,任由别人的弱点控制自己。但这并不意味着积极主动的人对外界刺激毫无感应,只不过他们会有意无意地根据自己的价值观来选择对外界物质、心理与社会刺激的回应方式。积极主动的人有选择的自由,而消极被动的人往往是被动地接受影响,忘记了自己的主观能动性,忘记了在刺激和回应之间还有选择的自由(There is always a space between stimulus andresponse)。当外界的刺激到来时,我们总是可以用自我意识、想象力、良知和独立意志做出自己的选择。
10.10平和的心态
10.11精进
巴菲特说:“人生就像滚雪球,关键是要找到足够湿的雪,和足够长的坡。”我觉得在技术领域,“雪”是足够多的,“坡”也足够长,关键是我们能不能坚持下去。但凡能持续学习和精进的人,其结果都不会差。
第11章技术Leader的修养
11.1 技术氛围
代码好坏味道 CR周报 技术分享 读书会
11.2目标管理
OKR SMART原则 S代表Specific,表示指标要具体;M代表Measurable,表示指标要可衡量;A代表Attainable,表示指标是有可能达成的;R代表Relevant,表示KR和O要有一定的相关性;最后,T代表Time bound,表示指标必须具有明确的截止期限。
11.3技术规划
11.4推理阶梯
在日常生活中,个人的判断大部分基于自身的主观认识而非事实,这会产生许多误会。在企业的日常运转中,管理者在对待员工时也会犯一些主观性的错误。例如,管理者要批评一个员工,前提是管理者认为员工做的事情是错误的,但是有没有可能管理者本身的认知就是错误的?因此,作为管理者,一定不要轻易对员工做推理,一些错误的推理如果不能及时被澄清,会激起员工的反感。要实事求是,尊重事实。这种推理的情况在团队管理中比比皆是。很多情况下,我们推理别人的“结论”让自己非常生气,但是后来发现事实并非如此,这源于我们习惯用自我推理而非沟通的方式来解决问题。这种推理也被称为“推理阶梯”。
一般而言,“推理阶梯”的发生会经历以下步骤。(1)收集数据:每个人每天都会接受来自外界的大量信息,这些是产生推理的基础。(2)选择性接收数据:尽管我们不愿意承认,但“选择性接收”才是大脑处理信息的固有方式。有句老话:“顺眼的人越看越顺眼,讨厌的人越看越讨厌。”“情人眼里出西施”,说的就是这个道理,没人能避免。就像2002年诺贝尔经济学奖获得者丹尼尔·卡尼曼说的:“我们根本不是理性的人,很多决定都是在稀里糊涂的状态下做出的感性决定,崇尚理性思维的博弈论很少在实际生活中得到应用。”(3)做出假设,得出结论,采取行动:在选择性接收数据之后,我们自然而然地就会想要赋予这些数据意义,从而做出种种假设,并得出相应的结论,然后采取行动,这就是大脑中“推理阶梯”的整个过程。比如,一个熟人迎面走来,没有打招呼,我们会很生气,感觉这个人没有礼貌。但实际情况可能是他没戴隐形眼镜、昨晚没睡好、加班了、走神了等各种情况。我们不要因为自己的好恶对别人进行推理,然后自己生气。因此在做决定之前,我们一定要问问自己:“此事是否可能只是我的推理,实际情况并非如此?”这个问题对于管理者非常重要,因为人与人之间的沟通是非常复杂的过程。别人的一个眼神、一个动作,就有可能让我们在大脑中产生不客观的推理。
11.5 Leader和Manager的区别
简单来说,Manager是管理事务,是控制和权威;而Leader是领导人心,是引领和激发。Leader要做一些Manager的管理事务,但是管理绝对不是Leader工作的全部。我在阿里巴巴内部曾发表过一篇言辞激烈的文章,其中指出:“我们不需要这么多‘高高在上’‘指点江山’的技术Manager,而是需要更多能真正深入系统里面,深入代码细节,给团队带来改变的技术Leader。”
第12章 COLA架构
12.1软件架构
随着互联网的发展,现在的系统要支撑数亿人同时在线购物、通信、娱乐等需要,相应的软件体系结构也变得越来越复杂。软件架构的含义也变得更加宽泛,我们不能简单地用一个软件架构来指代所有的软件架构工作。为了更清楚地表述COLA在软件架构中的位置,以及应用开发人员应该关注什么,我特意将软件架构划分成业务架构、应用架构、系统架构、数据架构、物理架构和运维架构。
·业务架构:由业务架构师负责,也可以称为业务领域专家、行业专家。业务架构属于顶层设计,其对业务的定义和划分会影响组织结构和技术架构。例如,阿里巴巴在没有中台部门之前,每个业务部门的技术架构都是烟囱式的,淘宝、天猫、飞猪、1688等各有一套体系结构。成立了共享平台事业部后,打通了账号、商品、订单等体系,让商业基础实施复用成为可能。
·应用架构:由应用架构师负责,他需要根据业务场景的需要,设计应用的拓扑结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速地支撑业务发展的同时,确保系统的可用性和可维护性。COLA架构是一个典型的应用架构,致力于应用复杂度的治理。
·系统架构:根据业务情况综合考虑系统的非功能属性要求(包括性能、安全性、可用性、稳定性等),然后做出技术选型。对于流行的分布式架构系统,需要解决服务器负载、分布式服务的注册和发现、消息系统、缓存系统、分布式数据库等问题,同时解决如何在CAP(Consistency,Availability,Partition Tolerance)定理之间进行权衡的问题。
·数据架构:对于规模大一些的公司,数据治理是一个很重要的课题。如何对数据收集、处理,提供统一的服务和标准,是数据架构需要关注的问题。其目的就是统一数据定义规范,标准化数据表达,形成有效易维护的数据资产,搭建统一的大数据处理平台,形成数据使用闭环。
·物理架构:物理架构关注软件元件是如何放到硬件上的,包括机房搭建、网络拓扑结构、网络分流器、代理服务器、Web服务器、应用服务器、报表服务器、整合服务器、存储服务器和主机等。
·运维架构:负责运维系统的规划、选型、部署上线,建立规范化的运维体系。要借助技术手段控制和优化成本,通过工具化及流程提升运维效率,注重运营效益。制定和优化运维解决方案,包括但不限于柔性容灾、智能调度、弹性扩容与防攻击、推动及开发高效的自动化运维和管理工具、提高运维的自动化程度和效率。
12.2典型的应用架构
分层架构、 命令查询分离、 六边形架构、 洋葱架构、 DDD
12.3 COLA架构设计
12.3.1分层设计
架构分层是我们在做架构设计时首要考虑的问题。架构上的不合理大多是分层不合理,没有分层或者层次太少,会导致“一锅粥”;层次太多,层次之间又有严格的限制,会导致“千层面”。因此,分层要合理,不能太少,也不能太多。COLA的分层是一种经过改良的三层架构,主要是将传统的业务逻辑层拆分成应用层、领域层和基础设施层。如图12-7所示,左边是传统的分层架构,右边是COLA的分层架构。
其中,每一层的作用范围和含义如下。
(1)展现层(Presentation Layer):负责以Rest的格式接受Web请求,然后将请求路由给Application层执行,并返回视图模型(View Model),其载体通常是数据传输对象(DataTransfer Object,DTO)。
(2)应用层(Application Layer):主要负责获取输入、组装上下文、做输入校验、调用领域层做业务处理,当需要时发送消息通知。当然,层次是开放的,若有需要,应用层也可以直接访问基础实施层。
(3)领域层(Domain Layer):主要封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Entities)的函数对外部提供业务逻辑的计算和处理。
(4)基础设施层(Infrastructure Layer):主要包含数据访问通道(Tunnel)、Config和Common。这里我们使用Tunnel这个概念对所有的数据来源进行抽象,数据来源可以是数据库(MySQL、NoSQL)、搜索引擎、文件系统,也可以是SOA服务等;Config负责应用的配置;Common是通用的工具类。
采用这样的分层策略,主要是考虑到“业务逻辑”是一个非常宽泛的定义,进一步分析我们会发现,“业务逻辑”可以被分层“核心业务逻辑”和“技术细节”。这正是六边形架构和洋葱架构中提倡的思想,即尽量保证内部核心领域的独立和无依赖,而外部的技术细节可以通过接口和适配器随时更换,从而增加系统的灵活性和可测性。