Technology

Chart Type 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go 内存管理 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

Architecture

实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 在离线业务混部 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF 生命周期管理 openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 概率论 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列


《clean code》笔记

2018年01月08日

前言

《计算机程序的构造和解释》中有这么一句话:代码必须能够被人阅读,只是机器恰好可以执行

让软件能工作和让软件保持整洁,是两种截然不同的工作。我们中的大多数人脑力有限,只能更多的把精力放在让代码能工作上,这也没啥问题。问题是,大多数人在程序能工作时就以为万事大吉了。

将本书与《重构…》《设计模式…》 一起看,相互印证,会有豁然开朗的感觉。比如要重构一段代码,重构、使其符合某个设计模式、使代码更整洁,这些都是统一的。

高水平的代码具有某种一目了然的简单性。专家能够弄清什么是不可简化的复杂性,可以识别和消除大部分意外的复杂性。当系统复杂性增加时,为系统增加更多价值的成本保持不变。

所谓优雅,相对应的是坑。只有见过足够多的坑,才会形成自己的编码理念。

《clean code》 主要为我们分享:

  1. 什么是整洁代码
  2. 在函数、类、系统、并发等层面上,如何编写整洁代码

Erlang 的作者之一 Joe Armstrong:”Make it work, then make it beautiful, then if you really, really have to, make it fast. 90 percent of the time, if you make it beautiful, it will already be fast. So really, just make it beautiful!”

什么是不好的代码

  1. 改动一个需求,涉及的类很多
  2. 不能主要通过新增代码,甚至是新增类解决 新需求,而需要更改现有代码/类
  3. 写的很复杂,那一定说明你没有把事情想清楚。
  4. 业务逻辑无明显的边界,互相调用,在更高层进行组合,由于层级不明确通常面临着树状结构变成网状结构(一个有条理的文章一定是树状结构的)。

代码层面的整洁

  1. 函数只做该函数名下同一抽象层上的步骤,函数中的语句都要在同一抽象层级上。 PS:人的沟通也是如此。
  2. 给变量、函数、类起一个有意义的名字,名称应与抽象层级相符这个知道的多,做到的少。

    • 比如测试类的测试方法写一个test就完事
  3. 最理想的参数数量是0,其次是一,再次是二,应尽量避免三。为何?参数与函数名通常处在不同的抽象层级,它要求你了解目前并不特别重要的细节。从测试角度看,参数更让人为难。
  4. 应当避免布尔参数(或枚举等类似选择行为的参数),它明显表示函数做了不止一件事
  5. 输出参数更让人难以理解。

    • 在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面向对象语言中对输出参数的大部分需求已经消失了,因为this也有输出函数的意味在内。
    • writeField(outputStream,name) 可以把writeField写成outputStream的成员之一;把outputStream 写成当前类的成员变量,从而无需传递它;还可以分离出FieldWriter类,在其构造方法中采用outputStream,并且包含一个write方法。
  6. 函数要么做什么事,要么回答什么事儿,这二者不可兼得。不可能兼得的原因不是写起来有问题,而是用起来经常会带来可读性损失。
  7. 重复可能是软件中一切邪恶的根源,面向对象的继承、aop、面向组件编程多少都是消除重复的策略
  8. 写代码和写别的东西很像,在写论文或文章时,你先想写什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。关键是,你别写完代码就写别的代码去了
  9. 如果你做好了上述细节,那么基本不需要注释,注释是一种失败,原因很简单:程序猿不能坚持维护注释。
  10. 代码格式关乎沟通,而沟通是代码开发者的头等大事,而不是“让代码能工作”。
  11. 如果一个函数很长,那么通常需要拆分。一个变量列表很长,通常也需要拆分。
  12. 错误处理

    • 使用异常而非返回码
  13. 测试代码和生产代码一样重要,它需要被思考、设计和照料,没有了测试,你就很难做改动。

    • 测试应该有布尔值输出,你不应查看日志文件来确认测试是否通过
    • 正常的业务代码中,或许应为方便测试留有一席之地。比如,一个对象虽然理论上只需提供get方法,但为了方便测试,可以提供set方法以直接注入demo数据。

这些具体细节与《重构——改善既有代码的设计》是相辅相成的

  1. 前者突出编写代码(代码还未完成),后者突出重构。
  2. 以方法为例,前者要求“方法参数越少越好,不要超过三个”(“坏味道”描述的比较明确),后者提出重构方法的各种技巧。
  3. 知道重构的技巧并不是难点,这又回到了老问题:知易行难,还是知难行易。

内聚比较直观的定义:类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。

保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加,出现这种情况时,往往意味着至少要有一个类要从大类中挣扎出来。

假设一个有许多变量的大函数function,你想把该函数中某一部分拆解成单独的函数subFunction。不过,subFunction使用了function中的4个变量,这4个变量都作为参数传递到subFunction么?只需要将这4个变量提升为实体变量,并将4个实体变量和subFunction一起抽出一个类即可。所以,拆函数往往跟拆类是一起的。保持内聚性,就会得到许多短小的类。

对象和数据结构

对比以下两个定义:

public class Point{
	public double x;
	public double y;
}

public class Point{
	private double x;
	private double y;
	public void setX(double x){
		this.x = x;
	}
	public double getX(){
		return x;
	}
	public void setY(double y){
		this.y = y;
	}
	public double getY(){
		return y;
	}
}

public interface Point{
	double getX();
	double getY();
	void setCartesian(double x,double y);
	double getR();
	double getTheta();
	void setPolar(double r,double theta);
}

第一段和第二段代码本质是一样的,第三段代码漂亮之处在于:

  1. 你不知道该实现会是在矩形坐标系中,还是在极坐标系中,可能两个都不是!然而,该接口还是明白无误的呈现了一种数据结构。
  2. 固定了一套存取策略,你可以单独读取某个坐标,但必须通过一次原子操作设定所有坐标。

无论出于怎样的初衷,setter/getter都把私有变量公开化,诱导外部函数以过程式程序使用数据结构的方式使用这些变量

隐藏实现并非只是在变量之间放上一个函数层那么简单,隐藏实现关乎抽象!第一/二段代码更像数据结构,暴露其数据,没有提供有意义的函数。第三段代码,对象,把数据隐藏于抽象之后,暴露操作数据的函数(暴露行为)。暴露什么,不暴露什么,直接暴露,还是间接操作,都关于抽象。

过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。老练的程序猿知道,一切都是对象只是一个传说,合适的才是最好的。比如dto,本身就是为了传输数据存在的,全是setter/getter 方法。

  行为 数据
对象 暴露 隐藏
数据结构 没有 暴露

隐藏什么,就容易更改什么。暴露什么,就难以新增什么。

系统层面的整洁

  1. 构造和使用分开

    • 一栋写字楼在建设时,有建筑工人和起重机,而在使用时只有白领和玻璃幕墙。启动代码、类构造代码很特殊,不要和使用逻辑混杂在一起。
    • 这或许是spring 在整洁代码这块的意义
  2. 系统新增功能不可避免,但是否可以不和原有代码写在一起呢?

    • 原有的代码,类尽量内聚。模块边界尽量清晰。
    • 通过aop等办法,将新业务逻辑和原来的业务代码织入到一起。这要求,合理的切分关注面,模块化系统性关注面
    • 良好的抽象,通过提供新的实现类的方式新增功能

并发代码的整洁

  1. 对象是过程的抽象,线程是调度的抽象
  2. 并发是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)分开。
  3. 分离线程相关代码和线程无关代码

并发编程的难点

public class x{
    private int lastIdUserd;
    public int getNextId(){
        return ++lastIdUserd;
    }
}

所谓线程相互影响,是因为线程在执行getNextId时有许多路径可行,需要理解Just-in-time编译器如何对待生成的字节码,还要理解java内存模型认为什么东西具有原子性。

就生成的字节码而言,对于在getNextId方法中执行的两个线程,有12870种不同的可能执行路径,如果lastIdUsed 的类型从int变为long,则可能路径的数量则增至2704156种。当然,多数路径都得到正确的结果,问题是其中一些不能得到正确结果。

这一段,让笔者想到了数据库事务,一行代码就像 一句sql 一样。

  1. 对于java,开发人员直接写的是代码,会被翻译成字节码。无特殊指令,os不保证指令的原子性,数据的可见性;jvm 提供的字节码指令类似。
  2. 对于数据库,开发人员直接写的是sql,会被解释为执行计划。数据库保证事务(sql序列)的ACID

它们的一个共同点是:虽然可以实现,但jvm不会直接保证一行java代码是原子的,数据库不会直接保证一个sql执行是原子的。尤其是代码,原子性和可见性(反映在sql中类似隔离性)的保证需要开发人员介入。

并发模型

大部分并发问题,都是这三个问题的变种

  1. 生产者消费者模型
  2. 读者作者模型,共享资源主要为读者提供信息源,偶尔被作者线程更新。
  3. 宴席哲学家。前两者多少带点协作性质,哲学家模型则纯粹是对资源的分时使用

《重构 改善既有代码的设计》

我们以前理解继承和接口,都是先知道概念,然后解释这个有什么用。其实,更自然的路径应该是,碰到问题,发现需要这样的一个工具。

《重构 改善既有代码的设计》有一个“提炼接口”一节,中间提到:类之间彼此互用的方式有若干种。

  1. “使用一个类”通常意味着用到该类的所有责任区。
  2. 另一种情况是,某一组客户只使用类责任区中的一个特定子集。
  3. 再一种情况则是,这个类需要与所有协助处理某些特定请求的类合作。 对于后两种情况,将真正用到的这部分责任分离出来通常很有意义,因为这样可以使系统的用法更清晰,同时也更容易看清系统的责任划分。如果新的类需要支持上述子集,也比较能够看清子集内有些什么东西。

《重构 改善既有代码的设计》有一个“梳理并分解继承体系”一节,中间提到:某个继承体系同时承担两项责任,建立两个继承体系,并通过委托关系让其中一个可以调用另一个。

《重构 改善既有代码的设计》有一章名为“处理概括关系”,包括

  1. 提炼子类、超类、接口
  2. 折叠继承体系
  3. 塑造模板函数
  4. 以委托取代继承

重构是一种微操作。很多稍有规模的修改,如果不能以重构的方式进行,常常很快就不知道自己改到哪了,这也是很多所谓“重写”项目面临的最大风险:一旦开始,不能停止。

代码的“坏味道”是这本书给行业最重要的启发。很多人常常是无法嗅到代码坏味道的,因此,他们会任由代码腐坏,那种随便添加 if 或标记的做法就是嗅不出坏味道的表现。

许式伟:怎么搬代码?和删除代码类似,我们要找到和该功能相关的所有代码。但是我们做的不是删除,而是将散落在系统中的代码集中起来。我们把对系统的每处修改变成一个函数,比如叫 doXXX_yyyy。这里 XXX 是功能代号,yyyy 则依据这段搬走的代码语义命个名。如果某个地方有好几个功能都加了 doXXX_yyyy 这样的调用,集中了这个功能所有代码后,这个功能与系统的耦合也就清楚了。