续前文 后端好书阅读与推荐 – Mageek`s Wonderland ,几十天过去了,又看了两本好书(还有以前看过的书),这里依然把它们总结归纳一下,加入一些自己的看法、有用的链接和可能的延伸阅读,并推荐给需要的同学。
深入理解Java虚拟机
深入理解Java虚拟机 (豆瓣) https://book.douban.com/subje…
Java怎么用,是一个问题;怎么用好是一个大问题;这么用是为什么,是一个更大的问题。搞懂这三个问题应该是每一个搞Java的人都要追求的目标,读完本书,就能把这个更大的问题搞懂了。
本书亮点:
模块化是解决应用系统与技术平台越来越复杂,越来越庞大的问题的一个重要途径,也是建立各种功能的标准件的前提。
Java运行时数据区几个主要部分:程序计数器(可看作当前线程所执行的字节码的行号指示器)、虚拟机栈(每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,而栈帧存储局部变量表、 操作数栈、 动态链接、 方法出口等信息)、本地方法栈(Native方法对应的栈)、堆(所有线程共享的一块内存区域,存放对象实例)、方法区(各个线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据)、常量池(方法区的一部分,存放编译期生成的各种字面量和符号引用)。
对象访问方式取决于虚拟机实现而定的,目前主流的访问方式有使用句柄和直接指针两种:如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;如果使用直接指针访问,reference中存储的直接就是对象地址。
Java垃圾回收采用分代回收机制,新生代和老生代采用不同的算法(node.js也是),而不管什么机制,判断一个对象是否存活都是基本的步骤,方法有:引用计数法,给对象添加一个引用计数器,每当一个地方引用它时,计数器就加1,引用失效时,计数器就减1,任何时刻计数器为0的对象就是不可能再被使用的,这个方法实现简单,但是不能解决循环引用问题,所以主流JVM不使用,但是这个算法也适用于许多地方如Python,微软的COM;可达性分析,按照对象之间的引用关系维护一个引用链,如果一个对象不可达GC Roots,那么就是可回收的,应用于主流JVM中。
finalize是一种迎合C++程序员的妥协,运行代价高昂,不确定性大,无法保证各个对象的调用顺序。有些教材中描述它适合做“关闭外部资源”之类的工作,这完全是对这个方法用途的一种自我安慰。finalize能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时,所以建议大家完全可以忘掉Java语言中有这个方法的存在。
几乎各种语言或多或少都提供过一些语法糖来方便程序员的代码开发,这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会。不过也有一种观点认为语法糖并不一定都是有益的,大量添加和使用“含糖”的语法,容易让程序员产生依赖,无法看清语法糖的糖衣背后代码的真实面目。所以我们既要会使用语法糖,同时也要搞懂背后的原理,这样才能进阶呀。比如泛型技术:泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型。
Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定“热点代码”(Hot Spot Code)。 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称JIT编译器)
基于高速缓存(Cache)的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性;除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,类似的JVM也有指令重排(Instruction Reorder)。这两点是提高程序运行的主要方法,然而也是多线程程序难以正确编写的主要原因。
先行发生(happens-before)是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。
这本书是基于jdk1.7的,如果要追踪最新的Java和JVM规范,可以看这里。
另外,上面提到的第一个问题可以参阅Java核心技术,第二个问题可以参阅Java编程思想、Effective Java,第三个问题还可以参阅HotSpot实战。这几本书我都浏览过,部分有细读,都是相当经典的。
高性能MySQL
高性能MySQL (豆瓣) https://book.douban.com/subje…
这本书可谓是MySQL领域的权威之作,从架构到测试,从性能分析到查询优化,从软件配置优化到服务器硬件优化,从单实例到主从复制、负载均衡,从底层数据库优化到应用层优化……本书真可谓是面面俱到,同时又很有深度,绝非浅尝辄止。
本书亮点:
所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡自然也影响到性能。这句话适用于世间的一切工具,安全性和可用性总是矛盾的,我们在使用工具或者开发工具的时候,都要寻求一个最佳平衡点。
除非需要使用InnoDB不具备的的特性,并且没有其他办法可以替代,则都应该优先使用InnoDB引擎,也不要多引擎混用,例如全文索引可以使用InnoDB+Sphinx,而不要使用MyISAM引擎,因为InnoDB其他方面的优点可以完全碾压MyISAM,比如崩溃恢复快,支持事务,支持行级锁,支持真正的热备份等等。
基准测试可以验证对于系统的假设,检查异常行为,找出扩展性瓶颈等等。需要注意的是不要使用真实数据的子集,作物的数据分布,忽略预热等等,这些错误的操作会使得测试结果无用或者不精确。而且要建立参数与结果文档化的规范,这样才利于结果分析与优化。
很多人在优化时都将精力放在修改某些东西上,却很少去测量;正确的做法是要尽量测量响应花的时间在哪,正确的测量一般都能将性能问题的点暴露出来,我们就能更好的对症下药,而不是盲目优化(花1000块优化一个只值500块的业务,或者已经处于顶点的业务不就是亏了吗)。所以说,决策要基于数据而不是感觉。
良好的逻辑设计和物理设计是高性能的基石,某些反范式的设计可能加快某些查询,比如计数表和汇总表是一种很好的查询优化方式,能提高统计类的查询速度,但是维护起来就比较麻烦,可能降低数据插入速度。这些都需要自己根据业务来进行权衡(比如读写比),有阴就有阳嘛。
选择能正确存储数据的最小类型:既省空间又省计算时间,简单就好:比如使用MySQL内建时间戳而不是自己使用字符串,尽量避免null:因为null使得索引、统计、值比较都更加复杂还可能会占用更多空间。
InnoDB有一个“自适应哈希索引”的功能,当引擎注意到某些索引值被频繁使用时就会在内存中基于B+Tree索引之上再创建一个hash索引,这样就让B+Tree也具有hash索引的优点比如快速查找。
小表通常全表扫描更高效,中大型表才适合用索引,使用索引过程中要注意,索引列必须单独的出现在比较符号的右侧而不是表达式的一部分(这会使索引失效),使用前缀索引来节省空间提高检索效率,多列索引要注意顺序不然容易失效,聚簇索引可以提高访问速度。
慢查询优化:只向数据库请求需要的列(比如不要随意select * )、避免不必要的行扫描、必要的时候分解查询(拆分大的查询,分解关联查询)。
默认配置文件是经过大量测试,所以属于较优解,一般符合普通用户,要修改也主要是根据业务而不是服务器配置;任何打算长期使用的配置都应该写到全局配置文件而不是在命令行指定,因为如果偶然启动忘了(事实是经常会忘,好记性不如烂笔头是个真理)设置就会有风险。
MySQL复制功能不仅有利于构建高性能应用,同时也是高可用性(负载均衡、故障切换)、可扩展性(升级)、灾难恢复、备份以及数据仓库等工作的基础。
数据如果非常庞大,比如几亿行了,单台机器已经撑不住了,通常要采取分片技术,分片最大的问题就是查询与获取数据,我们的目标是对最重要且频繁查询的数据减少分片(热点数据通常就那么多)。所以分片关键问题就在于选择一个好的分区键,通常是一个数据库中非常重要的实体的主键。
不仅要关注MySQL,还要关注应用层优化:Apache服务器处理静态文件都可能使用一个占用内存很大的进程(上一个请求处理完后,该进程仍然保持着),所以最好使用Nginx或者Lighttpd来处理静态内容服务,而且静态文件名不要重用,要加上版本号,这样就能避免浏览器缓存问题;主动缓存如Squid,被动缓存如Memcached,都可以对性能获得数量级的提升,关键就在于找到正确的粒度和缓存过期策略组合,通常主动缓存更好,因为对应用层隐藏了检查-生成-存储这个过程;
……
亮点太多,列不完了,需要大家自己去寻找。另外,这本书不适宜一次性的精读完毕(太厚,内容太多),可以快速浏览一遍,大概了解,以后遇上问题就可以把这本书当作一本问题解答手册来查询解决方案,或者找找灵感。
Redis实战
Redis实战 (豆瓣) https://book.douban.com/subje…
本书对redis的介绍是相当全面了,从基本用法讲起,然后讲了许多应用场景,包括购物车、数据库缓存等,然后讲了一些常见问题的解决办法,比如内存占用过高,自定义扩展来丰富redis的用法等等,看完一本书过后就能很好的使用redis了(还有本好书:redis设计与实现,我大致浏览了一下,这本书主要讲了redis的实现原理,这两本书加起来就能既懂原理又会使用,把redis搞个透彻)。
本书亮点:
使用冒号 : 或者管道号 | 等来实现命名空间的作用,比如一个名为 article:12222 的hash存了这篇article的title,link,time等属性,article:12223又是另一篇文章,这个可以部分实现数据库检索的功能。
为了减少redis与客户端之间的通信次数,可以用multi和exec来做事务处理,事务会一次性的把一批命令一次发给redis,提高吞吐率;另外事务还能保证一批操作的原子性。在node-redis实现中,如果只是想提高吞吐率则可以用batch替代multi。
通过复制(主从)和AOF能够增强redis抵抗系统崩溃损失数据的能力,AOF如果用得不好的话,要么损失很多数据,要么严重降低吞吐量,比较合适的做法是appendfsync everysec 亦即把每一秒的命令一次同步进文件。
使用 redis 的 setnx 来实现基本上正确的的分布式锁,再加上expire可以实现具有超时功能的锁,保证即使获得锁的客户端崩溃没有主动释放锁时,其他进程也有机会获得锁。
用 list 来替代 subscribe、publish 实现更可靠的发布订阅系统;另外,利用subscribe、publish加list来实现具有离线缓存的消息队列系统,保证即使发生连接故障也能把消息送达。
利用反向索引以及 redis 集合的并、交、差功能可以实现简易的搜索引擎,利用 hash 结构的 sort 功能还能对搜索结果进行简易的排序功能,利用有序集合 zset 能实现更高级的排序功能。
社交网站通常是用时间线这一数据结构来实现新鲜事浏览这一功能,虽然导致了大量的冗余信息,但是能够节省查询时间,这是典型的以空间换时间的操作,我以前做社交APP后台的时候直接把所有新鲜事直接放在一张表中,然后按用户id查询,这样虽然省了空间,但是如果用户剧增((⊙﹏⊙)b,我们的APP并没有用户剧增)就不行了,一张表几亿行还怎么查。
使用短结构来节约内存,使用较短的键名节约内存,分片降低单实例的内存占用。
使用lua脚本,在不编写C代码的情况下,为redis添加新的功能。
时效性问题,本书没有一些最新特性 比如 geohash 解决了地理坐标问题,键的异步释放使得我们可以放心删除而不必担心大量数据的删除使得redis短暂不可用。总之,要更好的利用redis还是要追踪redis的最新变化,以便更简洁、更靠谱的解决问题。
深入剖析Nginx
深入剖析Nginx (豆瓣) https://book.douban.com/subje…
本书从源码入手,依次讲解了进程模型、模块、响应处理机制、过滤、负载均衡等相关原理,极大的满足了我的好奇心,因为之前一直就对nginx高并发处理能力有一丢丢了解(比如nginx采用事件驱动机制而非apache的进程、线程每请求方式),自己也用过nginx,但是对他的原理还不是特别的明白,本书算是填补了我的这个空白。
本书亮点:
Nginx 将职责分为监控进程(主进程)和工作进程(主进程fork的子进程),监控进程与用户交互并对工作进程进行监控管理,工作进程完成具体业务逻辑,两者都有一个无限的for循环,这是服务进程的基本写法。
Nginx仅提供针对大块内存的回收不提供小块内存的回收,这是因为web server的特殊性亦即阶段和时效,请求就申请内存,处理完毕就释放内存,所以不会存在nginx长时间占据大量无用内存的情况,那么小内存也自然不必急于回收,而是成为大内存后在回收。
对于客户端的请求,nginx将整个过程分为11个阶段,每个阶段有数个回调函数进行专门的处理,每个阶段的处理功能都比较单一,达到高内聚低耦合的目的。
Nginx是以事件为驱动的,也就是说Nginx内部流程的向前推进基本都是靠各种事件的触发来驱动,内部事件主要有两类:IO事件与定时器事件。其中IO事件主要靠epoll,epoll主要优点是监控数目不受文件描述符限制、事件响应是触发式的,不需要遍历描述符(select需要)。
Nginx要处理动态的内容一般需要转发给后端服务器,常见的搭配是nginx+fastcgi+php,nginx把http请求转化为fastcgi协议的数据后转发给PHP引擎,PHP引擎处理结果后把数据返回给nginx,nginx把数据转化为http格式返回给客户端。
负载均衡有多重含义(或者说多重级别),可以是进程上的(根据master进程根据子进程压力调整其获取监听套接口的几率),更广的意义上是指反向代理上,亦即nginx把请求均衡的转发给后端服务器如PHP引擎,发挥多个单元的整体效能。一般采取加权轮询、IP hash等策略,但是只靠nginx是不能实现完整的负载均衡的,详见我以前写的一篇文章。
这本书主要从应用及其原理方面来介绍nginx,对于后端程序员应该是够用了(我也忽略了许多源码,因为只是想了解一下原理)。但是对于要想自己深入、进行模块编写的读者应该还不够用,可以再参考一下这本书 深入理解Nginx(第2版)(我大概浏览了一下,本书会指导读者编写具体的模块及其底层原理,比我们今天介绍的书更深入一些)。
第一本Docker书
第一本Docker书 修订版 (豆瓣) https://book.douban.com/subje…
书如其名,这就是真正的第一本docker书。docker是什么、怎么安装、如何使用、测试集成、构建服务等都有介绍。之前就久仰docker大名,也试着试用了一下,但是直到这本书读完我才对docker有了一个完整的认知。另外,不出意料,本书的推荐序也很精彩。
本书亮点:
docker的核心价值在于可能改变软件的交付方式和运行方式。传统的交付方式下,软件运行期依赖的环境是无法控制,不能标准化的,开发人员常常需要解决开发环境和生产环境的差别带来的问题,而docker则把软件及其依赖环境打包在一起,以镜像形式交付,让软件运行在标准环境中,非常符合云计算的需求,同时docker的轻量虚拟化技术也符合实例水平扩展,资源动态调整的要求。
docker提供以下几个好处:简单轻量的建模方式,工程容易docker化,随时修改代码,运行快速;职责分离,开发人员只管开发,运维人员只管容器管理,减少环境不同带来的问题;快速高效的开发生命周期,程序容易部署、移植和协作;容易实现面向服务的架构和微服务架构。
docker只支持64位架构,原生的Linux容器格式:libcontainer,使用命名空间来隔离文件系统(每个容器都有自己的root系统)、进程(每个容器都运行在自己的进程环境中)和网络(容器间虚拟网络地址和IP地址都是分开的),使用cgroups将CPU内存之类的资源独立分配给容器,写时复制使得文件系统分层隔离、速度更快、占用空间更小,还提供日志和交互式shell。
镜像分层:新镜像是从 base 镜像一层一层叠加生成的,文件系统发生变化时,就在现有镜像的基础上增加一层,这一层叫做“容器层”(读写层),“容器层”之下的都叫“镜像层”(只读层),只有当需要修改时才从镜像层复制一份数据到容器层,这种特性被称作写时复制(Copy-on-Write),达到了镜像共享,快速构建的目的。
可以使用docker commit来构建镜像,也可以基于dockerfile和docker build命令构建,通常建议使用后者,因为dockerfile更具备透明性(可以清晰地看出安装了什么软件,修改了什么配置)、可重复性(一次编写,多次使用。此外构建缓存还可以制作构建模板)、幂等性(同一个dockerfile不论执行多少次,结果都是相同的)。
docker容器之间通讯有3种方式,1.9之前推荐用Docker Link(安全:只有link之前的容器可以通信,不必硬编码,不支持多主机),1.9之后推荐Docker Networking(支持多主机容器连接,可以热更新容器,Networking网络内部容器可以自主发现),不太推荐docker内部网络(IP硬编码等导致该方法不够灵活)。
volume(卷)具有一些有用的特性:容器之间共享数据,对卷的修改会直接反映在包含改卷的容器里所以可以在不修改容器的情况下向容器里加入、更新、删除数据,更新镜像时不会影响卷。利用这些特性可以更好的进行数据共享与持久化。
docker编排与集群化之路:Docker Compose 是用来做 docker 的多容器控制,使用Compose ,你可以在一个文件中定义多个容器应用,然后使用一条命令来启动你的所有应用,避免繁复操作,docker 自动化构建容器栈;Consul提供了一个易于使用,基于开放标准的服务发现解决方案,服务发现允许某个组件在想与其他组件通讯时自动找到对方;Docker Swarm是一个用于创建Docker主机(运行Docker守护进程的服务器)集群的工具,使用Swarm操作集群,会使用户感觉就像是在一台主机上进行操作亦即将容器抽象到集群级别。还有其他很多工具如:fleet、etcd、Kubernetes、Apache Mesos、Helios、Centurion。
时效性原因,书中一些例子已经过时(如Docker1.12开始内置编排机制,Docker1.13正式支持docker stack),需要结合最新版本来使用。
另外,想要深入理解docker可以阅读这本书 Docker——容器与容器云。我大致浏览了下,这本书不仅讲了docker如何使用,还深入讲解了docker的核心原理如namespace资源隔离、cgroups资源限制、libcontainer原理和一些高级实践技巧。此外,还讲了对容器、容器云的思考,包括如何构建自己的容器云,以及Kubernetes实现一切皆容器的“大同理想”。
UNIX/Linux 系统管理技术手册
UNIX/Linux 系统管理技术手册 (豆瓣) https://book.douban.com/subje…
又是一本进千页的大部头,但是不怕,这本书如其名,是一本手册性质的书,非常大而全,包括基本管理技术、网络管理技术和其他补充管理技术,几乎包揽了所有我们可能用到的功能(小到一行代码整么写,大到数据中心怎么建),对于宏观把握整个Linux生态系统有很大作用。我的应对策略是跳跃式阅读(不错,就像上面那本MySQL),留下整体映像,等遇上问题时再来具体的查询相关部分的内容。也就是说,大脑相当于内存,书本相当于硬盘数据库,我们首次阅读就是在内存中建立索引,便于提升以后的查找速度:-D。
本书亮点:
Linux各个发行版其实并没有那么巨大的差别,我们选择一个发行版时主要考虑几点:是否能长期存在,是否会有持续的安全补丁,是否会持续更新软件,发行商是否会在出了问题时帮我们解决,不同发行版侧重点会有所不同我们要根据自己的业务来进行选择。
编写脚本时注意形成一种指导风格,这样你和你的团队成员可以按照相同的规范来书写代码,有了这种指导在阅读别人写的代码或者别人阅读你的代码时都会更容易;注释不要多也不要少,最好的效果是一两个月后再来读代码发现注释和有用。
一个进程由一个地址空间(一组内存页面)和内核一部分数据(有关进程的信息如地址空间映射、状态、优先级、资源)组成;一个线程是在进程内执行fork的结果,继承了包含它的进程的许多属性,多个线程可以共享该进程内数据,并行(多核)或并发(单核,模拟并行)执行
Unix家族的文件系统目前没有一个标准,我们尽量按照如下标准来组织。bin:核心操作系统命令;sbin:系统最小规模运行所需命令;boot:内核及加载内核所需软件;etc:关键启动文件及配置文件;usr:次要的命令文件;var:随主机变化的文件如日志,数据文件;mnt:可移动介质临时挂载点;opt:可选的应用软件包;proc:正在运行的进程信息;tmp:临时文件;
合适的备份计划取决于:文件系统的活跃性,转储设备的容量,用户期望的冗余度,想要购买的备份介质数量。
版本控制与多人合作:svn是集中式的,一台中央服务器充当了一个项目的权威库;git是分布式的,没有中央库,每个用户都含有一个完整的项目,采用的是拷贝-分支策略。
信息安全领域的基本思想——CIA原则:Confidentiality(机密性),Integrity(完整性),Availability(可用性)。在设计、实现或者维护系统的时候,需要考虑CIA安全三原则,正如老话所说“安全性是一个过程”。
负载均衡既提高了性能又增加了冗余性,包括几种方式:循环域名服务(也就是DNS轮询)、负载均衡硬件(比如Big-IP Controller、Content Services Switches等)、软件负载均衡(比如Nginx)。
Squid既是一个高速缓存软件也是一个代理服务程序,代理服务很有用,但是Squid真正厉害之处是其高速缓存,它甚至能够形成一个缓存层次结构,以最大化提高缓存命中率。Squid是有意义的,因为用户对web的探索具有趋同性,所以在适度的规模上会出现相当多重复请求,运行高速缓存可以节省带宽和计算资源。
虚拟化技术让多个彼此独立的操作系统同时运行在相同的物理硬件上,系统管理员把每个虚拟机当做一台独立的服务器,既满足了软件厂商的要求,又降低了单一服务的成本。包括全虚拟化(如VMwareESX)、半虚拟化(Xen)、操作系统级别虚拟化(如workload partition)。除了传统的虚拟化技术,近年来的云计算也是一种(或者说类)虚拟化技术,它把计算能力作为对外提供的服务类似于水电等基础设施,直接使得硬件层对开发人员和系统管理员透明,提高了效率。
分析性能问题步骤:明确表述问题、收集证据并分类、批判性的评价数据、用语言和图示总结证据、形成一份总结说明。这一套其实不止适用于性能分析问题,也适用于大多数其他问题,比如架构、重构、debug等等。
系统管理不是一种行为艺术。无论做的什么,都应该能重复完成,切前后一致。通常意味着最底层的变化应该有脚本或者配置程序来做,而不是管理员来做。配置上的变化应该体现在系统管理软件的配置文件里。说白了,就是文档的重要性,在我看来:首先文档就是一家公司的财富,没有文档,人走了,那么之前公司的积累也就没了,又得重头来,损失很大;其次,文档是一种保证,大家都按约定办事,保证操作可重复,避免歧义与甩锅;再其次,文档能节约时间,虽然写的时候可能费点事,但是能节省后面大量的人员沟通的时间;最后,文档保证了系统的完整性,亦即文档保证了系统的后续修改遵循一致的思路和风格,才不至于系统随着时间流逝而越来越乱,难以维护与使用。
本书教会了我们怎么使用Linux,这对于我们后端开发人员是足够受用了。而这本书Linux内核设计与实现(原书第3版)就会告诉我们Linux是如何实现它这么多这么强大的功能的内在原理,精力有限,我只是浏览了一下没有细读,等以后真的用得上Linux的深入知识时,或者遇上了什么解决不了的问题,我会再来求助于这本书的。
代码整洁之道
代码整洁之道 (豆瓣) https://book.douban.com/subje…
本书提出一个观点就是:代码质量与整洁度成正比,围绕提高整洁度,作者展开了方方面面的阐述,从命名的方法到函数的定义,从注释的使用到格式的目的,还介绍了对象,错误处理等等等等。看完后的感觉就是:写好代码,从本书开始。虽然我现在写的代码还比较“乱”,但是今后的代码中会努力践行书中的原则,争取写出整洁的代码。
本书亮点:
软件质量既依赖于架构和项目管理,又与代码质量紧密相关,而代码质量与整洁度成正比,所以我们要致力于写出干净整洁的代码。并且不仅要知道书写整洁代码的原则,还有查看大量代码实例,进行案例研究,在实践中贯彻这些原则,才能真正做到知行合一。
不同的人对整洁代码有不同的理解,总归起来,有如下一些特点:代码逻辑直截了当,依赖关系很少,性能最优,每个函数模块类专注于一件事,可以轻易的被其他人阅读,具有完整的测试,作者自己要在乎自己的代码悉心维护,没有重复代码提前构建重复代码应有的抽象,体现系统的所有设计理念,包含尽量少的实体。
命名看似简单却无处不在,所以我们不妨命好名,几个原则:名副其实,不用注释也能明白这个变量代表什么;避免误导,不要用保留词或太相同的词引起歧义;做有意义的区分,不要添加在名字后数字或者废话而是以读者能鉴别不同之处的方式来区分;名字要读的出来,便于交流;名称要便于搜索;一个概念统一用一个词,比如不要混用manager和Controller;避免双关词;给名词添加语境,但是避免冗余的语境,含义明确的情况下,短名字总是比长名字好呀……
函数构成了当今程序的基石,函数要写的明白要遵循如下原则:短小;只做一件事,也就是函数中的语句要在同一个抽象层级上,不在同一个抽象层级就得拆出来形成一个新的函数;把switch埋藏在较低的抽象层级,比如抽象工厂中,虽然依然免不了判断或者条件增多时要增添代码,但是能够把变化截留在工厂内,减少影响范围;不要向函数传入标志参数,比如TRUE/FALSE,而是应该直接把这个函数重构成两个函数;一个函数要么下达什么指令(set)要么回答什么问题(get),不要添加副作用;DRY(别重复自己)。
注释是一种必须的恶,如果代码写的好根本不需要注释(感觉有点过了,至少一个大的模块用来干啥还是因该用注释或者说文档直接来说明),而且代码才是真实的地方,注释很可能没有被维护而导致失效;别给糟糕的代码加注释,重写吧;版本控制系统可以帮我们省掉许多注释如作者署名,注释掉的代码。
或许你认为“让代码工作”是开发者的头等大事,但是其实并不是这样的,代码风格关系着沟通,而沟通才是头等大事。代码风格影响着可维护性和扩展性,即使代码已不复存在,其风格和律条依然存在,因而一个团队应该形成一个统一的风格,便于沟通与维护。
隐藏实现并非就是简单的在变量之间放一个函数层。隐藏关乎抽象,类并不是简单的用取值器和赋值器将变量向外推,而是暴露抽象接口,以便用户无需了解数据的具体实现就能操作数据本身。对象暴露行为,隐藏数据;数据结构暴露数据,没有明显的行为。要合理的利用二者。
错误处理一旦处理不好就容易污染整洁的代码:使用异常来保证错误处理不会打乱正常的程序逻辑;可检异常可能会破坏开闭原则,要谨慎使用;打包第三方API就降低了对它的依赖;不要返回null值,只是突然增加了工作量,可以用异常或者特例对象来代替。
测试非常重要:测试代码和生产代码一样重要,脏测试就等于没测试;有了测试就可以毫无顾虑的进行重构和改善,因为有保证,消除了清理代码就会破坏代码的恐惧(这句话可谓是戳中了我,我之前的项目也有过一些重构的努力,但是就是怕重构过后不能正常运行,导致重构举步维艰);测试代码要清晰,符合构造-操作-检验三个环节;单个测试中的断言应该最小化,保证一个测试对应一个概念;整洁的测试遵循 FIRST 原则(fast,independent,repeatable,self-validing,timely)。
系统层级的整洁:城市能有效运转是因为演化出了恰当的抽象层级和模块,有人负责全局有人负责细节,所以软件系统也有架构师和项目经理等;将构造和使用分开(依赖注入,控制反转);扩容(要考虑可扩展性,但是注重现有的系统的构造,将来再重构和添加,没必要Big Design Up Front,考虑好模块化和关注切面划分就好)。
KISS(Keep It Simple, Stupid),简单设计原则:运行所有测试(测试是对一个系统的保证);不要重复(重复意味着额外的工作、风险、复杂度);表达了程序员的意图(清晰可读);尽可能少的类和方法数量(避免教条如:每个类都要有接口)。
编程是一种技艺甚于科学的东西,不要指望一开始就写出优雅整洁的代码,一般是先写出肮脏的代码,然后进行重构,清理。所以代码仅仅能工作还不够,满足于代码能工作的程序员不够专业,他们害怕没时间进行代码结构的重新设计,其实没有什么比糟糕的代码给项目带来更长远的损害了,糟糕的代码会一直腐败发酵,影响各个模块(找出这些依赖和影响相当不容易)无情的腐蚀整个项目和团队。
……
这本书里面的良心建议实在是太多了,无法一一列举,真的得自己看一遍才能有收获。
重构-改善既有代码的设计
重构 (豆瓣) https://book.douban.com/subje…
上一本书教会我们怎样书写整洁的代码,那么面对不整洁的代码,我们怎么办呢?这本书就手把手教我们怎么重构,改善现有的代码。从一个实例入手,讲了重构的理由、原则、技巧、步骤与时机。我读完过后感觉心里就有了些烙印,无论是写代码还是改代码,基本都会不自主的向这本书靠拢,很有收获。
本书亮点:
重构是在不改变代码外在行为的前提下对代码作出修改,以改进程序的内部结构,本质上说重构就是在代码写好后改进其设计,提高其可理解性,降低修改成本;记住所有代码的“坏味道”及其对应的重构手法,才能有信心面对各种情况——学会所有招式才可能“无招胜有招”;为了避免自掘坟墓,重构必须系统化进行,用一些经过检验的重构手法,就可以一次一小步的修改代码,所以任何错误都可以比较容易的发现,降低了重构过程中的风险。
如果你发现自己需要为程序添加一个特性,而现有的代码结构使你无法方便的添加,那就先重构程序,使得特性添加变得容易,然后再添加这个特性。而重构的第一步就是建立一组可靠的测试,来尽量避免bug,保证重构的正确性。
对象A最好不要在另外一个对象B的属性基础上使用switch语句,因为将来B变了A也必须变,如果不得不使用switch也要在自己的属性上使用,也就是把switch移动至B里面去;最好是用多态替换switch语句(其实也不是替换,而是把switch放在了较低的抽象层级,使得可能变化的部分就在一个对象里面,将其与不变的部分隔离开)。
添加新功能和重构是两种行为,一定要加以区分,并且同一时刻只做好一件事就行。
任何能够查询的东西,我都不太愿意去记,因为怕把大脑挤爆了。所以说大脑这个珍贵而有限的内存主要做的事一定是建好索引,而非存储数据;另一方面,常用的数据也应该存进大脑,以提高效率,这和内存中的缓存是一个道理。
不要为了重构而重构,一定是因为你想做某件事时重构恰好可以帮你做好,重构的几个时机:添加新特性时,修补错误时,复审代码时。
程序有两面价值“今天可以为你做什么”和“明天可以为你做什么”,如果只关注今天的工作,那么明天我们将无法工作,所以需要重构,来避免代码出现以下四个情况:难以阅读,重复太多,新行为无法简单添加,逻辑复杂。这四个情况导致的结果都是程序难以改变(修改),而唯一不变的就是改变:-D
代码的坏味道——重构的时机:重复代码、过长函数、过大的类、过长的参数列、发散式变化、霰弹式修改、依恋情结、数据泥团、基本类型偏执、switch语句、平行继承体系、冗赘类、夸夸其谈未来性、临时字段、过渡耦合的消息链、中间人、不适合的亲昵关系、异曲同工的类……
本书给出了一份重构列表,包含了很多很多的重构类型的名称、动机、方法、范例,涵盖了函数、对象、数据、表达式、调用、概括关系、大型项目等方方面面,值得我们借鉴,下面摘抄几条作为示例。
当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码就会把这段代码放进一个独立的函数中。短函数的好处:更可能被复用,让高层函数看起来更清晰易懂,复写更容易。“短”的含义不在代码行数,而在于函数名称和函数本体之间的语义距离。
如果一个类的某个方法、字段被另一个类频繁使用,就应该搬移该方法、字段。
混乱的继承体系是一个严重的问题,因为它会导致重复的代码,这正是程序员职业生涯的致命毒药。它还会使修改变得困难,因为特定问题的解决策略被分散到了整个继承体系,最终你的代码难以理解。所以可以通过建立多个继承体系,并利用委托来互相调用,使得原来负责多个任务的继承体系变成多个负责单个任务的继承体系。
……
感觉本书最大的问题就是太老了(1999年),有些工具或者方法在如今技术进步的情况下显得有些多余,比如现在的IDE如Eclipse或者IDEA都有很强大的功能,书里提到的一些技巧完全用不着。不过经典终究是经典,里面的绝大部分思想我们如今都还是可以借鉴的,尤其是对于重复代码的观点——应该坚决消灭重复。
2017.9.8 后记
花费了几个月来看书,又花了几天来整理,希望对我们都有所帮助:-D。欢迎拍砖,我的主页Mageek`s Wonderland。
查看原文