代码精进之路:从码农到工匠读书笔记

代码精进之路:从码农到工匠 代码精进之路:从码农到工匠读书笔记读书笔记

第一章技艺

代码命名规范的意义:

由此可见,事物的复杂程度在很大程度上取决于其有序程度,减少无序能在一定程度上降低复杂度,这正是规范的价值所在。通过规范,把无序的混沌控制在一个能够理解的范围内,从而帮助我们减少认知成本,降低对事物认知的复杂度。

第二章 代码规范

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,在应用处理请求的切面上进行异常处理收敛,其处理流程如下:

第三章 函数

单一职责

函数要尽可能的短小,java语言最好不要超过20行。

职责单一:一个方法只做一件事。(作用:提高代码的可读性,职责越单一,功能越内聚,提升代码的可复用性。)

优化判空

抽象层次一致性(SLAP)

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

举个例子,假如有一个冲泡咖啡的原始需求,其制作咖啡的过程分为3步。

(1)倒入咖啡粉。

(2)加入沸水。

(3)搅拌。

如果要加入新的需求,比如需要允许选择不同的咖啡粉,以及选择不同的风味,那么代码就会变成这样:

按照组合函数和SLAP原则,我们要在入口函数中只显示业务处理 的主要步骤。具体的实现细节通过私有方法进行封装,并通过抽象层次 一致性来保证,一个函数中的抽象在同一个水平上,而不是高层抽象和 实现细节混杂在一起。

根据SLAP原则,我们可以将代码重构为:

重构后的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行。

来源:九城风雪

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年4月13日
下一篇 2021年4月13日

相关推荐