技术

Python ioc 从0到1构建一个db 上下文记忆 agentic chat 图数据库的一些考量 LLM一些探索 Agent实践 LLM预训练 向量数据库的一些考量 fastapi+sqlalchemy进行项目开发 LLM微调实践 Python协程实现 Agent Functon Calling LLamaIndex入门 Multi-Agent探索 Python虚拟机 LLM工作流编排 Python实践 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 Transformers源码学习 LangChain源码学习 通用分布式计算引擎Ray Python并发 go依赖注入 go collection gc的基本原理 golang性能分析及优化 数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习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内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup 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快速入门

架构

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

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 kubevela源码分析 容器和CPU那些事儿 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 K8S YAML 资源清单管理方案 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI——kubelet与容器引擎之间的接口 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 什么是云原生 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件
上下文记忆 agentic chat bert rerank微调 大模型推理tips LLM一些探索 Agent实践 LLM预训练 RAG向量检索与微调 LLM微调实践 RAG与知识图谱 大模型推理服务框架vLLM Agent Functon Calling LLamaIndex入门 Multi-Agent探索 LLM工作流编排 大模型推理服务框架 模型服务化(未完成) 大模型Post-Training 大模型训练 大模型推理 从Attention到Transformer 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Transformers源码学习 LangChain源码学习 如何应用LLM 小鼠如何驾驭大象(LLM)? AutoML和AutoDL 特征平台 实时训练 tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 推荐系统embedding原理及实践 机器学习中的python调用c 机器学习训练框架概述 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 推理服务 mpi 学习pytorch 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tensorflow学习 kaggle泰坦尼克问题实践 神经网络模型优化 概率论 直觉上理解深度学习 如何学习机器学习 深度学习泛谈

《clean code》笔记

2018年01月08日

前言

《计算机程序的构造和解释》中有这么一句话:代码必须能够被人阅读,只是机器恰好可以执行。以前在编译器还不是那么完善和智能的时候,我们还会去考虑什么样的代码,什么样的语法和调用,可以最大化的提升代码的执行速度,随着时间的发展,基本上compiler目前可以比我们做的更好了,所以我们在如何和机器相处的更好的话题上,已经被compiler完全接管了,那这个时候,我们需要做的更多的是去如何和代码后面的reader(可能是你也可能是别人)做好沟通;

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

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

高水平的代码具有某种一目了然的简单性。专家能够弄清什么是不可简化的复杂性,可以识别和消除大部分意外的复杂性。当系统复杂性增加时,为系统增加更多价值的成本保持不变。PS: 代码或架构要体现一种设计感(得有设计),让新手/生手很快可以找到一种“填代码”的感觉,而不是怎么写好像都可以。

《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. 业务逻辑无明显的边界,互相调用,在更高层进行组合,由于层级不明确通常面临着树状结构变成网状结构(一个有条理的文章一定是树状结构的)。

可维护性Maintainability,可以体现在,在做一个变更的时候,我们需要去阅读多少上下游或者相关的代码,涉及面越广,出错或者遗漏某些潜在变动的概率就大,可维护性就低;

代码层面的整洁

  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. 知道重构的技巧并不是难点,这又回到了老问题:知易行难,还是知难行易。

一文聊聊代码的可读性可读性体现优势的三个方面:

  1. 更贴近人的语言(通俗表达)。 能让后面读你代码的人能够读懂,并且是直观的通顺的理解。足够准确的去描述这个方法所做的事情。
  2. 方法的意图(明确意图)。 明确的意图本质上是现实和预期的匹配:1. 现实:代码作者在写这个方法的具体实现;2.预期:读代码的人在看到这个方法的时候心里预期的实现;两者如何是匹配的,那我们定义为这段代码是有明确意图,并且没有side effect的;side effect就是副作用,也可以理解为是否在明确定义的方法之下有夹带行为,这类事情其实代码中犯的很多,而且属于大家觉得不应该犯,但是多多少少个人在写的时候,都是潜意识的没注意到。side effect最大的问题大于惯性犯错,因为我们都是秉持相信上一代作者的设计、意图和结构,去做后面的需求迭代的,这时候意图的错误传达,会导致异常乃至故障的发生,同时这样的问题还非常不好排查,因为这个漏洞属于存在于惯性思维的掩护之下的;
  3. 层次(层次结构);不是以前我们说的比较多的应用分层的层次结构,一个函数内的代码应属于一个层次的抽象。

执行过程中的建议:上面给了一些定义说明和一种分层方式,但是部分靠理解,另外一部分靠实践慢慢来体会和熟练,都属于不是马上可以应用上的,那面向如何快速在实践中做一些针对可读性的提升和做可读性待提升代码的识别,下面给几条tips,完全是for实操,背后不一定有理论能对应背书的;

  1. Five Lines。方法实现不要超过5行,非常极端的要求,但是不是去follow这个规范,拿这个标准可以去识别潜在的可被优化的代码,帮助你去嗅到bad smell;
  2. Either call or pass。在java中,以. 符号调用为基础操作,call具体方法,视为高级操作,尽量避免两种调用方式(基础操作和高级操作)在同一个方法内;
  3. Use pure conditions。指该条件的运行没有额外的影响,这个和fp的要求一致,多次运行这个条件判断,1.结果是幂等的,2.没有其他隐含的行为动作,纯判断;no side effect;
  4. if only at the start。方法里面如果有if语句,尽量在方法的开头,如果在方法体中间有,那么需要把if整块语句拆分出去一个方法;PS: if else 通常可以简写为 if return
  5. Stay Away From Comments。远离注释(非方法声明注释),是个理想主义要求,但是这个tips得出的逻辑是: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 这样的调用,集中了这个功能所有代码后,这个功能与系统的耦合也就清楚了。

刘慧卿:

  1. 从理论和实际经验来讲,系统或代码的重构往往是个人能力实现快速提升的良好契机。相同条件下,有重构经验的同学和没有重构经验的同学,对很多概念和规则的理解深度会有很大区别。这就是典型的习得性经验,通过教学很难掌握。生活场景中的游泳,骑自行车就是习得性经验的代表。
  2. 重构的前提:重构是为了满足业务诉求而不得不做的最佳方案;重构的最大风险就是:低估了实施难度。考虑兼容现有业务,同时支撑好未来规划。负重前行,要比重启新项目要复杂的多,如果新项目的难度是0到1,那么重构就是从-1到1;重构最大难度是:目标制定和过程管理。

未来

目前业界用的比较多的代码生成工具是github copilot,底层基于GPT3,代码生成工具的强项其实还是在所谓的基础方法生成的范畴,业务方法的生成,应该还是离直接使用比较难,因为业务代码的生成,需要理解业务,理解商业,这个考虑到目前各类copilot训练的数据集来说,基本上不太可能完成;对于具体的方法,应该都可以比较好的去生成对应的代码,并且能具备比较好的代码接受率,同时这类方法都是比较容易通过单测来保障实现的正确性的;基于以上,个人觉得后面业务团队的编码方式会产生这样的变化:业务技术同学实现业务语义到通俗编程语言语义的转换,主要包括方法的定义(包含方法的职责,实现的功能定义),方法的调用顺序(主要面向业务规则和需求),方法调用失败或异常的处理等,然后具体的子方法的内部实现,可以通过各类的代码生成工具进行,并且可以通过工具生成对应的单测,也许后面更进一步,业务产品定义代码执行流程,技术同学都不需要做programming,只需要去做验证生成的代码是否正确的debug工作。