读书笔记|程序员的README

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 作者阅读《程序员的README》这本书后结合了自己一年在工作中的经历,总结出程序员工作中的新认知。

入职一年后再读到这本书其实有些晚了,每一章节几乎都是在这一年的工作中所经历和摸索过后才建立的认知,每一章外延为单独的一本书大概才能成体系地深入学习,作者也确实每一章的末尾都做了相关阅读推荐。


按「道法术器」的角度看,这本书讲的应该更多是「法」和「术」的东西,虽然也可以被理解为是叭叭一堆“正确的废话”,但只要是正确的就是值得输入的,哪怕最终对自己的帮助和影响只有分毫。


此外不得不提的是,本书的翻译实在是过于“简单直接”,和Google机翻也大致无二了,尽可能还是阅读英文原版吧。

书名:程序员的README

作者:克里斯·里科米尼 德米特里·里亚博伊

译者:付裕

出版社:人民邮电出版社有限公司

出版时间:2023-07-01

ISBN:9787115599438

品牌方:人民邮电出版社有限公司

image.png


第1章 前面的旅程

入门工程师晋级需要具备的核心领域能力:


  1. 技术知识
  2. 执行力
  3. 沟通能力
  4. 领导力


新手期过程:

1. 新人入职:熟悉公司、团队、本职工作。参加入职会议,设置开发环境,弄清楚团队的常规流程和会议,阅读文档并向同事请教,参加入职培训,了解组织架构。建议在团队中用文档记录下会议的内容、入职流程和其他口口相传的东西,重点不是要写一份完美的文档,而是要写得足够多,以引发讨论、充实细节。被分配到一个小任务,学习如何修改代码并安全发布到生产环境,如果没有就要主动要求承接小需求。这些改动一定要小,重点是去了解那些规范步骤。


2. 成长试炼:为团队开始分担真正的工作,熟悉代码库,了解编译、测试和部署代码的流程,多提问,多拉代码评审。持续学习,多参加技术讲座、阅读小组、导师计划。参加迭代会议、评审会议、项目会议,了解团队的产品全貌和路线规划。与管理者建立联系,了解他们的工作风格和期望,并在一对一沟通中表达自己的目标。


3. 贡献价值:提升自己编写生产级代码的能力。主动帮助队友,参与到代码评审。提升自己交付和运维的水平,包括监控、日志、跟踪、调试。独立负责一个小项目,撰写技术设计文档并帮助团队进行项目规划。在系统架构、项目代码中看到不足,进行必要的维护和重构。参与到团队计划当中,与管理者一同制定目标或OKR,主动表达自己的想法和规划,并在绩效评估中获得反馈。

坎宁安定律 (Cunningham’s Law):The best way to get the right answer on the Internet is not to ask a question, it's to post the wrong answer. 在谈话中,我们也能利用“说错话”来激发更多的对话。
自行车棚效应 (Bike-shed Effect) / 帕金森琐碎定律(Parkinson's Law of Triviality):A common phenomenon in group decision-making where people give disproportionate weight to trivial issues. 一个被指派到发电厂对该发电厂的设计方案进行评审的委员会,因为发电厂的设计方案过于复杂,以至于无法讨论出什么实际的内容,所以他们花了几分钟就批准了这些计划。然后,他们又花了45分钟来讨论发电厂旁边的自行车棚的材料问题。


第2章 步入自觉阶段

学习如何学习:


  1. 前置学习:入职后学习开发流程和项目知识,参与设计讨论、On-Call轮换、解决运维问题和评审代码。
  2. 在实践中学习:上手编写并发布代码,适当谨慎,不要过于害怕造成问题。
  3. 执行代码:在生产环境外运行项目,多调试,多打日志。
  4. 阅读:团队文档、设计文档、代码、积压的tickets、书籍、论文和技术网站。
  5. 观看讲座:观看教程、技术讲座和阅读会议简报。
  6. 适度地参加会议和聚会:学术会议、草根兴趣小组聚会和科技展览会,偶尔参加。
  7. 跟班学习并同有经验的工程师结对:在另一个人执行任务时跟着他,他做笔记并提出问题;结对编程(pair programming),两名工程师一起写代码,轮流编程。
  8. 用副业项目实践:贡献导向,参与开源项目;兴趣导向,找到有兴趣解决的问题,用想学习的工具来解决。


提出问题:


  1. 尝试自己寻找答案:不要直接请教同事,可以尝试互联网、文档、内部论坛、聊天记录、源代码等地方。
  2. 设置时间限制:设定好研究一个问题预期花费的时间,防止收益递减。
  3. 记录全过程:向别人提出问题时,要描述背景、自己的尝试和发现。
  4. 别打扰:注意提问的时机,不要打扰别人工作的专心状态。
  5. 多用“非打扰式”交流:组播(multicast)是指将消息发送到一个组而不是个人目标;异步(asynchronous)是指可以稍后处理消息,而不需要立即响应。即多使用群聊,邮件或论坛。
  6. 批量处理你的同步请求:面对面的交流是“高带宽”和“低延迟”的,所以在聊天和邮件外,预约时间或安排会议集中处理问题更好,但事前要做好功课,事中要多记录,事后要有反馈。


克服成长的障碍:

  1. 冒充者综合征:觉知(awareness),意识到是自己主动在寻找“冒充”的证据,而自己是真真切切取得了成就;重塑(awareness),把悲观的、消极的自我暗示转化为乐观的、积极的自我鼓励;反馈(feedback),与导师、敬重的同事交流,明确自己做得怎么样。
  2. 邓宁-克鲁格效应:有意识地培养好奇心;对犯错持开放态度;向受人尊敬的工程师取经;讨论设计决策并倾听建议;培养一种权衡利弊而不是非黑即白的心态。


推荐阅读:
  1. 《软件开发者路线图:从学徒到高手》(Apprenticeship Patterns: Guidance for theAspiring Software Craftsman)


  1. 《你要做的全部就是提问:如何掌握成功最重要的技能》(All You Have toDo Is Ask:How to Master the Most Important Skill forSuccess)


  1. 《解析极限编程——拥抱变化》(ExtremeProgramming Explained: Embrace Change)


  1. 《高能量姿势:肢体语言打造个人影响力》(Presence: Bringing Your Boldest Self to Your BiggestChallenges)


马丁·M.布罗德威尔在其文章《为学而教》(“Teaching forLearning”)中定义了能力的4个阶段:“无意识的无能力”(unconscious incompetence)、“有意识的无能力”(consciousincompetence),“有意识的有能力”(conscious competence)和“无意识的有能力”(unconscious competence)。具体说来,无意识的无能力意味着你无法胜任某项任务,并且没有意识到这种差距。有意识的无能力意味着你虽然无法胜任某项任务,但其实已经意识到了其中的差距。有意识的有能力意味着你有能力通过努力完成某项任务。最后,无意识的有能力意味着你可以很轻松地胜任某项任务。

冒充者综合征(冒名顶替症候群,Impostor syndrome):a behavioral health phenomenon described as self-doubt of intellect, skills, or accomplishments among high-achieving individuals.

邓宁-克鲁格效应(Dunning-Kruger effect):a cognitive bias in which people with limited competence in a particular domain overestimate their abilities.


第3章 玩转代码

软件的熵(software entropy):混乱的代码是变化的自然副作用,不要把代码的不整洁归咎于开发者,这种走向无序的趋势是必然的。


技术债(technical debt):


  1. 定义:为了修复现有的代码不足而欠下的未来工作,是造成软件的熵的主要原因之一。与金融债务一样,技术债也有“本金”和“利息”,本金是那些需要修复的原始不足,利息是随着代码的发展没有解决的潜在不足。因为实施了越来越复杂的变通方法,随着变通办法的复制和巩固,利息就会增加,复杂性蔓延开来,就会造成bug。


  1. 技术债矩阵:
  • 谨慎的、有意的:在代码的已知不足和交付速度之间进行务实的取舍,团队要有规划地解决。
  • 鲁莽的、有意的:在团队面临交付压力的情况下产生的,“就...”,“只是...”这样的形容就是鲁莽债务。
  • 鲁莽的、无意的:不知道自己不知道,需要通过做方案评审、代码评审和持续学习来最大限度减少。
  • 谨慎的、无意的:成长经验积累的自然结果,有些教训只有在事后才会被吸取。


  1. 解决:
  • 不要等到世界都停转一个月了才去解决问题,要边做边解决,着手去做小幅的重构。
  • 有时增量重构还不够,需要大型的重构。短期看,偿还技术债会拖慢交付特性的速度,而承担更多的技术债会加速交付,但长期看情况则相反,平衡点在很大程度上取决于环境。如果有大规模重构或重写某些模块的建议,可以向团队说明情况:按事实陈述情况,描述技术债的风险和成本,提出解决方案,讨论备选方案(不采取行动也是备选之一),权衡利弊。要注意的是以书面形式提出建议,不要把呼吁建立在价值判断上(“这段代码又老又难看”),要足够具体,将重点放在技术债的成本和修复它带来的好处上。


变更代码:

  1. 善于利用现有代码:定位需要改变的代码即变更点并阅读,然后找到测试点即修改代码的入口,这也是测试用例需要调用和注入的区域。有时候不得不打破现有依赖结构,比如拆分方法、引入接口,这可以更方便地编写测试用例。


  1. 过手的代码要比之前更干净:在不影响整个项目持续运转的情况下要持续地重构工程,这样重构的成本就会平摊在多次的版本更迭中。


  1. 做渐变式的修改:不要一次提交就“翻天覆地”,保持每次重构代码的体量很小,要尽量将重构代码的提交和新特性的提交分开。


  1. 对重构要务实:团队的工作有优先事项和交付时效,重构需要花费时间,成本也可能超过其价值。正在被替换的旧的、废弃的代码,低风险或很少被触及的代码,都是不需要重构的。


  1. 善用IDE:重构时多使用IDE的功能,比如抽取方法和字段、更新方法签名等。


  1. 善用Git:尽早并频繁地commit,但一定要规范自己的commit message,要压缩但清晰。


避坑指南:


  1. 保守一些的技术选型:新技术的问题是它不太成熟,缺乏成熟的稳定性、兼容性、框架、文档、社区。只有新技术的收益超过其成本时,才值得考虑。


  1. 不要特立独行:不要因为个人喜好就忽视公司或行业标准,自定义的方案必然会付出代价。


  1. 不要只fork而不pr:没有及时贡献到上游代码库的小变更也会随着时间的推移而变得复杂。


  1. 克制重构的冲动:把重构看作最后的手段,因为其成本和风险都巨大,但又很难评估价值。
推荐阅读:

  1. 《修改代码的艺术》(Working Effectively with Legacy Code)


  1. 《处理遗留代码的工具箱:软件专业人员处理遗留代码的专业技能》(The Legacy Code Programmer’sToolbox: Practical Skills for Software Professionals Workingwith Legacy Code)


  1. 《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code)


  1. 《人月神话》(The Mythical Man-Month)


本·霍洛维茨在他的《创业维艰:如何完成比难更难的事》(TheHard Thing About Hard Things)一书中说:任何技术创业公司必须做的主要的事情是建立一个产品,这个产品在做某件事情时至少要比目前流行的方式好十倍。两倍或三倍的改进不足以让人们快速或大量地转向新事物。

丹·麦金利在他的演讲“选择保守的技术”中指出“在保守技术上出现的故障模式很好理解”。所有的技术都会发生故障,但旧的东西以可预测的方式发生故障,新东西往往会以令人惊讶的方式发生故障。

弗雷德里克·布鲁克斯在他的名作《人月神话》(The Mythical Man-Month)中创造了“第二系统综合征”这一短语,描述了简单系统如何被复杂系统所取代。第一个系统的范围是有限的,因为它的创造者并不了解可能会出问题的地方。这个系统完成了它的工作,但它是笨拙的和有限的。现在有经验的开发者清楚地看到了他们的问题所在,他们开始用他们的一切聪明才智来开发第二个系统。新系统是为灵活性而设计的,所有东西都是可配置和可注入的。可悲的是,第二个系统通常是一个臃肿的烂摊子。


第4章 编写可维护的代码

防御式编程:

  1. 避免空值:使用空对象模式(null object pattern)或可选类型(option type)来避免空指针异常;使用@NotNull注解替代手动空值检查让代码更干净。


  1. 保持变量不可变:将变量声明为不可变可以防止意外修改,比如Java的final。


  1. 使用类型提示和静态类型检查器:限制变量可以被赋的值。


  1. 验证输入:永远不要相信你的代码接收的输入,一定要进行校验。


  1. 善用异常:不要使用特殊的返回值来标识错误类型(如null、0、−1等),尽量使用异常,它可以被命名,有堆栈跟踪和错误信息。


  1. 异常要有精确含义:尽可能地使用内置的异常,避免创建通用的异常。使用异常处理来应对故障,而不是控制应用程序的运行逻辑。


  1. 早抛晚捕:“早抛”意味着在尽可能接近错误的地方引发异常,这样开发人员就能迅速地定位相关的代码。“晚捕”意味着在调用的堆栈上传播这个异常,直到你到达能够处理异常的程序的层级。


  1. 智能重试:单纯的重试方法是捕捉到一个异常马上就进行重试,但可能会影响系统性能;谨慎的做法是backoff,非线性地增加休眠时间(通常使用指数如(retry number)^2);不要盲目地重试所有失败的调用,最好是让应用程序在遇到其在设计时没有预想到的错误时崩溃,即fail-fast。


  1. 构建幂等系统:处理重试的最好方法是构建幂等系统,一个幂等的操作是可以被进行多次并且仍然产生相同结果的操作,比如往一个Set里添加一个值,客户端单独为每个请求提供一个唯一ID。


10. 及时释放资源:故障发生后,要确保清理所有的资源,释放你不再需要的内存、数据结构、Socket和文件句柄。


日志:

  1. 给日志分级:通过全局配置和对包或类级别的覆写来控制,设置后所有处于该级别或高于该级别的日志都会被发出来,常见分级有TRACE、DEBUG、INFO、WARN、ERROR、FATAL。


  1. 日志的原子性:原子日志就是指在一行消息中包含所有相关的信息,不能原子化输出时考虑在每一条日志中加上Trace ID。


  1. 关注日志性能:用参数化的日志输入及异步附加器避免日志对性能的损害。


  1. 不要记录敏感数据:日志信息不应该包括任何私人数据,如密码、安全令牌、信用卡号码或电子邮件地址。


系统监控:


  1. 常见系统指标形式:计数器、仪表盘和直方图(百分比),汇总到一个集中式可视化系统,在聚合指标之上提供面板和监控工具,跟踪SLO(service level objective,服务等级目标),根据指标值触发警告,或扩容缩容。


  1. 使用标准的监控组件:不要推出你自己的系统指标库,非标准库是“维护噩梦”,而标准库可以与其他一切“开箱即用”的东西集成。


  1. 测量一切:监测的性能开销很低,要尽可能地监测各种数据结构、操作和行为,比如资源池(线程池、连接池)的大小、缓存的命中数和失误数、数据结构的大小、CPU密集型操作的性能开销、I/O密集型操作的处理时间、异常和错误的数量、远程请求和响应的数量和耗时。


跟踪器:


  1. 堆栈跟踪:异常。


  1. 分布式调用跟踪:Trace ID。


配置相关注意事项:


  1. 配置的形式和方式:形式有JSON或YAML文件,环境变量,命令行参数,领域特定语言(DSL),应用程序所使用的语言;方式可以是静态配置或动态配置。


  1. 记录并校验所有的配置:在程序启动时立即记录所有(非秘密的)配置,并加载配置的值时立即对其进行类型和逻辑意义的校验。


  1. 不要玩花活:配置方案越聪明,bug就越奇怪,尽量使用最简单、有效的方法,理想状态应该是单一标准格式的静态配置文件;如果不得不使用变量替换、条件配置或动态配置,也要日志跟踪配置的变化和记录实际生效的配置值。


  1. 提供默认值:提供良好的默认值能让应用开箱即用,避免用户上手就配置大量参数。


  1. 给配置分组:可以使用YAML的嵌套将相关属性分组。


  1. 配置即代码(configuration as code,CAC):配置应该受到与代码同样严格的要求,要进行版本控制、评审、测试、构建和发布。


  1. 保持配置文件清爽:删除不使用的配置,使用标准的格式和间距,不要盲目地从其他文件中复制配置。


  1. 不要编辑已经部署的配置:避免在生产环境中手动编辑某台特定机器上的配置文件。


工具集:


  1. 多了解并尽量使用公司或团队的通用运维工具。


  1. 如果是自己编写的脚本,一定要注意规范和测试,有价值的工具可以尝试抽象为共享库或服务。
推荐阅读:
  1. 《代码大全:软件构造之实践指南》(CodeComplete: A Practical Handbook of Software Construction)
  2. 《代码整洁之道》(Clean Code: A Handbook of Agile Software Craftsmanship)
  3. 《Google系统架构解密:构建安全可靠的系统》(BuildingSecure & Reliable Systems:Best Practices for Designing, Implementing, and Maintaining Systems)
  4. 《SRE:Google运维解密》(Site Reliability Engineering: How Google Runs ProductionSystems)


第5章 依赖管理

依赖管理基础知识:

  1. 相依性是指你的代码所依赖的代码,依赖关系是在软件包管理或构建文件中声明的。


  1. 好的版本管理方案要有唯一性(版本不应该被重复使用),可比性(推断版本的优先顺序),信息性(区分预发布和正式发布)。


  1. 语义版本管理(semantic versioning,SemVer):主版本号.次版本号.补丁版本号,还可以添加一个-来定义预发布版本。


  1. 传递依赖:软件包管理或构建文件揭示了项目的直接依赖关系,直接依赖又依赖于其他类库,形成依赖传递。


相依性地狱:


  1. 循环依赖:A依赖B,B依赖C,C依赖A。一个库间接性地依赖它自己


  1. 钻石依赖:A依赖B和C,B和C依赖D。一个底层库被多个路径依赖。


  1. 版本冲突:A直接或间接依赖了B的2个版本。一个项目中存在同一个库的多个版本。


避免相依性地狱:


  1. 隔离依赖项:不必把依赖管理交给构建和打包系统,直接复制依赖相关的必要代码,Java还可以使用包路径的区分来遮蔽依赖。


  1. 按需添加依赖项:将你使用的所有类库显式声明为依赖项,不要使用来自横向依赖的方法和类。
  2. 指定依赖项的版本:明确设定每个依赖项的版本号。


  1. 依赖范围最小化:依赖范围指在构建生命周期的编译、运行、测试阶段何时使用某个依赖,对每个依赖项指定尽可能精确的依赖范围。


  1. 保护自己免受循环依赖的影响:使用构建工具检测,不要引入循环依赖。
推荐阅读:
  1. SemVer的官方主页
  2. Python官方主页的版本管理说明
帕累托法则(Pareto principle):也被称为二八定律,由意大利经济学家维尔弗雷多·帕累托在20世纪初提出,他发现意大利约有80%的土地由20%的人口所有。在编程领域,80%的软件缺陷来源于代码中的20%部分;80%的性能提升可以通过优化20%的代码来实现;80%的开发时间被用来完成项目中20%的功能点。


第6章 测试

测试的多种用途:


  1. 检查代码是否正常工作,验证软件的行为是否符合预期。


  1. 编写测试迫使开发人员思考他们程序的接口和实现过程,尽早地暴露出笨拙的接口设计和混乱的实现过程。


  1. 测试代码可以是另一种形式的文档,是开始阅读并了解一个新的代码库的入口。


测试类型:


  1. 单元测试:测试某个单一的方法或行为。


  1. 集成测试:验证多个组件集成在一起之后是否还能正常工作。


  1. 系统测试:验证整个系统的整体运行情况。


  1. 性能测试:负载测试和压力测试并监控系统性能。


  1. 验收测试:验证交付的软件是否符合验收标准。


测试工具:


  1. 模拟库:用于单元测试,特别是面向对象的代码中,可以模拟外部依赖的系统、类库或对象。


  1. 测试框架:管理测试的setup和teardown,管理测试执行和编排,提供工具,生成结果报告和代码覆盖率。


  1. 代码质量工具:运行静态分析并执行代码风格检查,报告复杂度和测试覆盖率等指标。


自己动手编写测试:


  1. 编写干净的测试:测试代码也要注意质量,专注于测试基本功能而不是实现细节,将测试的依赖项与常规代码的依赖项分开。


  1. 避免过度测试:编写有意义的、对代码影响最大风险点的测试,不要为了覆盖率而去写无效测试。


测试中的确定性:


1. 确定性的代码对于相同的输入总是给予相同的输出;非确定性的代码对于相同的输入可以返回不同的结果。测试中的不确定性降低了测试的价值,间歇性的测试失败(被称为拍打测试)是很难重现和调试的,应该被禁用或立即修复。


2. 随机数生成器:可用一个常数作为随机数生成器的种子,迫使每次运行拿到相同的随机数。


3. 不要在单元测试中调用远程系统:模拟依赖,规避网络的不稳定和远程系统的不可靠,保证单元测试可移植。


4. 采用注入式时间戳:使用注入式时间戳而不是静态时间方法now或sleep,控制代码在测试中获取的时间。


5. 避免使用休眠和超时:sleep和超时都假设另一个执行线程会在特定时间结束,但可能会因为垃圾回收或操作系统导致意外,还会减慢测试执行过程。


6. 记得关闭socket和文件句柄:避免系统资源的泄漏,可以多用try- with-resource,共享资源在setup和teardown方法中进行管理。


7. 绑定到0端口:测试不应该绑定到某个特定的网络端口,避免端口占用,要绑定到0端口允许操作系统去自动选择。


8. 生成唯一的文件路径和数据库位置:测试不应该写入某一个已经被静态定义好了的位置,要动态地生成唯一的文件名、目录路径以及数据库或表名。


9. 隔离并清理剩余的测试状态:状态存在于数据生命周期内的任何地方,全局变量如计数器是常见的内存状态,而数据库和文件是常见的磁盘状态,共享资源都可能导致意外和干扰,测试结束都要重置内存状态,清理磁盘状态。


10. 不要依赖测试顺序:特定的执行顺序会让测试变得难以调试和验证,改用setup在测试之间共享逻辑,teardown在测试结束后清理数据。

推荐阅读:
  1. 《单元测试:原则、实践与模式》(Unit Testing: Principles, Practices, and Patterns)
  2. 《测试驱动开发:实战与模式解析》(Test-DrivenDevelopment: By Example)
  3. 《程序员修炼之道——从小工到专家》(The Pragmatic Programmer: From Journeyman to Master)
  4. 《探索吧!深入理解探索式软件测试》(Explore It!: Reduce Risk and Increase Confidence with Exploratory Testing)


第7章 代码评审

为什么需要评审代码:


  1. 捕捉bug并保持代码整洁。


  1. 团队的教学和学习工具。


  1. 评审的comments也是一种文档。


  1. 保证安全和合规。


当你的代码被评审时:


  1. 准备工作:保持单个代码的小幅改动,将特性和重构工作分到不同的评审中,并写出描述性的commit message,务必将注释和测试包括在内。


  1. 用评审草案降低风险:在真正进行代码的编写和评审之前,进行代码草案即大致设计和改造点的评审。


  1. 提交评审请勿触发测试:通过提交代码评审来触发持续集成的测试是一种浪费,CI环境应该只用于要发布的代码。


  1. 预排大体量的代码修改:大体量代码修改的评审要提前做好预排,提前让同事们了解设计文档和熟悉代码,演示时按照运行步骤来讲解代码。


  1. 不要太在意:批评性的评论可能让你很难接受,切记应该保持一些情感上的距离——这些评审意见是针对代码的,而不是针对你个人的,而且这甚至都不算是你的代码,这是整个团队会拥有的代码。


  1. 保持同理心,但不要容忍粗鲁:允许评审者怀疑,但如果他们的评论偏离了中心或粗暴无礼,要明确沟通,扫清障碍。


  1. 保持主动:不要羞于要求别人评审你的代码,收到评论时要有所回应。


评审别人的代码时:


1. 分流评审请求:根据代码的关键程度、变更的体量来分流评审。


2. 给评审预留时间:不要每次有评审需求时就中止你正在做的一切,最好能预留时间集中处理。


3. 理解修改的意图:不要一上来就提交评论,先阅读理解再提出问题。


4. 提供全面的反馈:对代码修改的正确性、可实施性、可维护性、可读性和安全性提供反馈。


5. 要承认优点:评审代码时会很自然地集中在发现问题上,但代码评审不一定意味着都得是负面的评论,对好的东西也要进行赞扬。


6. 区分问题、建议和挑剔:写comments的时候要做好分类,强制 / 推荐 / 参考。


7. 不要只做橡皮图章:抵制用草率批准的方式快速给评审点击通过的形式主义,橡皮图章式的评审是有害的。


8. 不要只局限于使用网页版的评审工具:仍然可以把代码拉下来到IDE中检查。


9. 不要忘记评审测试代码:阅读测试代码可以更好理解变更点,也要检查测试代码的可维护性和清洁度。


10. 推动决断:不要坚持要求完美,不要扩大修改范围,清楚描述哪些评审意见是关键的,帮助提交者尽可能快地通过评审,不要让质量成为效率的障碍。


推荐阅读:
  1. 《开发者代码评审指南》(“Code Review Developer Guide”)
  2. 《高难度谈话Ⅱ:感恩反馈》(Thanks for the Feedback:The Science and Art of Receiving Feedback Well)


第8章 软件交付

软件交付流程:

  1. 并没有行业标准定义。
  2. 作者讲解使用的流程:构建(build)、发布(release)、部署(deploy)和展开(rollout)。


分支策略:

  1. 基于主分支的开发模式(Trunk-Based Development):主分支包含整个代码库的主版本,并有修改的历史记录。分支是从主分支上“切”下来的,以进行代码修改,多个分支允许开发人员并行工作,然后将修改过的内容合并回主分支上。只有当各分支可以快速合并到主分支时,基于主分支的开发模式的效果才是最好的,即持续集成(CI)。


  1. 基于特性分支的开发模式(Feature Branch Workflow):开发人员同时在长期存续的特性分支上工作,每个特性分支与产品中的一个特性相关联,特性分支存续时间都较长,开发人员需要经常从主分支中合并代码,使特性分支不偏离主干太远。发布时特性分支会被拉入发布分支进行测试,而特性分支可能会继续开发,最终发布的软件包是建立在稳定的发布分支上的,如果有问题就拉hotfix分支进行热修复。


构建环节:


  1. 软件包:为每个发布版本而构建,内容和结构各不相同。


  1. 打包的注意事项:带版本号,将不同的资源单独打包。


发布环节:


  1. 发布步骤:内部发布可能只需要将软件包发行到共享仓库,面向用户的发布则需要发布构建、文档更新、发行说明和用户透传。


  1. 不要只想着发布:发布不是工作的结束,你需要对发布负责,确保适当的部署和良好的运转,并及时解决在生产环境产生的问题。


  1. 将包发布到仓库:用于发布的软件包要持久化到一个存储资源库。


  1. 保持版本不变性:一旦发布了,就永远不要改变或覆盖这个已发布的包。


  1. 频繁发布:快速频繁的发布会让每个版本的风险都较小,产生更稳定的软件,当发现bug时也更容易修复。


  1. 对发布计划保持透明:发布时间表一定要明确和公开。


  1. 撰写变更日志和发行说明:可以帮助你的用户和支持团队了解一个版本中包含的具体内容。


部署环节:


  1. 自动部署:使用脚本而不是手动步骤来部署软件。


  1. 部署的原子性:脚本通常涉及多个步骤,每一步都可能失败,要保证部署要么全部完成要么什么都没做(即原子性)。简单方法之一是在与旧版本不同的位置上安装软件,不要覆盖任何东西,让回滚也变得容易。


  1. 独立地部署应用:顺序部署会降低部署效率,还可能导致冲突,最好保证每个应用可以独立部署,做好向前和向后的兼容。


展开环节:


  1. 系统监控:新代码被激活后,监测健康指标,诸如错误率、响应时间和资源消耗。


  1. 特性开关(feature flag):代码被包裹在一个if语句中,该语句检查一个flag(动态配置)以确定应该运行哪个分支的代码。flag可以是布尔值,列表,灰度百分比等。需要注意的是数据库通常不受开关控制,新旧代码会处理相同的表,因此一定要注意兼容。


  1. 熔断器:一种特殊的特性开关,由运维事件(性能阈值、异常日志)控制,熔断器的特点是二进制的(开/关)、永久性的、自动化的。


  1. 并行的服务版本梯队:
  • 金丝雀部署(Canary Deployment):用于处理高流量、部署到大量实例的服务。一个新的应用程序版本被首先部署到一组少量机器上,用户的一个子集被路由到这个金丝雀版本,进行监控和评估,决定继续扩大覆盖范围,或快速回滚切回老版本。
  • 蓝绿部署(Blue-Green Deployment):两个相同的生产环境但部署不同的应用版本。蓝色环境是当前活跃的生产环境,承载用户流量,绿色环境初始化为蓝色环境的镜像,但不承载实时用户流量,用于部署新的软件版本,就绪并测试后,切换路由让所有新的用户流量打向绿色环境,此时绿色环境变为新的生产环境,旧的蓝色环境空闲,新版本有问题可以迅速切回。当流量不容易划分的时候可以考虑蓝绿部署,但要注意每个环境都必须能够处理100%的用户流量。


  1. 摸黑启动:即影子流量,将新的代码暴露在真实的流量中,但不使其对终端用户可见,这样即使代码有问题也没有用户受到影响。实现方式是将实际用户的请求复制一份,同时发送到旧系统和新系统,这样新系统处理的请求是与旧系统完全相同的,但新系统的处理结果并不会返回给用户,而是用于内部测试和分析。


推荐阅读:
  1. 《Git团队协作:掌握Git精髓,解决版本控制、工作流问题,实现高效开发》(Git for Teams:A User-CenteredApproach to Creating Efficient Workflows in Git)
  2. 《持续交付:发布可靠软件的系统方法》(Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation)
  3. 《发布!设计与部署稳定的分布式系统(第2版)》(Release It! Design and Deploy Production-Ready Software 2nd)


第9章 On-Call

On-Call的工作方式:

  1. 开发人员根据时间表进行轮换。有些时间表会安排一名主要责任人和一名辅助的On-Call人员。


  1. On-Call人员的大部分时间用来处理临时性的支持,如bug报告、软件如何运行以及使用的问题,当然也可能遇到运维事故,响应电话或消息告警。


On-Call技能包:

  1. 随时响应。
  2. 保持专注
  3. 确定工作优先级。
  4. 清晰的沟通。
  5. 记录跟踪工作。


事故处理:


  1. 分流(triage):工程师必须找到问题,确定其严重性,并确定谁能修复它。


  1. 协同(coordination):团队(以及潜在的用户)必须得到这个问题的通知,如果On-Call人员自己不能解决这个问题,他们必须提醒那些能解决的人。


  1. 应急方案(mitigation):工程师必须尽快让事情稳定下来。缓解并不是长期的修复,你只是在试图“止血”,通过回滚一个版本、将故障转移到另一个环境、关闭有问题的特性或增加硬件资源来缓解。


  1. 解决方案(resolution):在问题得到缓解后,工程师有一些时间来喘口气、深入思考,继续调查问题,以确定和解决潜在的问题。


  1. 后续行动(follow-up):对事故的根本原因——为什么会发生,进行调查。如果事故很严重更要进行正式的事后调查或回顾性调查,建立后续任务,以防止重复的根本原因的再次出现。


提供支持:


  1. On-Call工程师也得花时间处理支持类请求,来自组织内部或外部用户,支持请求必须遵循一个相当标准的流程。


  1. 由于你“真正的”工作是编程,参与到支持工作中可能会让你分心。把支持工作看成一次学习机会,了解你的应用实际的使用方式,接触你不熟悉的代码,发现总结问题,帮助别人,建立良好的关系和影响力。


不要逞英雄:



  1. 不要“做得太多”,“救火”工程师、团队“万金油”都会让团队形成依赖,从而变成长期On-Call人员,长时间和高风险将导致倦怠,这种单点模式也是不健康的。


  1. 如果你觉得你正在成为“救火”工程师,与你的管理者或技术负责人讨论如何找到更好的平衡,让更多的人接受培训并可以介入“救火”;如果你的团队中有一名英雄,看看你是否可以向他学习,挑起一些重担。
推荐阅读:
  1. 《当寻呼机响起时,会发生什么?》(“What Happens When the Pager GoesOff ?”)


第10章 技术设计流程

技术设计的V形结构:


  1. 软件设计并不是一种将研究和头脑风暴的结论落到文档上并获得批准的线性过程。它更像是在独立工作和相互合作之间交替进行的螺旋式上升的过程。


  1. 从问题空间的了解开始,在独立工作和讨论脑暴中进行设计文档的迭代,完成设计。


关于设计的思考:


  1. 定义问题:技术设计的首要任务是定义和理解你要解决的那个(或那些)问题。了解问题的边界,以便知道如何解决,判断什么问题亟待解决,什么问题甚至不值得解决。


  1. 技术选型:不要从问题定义直接就过渡到“最终”设计。考虑相关的研究、替代的解决方案,以及权衡各方案的利弊。你提出的设计不应该是你的第一个想法,而应该是你若干想法中最好的那个。


  1. 进行实验:写代码草稿、运行测试来对你的想法进行实验,编写API草案和部分实现、运行性能测试、甚至A/B用户测试,以了解系统和用户的行为。


  1. 给些时间:好的设计需要创造力和深入思考,不要指望坐下来一次就能完成一份设计,要给自己大块的时间,休息一下,换换环境,耐心一点儿,然后深度工作。


撰写设计文档:


  1. 判断是否需要设计文档:并非每一项变更都需要设计文档或设计评审,按照团队的指导方针来决定。


  1. 了解撰写文档的目的:从表面上看,设计文档是告诉别人某个软件或模块是如何开发、工作的。但设计文档也是一种工具,可以帮助你思考、获得反馈,让你的团队了解情况、培养新的工程师,并推动项目规划。


  1. 学会写作:写作作为一项技能,就像其他技能一样,是通过实践来进步的,充分利用写作的机会——设计文档、电子邮件、代码评审意见——努力写得清晰。


  1. 保证文档是最新的:设计文档是活的,必须要随着方案的迭代和实现过程中的变更而更新,最好还要进行版本控制。


使用设计文档模板:


  1. 概要;
  2. 现状与背景;
  3. 变更的目的;
  4. 需求;
  5. 潜在的解决方案;
  6. 建议的解决方案;
  7. 设计与架构;
  • 系统构成图;
  • UI/UX变更点;
  • 代码变更点;
  • API变更点;
  • 持久层变更点;
  1. 测试计划;
  2. 发布计划;
  3. 遗留的问题;
  4. 附录。


协作设计:

  1. 理解你的团队的设计评审流程。
  2. 随时让团队和技术负责人了解设计进展。
  3. 可以使用团队内的设计讨论来脑暴。
  4. 除了自己负责的项目,也要为团队的技术设计贡献力量。
推荐阅读:
  1. 大型开源项目的文档
  2. 《吊床驱动开发》(“HammockDriven Development”)
  3. 《有效的软件设计文档》(“Effective Software Design Documents”)
  4. 《英语写作手册:风格的要素》(TheElements of Style)
  5. 《写作法宝:非虚构写作指南》(OnWriting Well: The Classic Guide to Writing Nonfiction)
  6. 《如何有用地写作》(“How to Write Usefully”)
  7. 《像说话一样写作》(“Write Like YouTalk”)


第11章 构建可演进的架构

理解复杂性:


  1. 高依赖性:即软件依赖于其他的API或代码行为,每一次新增依赖都会导致耦合让系统更难以修改。
  2. 高隐蔽性:即程序员很难预测某项变更的副作用、代码的行为方式,以及需要修改的地方。
  3. 高惯性:即对服务的调用者或软件的使用者倾向于保持之前的使用习惯,变更成本会随着时间推移而增加。


可演进的设计:


  1. You ain’t gonna need it:不要构建你不需要的东西,避免过早优化,避免不必要的灵活抽象模型,以及避免最小可行产品(minimum viable product,MVP)所不需要的产品特性。
  2. Principle of least astonishment:不要让用户感到惊讶,产品特性不要让用户有上扬的学习曲线和感到奇怪;也不要让开发者感到惊讶,晦涩难懂的代码和隐性知识会导致维护的困难和复杂,还容易产生bug。
  3. 封装专业领域知识:基于业务领域来封装模块,考虑领域驱动设计(domain-driven design,DDD)。


可演进的API:


  1. 保持API小巧:只添加当下需要的API方法和入参字段,有许多字段的API方法应该有合理的默认值。
  2. 公开定义良好的服务端API:使用业界的标准模式如OpenAPI或公司定义的模式来定义服务端API,良好的服务必须声明其模式、请求和响应方法、异常。
  3. 保持API变更的兼容性:兼容性可以让客户端和服务端版本独立发展,向前兼容的变更允许客户端在调用旧版时使用新版的API服务,向后兼容的变更让旧的客户端代码在使用新版本API时可以继续运行而无需改造。
  4. API版本化:完全向后或向前兼容会让API更难维护,最终还是需要升级版本,可以引入一个必填的版本字段用以区分API的新老版本,并统一由API网关来管理和路由,注意文档也要和API保持一致的版本化。


可持续的数据管理:


  1. 数据库隔离:共享的数据库由多个应用程序共同访问,很难演进,并且会导致丧失自主性——开发人员或团队对系统进行独立修改的能力。隔离的数据库只有一个读取者和写入者,其他所有流量都通过远程过程调用,保证了隔离性和灵活性。
  2. 使用schema:僵化的预定义数据字段以及演进的重量级过程导致了无模式(schemaless)数据管理的出现,比如JSON或对象存储,不需要预先定义结构,但会产生数据完整性和复杂性问题。强类型的面相schema的数据库降低了应用的隐蔽性,也就降低了复杂性,因为数据大多数时候是“一次写入,多次读取”,schema明显使读取更容易。
  3. schema自动化迁移:改变schema是高危操作,DDL很容易导致故障,要用schema管理工具和自动化迁移工具,并做好变更记录的SOP。
  4. 保持schema的兼容性:使用schema兼容性检查来探知不兼容的变更,并使用数据产品来解耦内部和外部schema,比如将应用数据库通过数据管道抽取、加工、转换到数仓。
推荐阅读:
  1. 《演进式架构》(Building Evolutionary Architectures)
  2. 《软件设计的哲学》(A Philosophy of SoftwareDesign)
  3. 《Clojure的要素》(Elements of Clojure)
  4. “简单造就易用”(“Simple MadeEasy”)
  5. 《数据网格:规模化地提供数据驱动的价值》(DataMesh: Delivering Data-Driven Value at Scale)
  6. 《数据密集型应用系统设计》(Designing Data-IntensiveApplications)

第12章 敏捷计划

敏捷宣言:

  1. 个人和互动高于流程和工具
  2. 工作的软件高于详尽的文档
  3. 客户合作高于合同谈判
  4. 响应变化高于遵循计划


敏捷计划的框架:

  1. Scrum:鼓励短期迭代,并经常设有检查点来调整计划。开发工作被分成几个冲刺阶段,在冲刺开始时,每个团队都会安排一场冲刺计划会议来分配工作,这些工作被记录在用户故事或任务池中。规划之后,开发人员便开始工作,工作进展在任务票或问题系统中被跟踪。每天都设有一个简短的站会,以分享最新情况并指出问题。在每个冲刺阶段结束后,团队会进行一次回顾总结,
  2. Kanban:看板定义了工作流程中的各个阶段,所有的工作条目都要经历这些阶段(例如,待着手、计划中、实施中、测试中、部署、展开)。看板将进行中的工作可视化,相当于为每个工作流程阶段设置了垂直列的仪表盘,通过限制每个阶段的任务数量来限制正在进行中的工作,


Scrum框架:

  1. 用户故事:从用户的角度定义了特性的需求,把重点放在提供用户价值上。格式是“作为一名<用户>,我<想><这样>”。
  2. 任务分解:单一的用户故事需要被分解成更小的任务,以预估它需要多长时间才能完成,用来给多名开发人员分配工作,并跟踪实施进度。
  3. 故事点:团队的工作能力是以故事点(小时、天或“复杂性”)来衡量的,一次冲刺迭代的能力以开发人员的数量乘每名开发人员的故事点来计算的。例如,某个有4名工程师的团队,每名工程师有10个故事点,其能力为40点。
  4. 消化积压:积压是指候选的用户故事列表,分流是为了保持它的新鲜度、相关性和优先级。
  5. 冲刺计划:冲刺计划会议由工程团队与产品经理一起,讨论高优先级的用户故事,决定做什么。冲刺最重要的特点是周期短,因此一定要将大型任务分解成小型任务,才能更好理解和预估。
  6. 站会:每天早上安排15分钟的会议,让每个人都了解你的进展,并让团队有机会对任何危及冲刺目标的事情做出反应。
  7. 评审机制:评审发生在某两轮冲刺之间,通常分为两个环节:演示和项目评审。演示是团队成员展示他们在本轮冲刺中取得的进展和结果,评审是对当前冲刺目标的完成情况的评估和总结。
  8. 回顾会:团队聚在一起讨论自上次回顾会以来有哪些进展,哪些不足。会议通常分为3个阶段:分享、确定优先级和解决问题。
  9. 路线图:业产研的管理者需要按季度使用产品路线图对下一个季度的工作方向,人力资源,以及更庞大的项目进行前置规划,路线图也可能在实施过程中不断调整和迭代。
推荐阅读:
  1. 〈敏捷宣言〉背后的原则》(“Principles Behind the Agile Manifesto”)
  2. Atlassian网站的文章


第13章 与管理者合作

管理者是做什么的:

  1. 人:管理者构建团队、指导和培养工程师,并进行人际关系的动态管理。
  2. 产品:管理者计划和协调产品的开发,他们也可能参与产品开发的技术方面如代码评审和技术架构,但通常不需要亲自写代码。
  3. 流程:管理者对团队流程进行迭代,以保持其有效性。
  4. 管理者们通过与高管或董事(“向上”)合作、与其他管理者(“横向”)合作以及与他们的团队(“向下”)合作来“管理”所有的这些事务。

沟通、目标与成长:

  1. 一对一面谈(1∶1):用来讨论关键问题、解决大局观上的偏差,并建立富有成效的长期关系。
  2. 进展、计划与问题(progress-plans- problems,PPP):更新工作状态能帮助你的管理者发现问题、提供信息、协调资源。
  3. 目标和关键结果(OKR):定义目标O,每个O附着3-5个关键结果KR作为衡量标准。按季度设定和评估,与你的管理者对齐。
  4. 绩效考核:每年或每半年一次,职级和薪酬的调整一般也是在绩效考核期间进行的。员工先自评,然后环评,管理者进行回应,最后管理者和员工聚在一起讨论、反馈,在讨论后员工需要确认绩效考核结果。务必保有一份一年的工作清单方便回顾,也要试着把考核看作一次总结和计划的机会。

向上管理:

  1. 接收反馈:绩效考核的频率太低,你需要定期的反馈才能迅速调整,但管理者并不总是主动提供反馈,你可能需要主动通过一对一面谈的方式询问来获得反馈。要知道你的管理者仅仅是视角之一(诚然是很重要的视角),试着把管理者的反馈纳入你的观点,而不是直接采用管理者的反馈。此外对对方的反馈意见也要给予反馈。
  2. 给予反馈:反馈可以关于任何事情:团队、公司、行为、项目、技术计划,甚至是人力资源政策。提出问题,但不要只关注问题,可以使用情况、行为和影响(situation-behavior- impact,SBI)框架。
  3. 讨论你的目标:不要指望你的管理者知道你对自己职业的要求。你需要清楚地阐述你的目标和愿望,以便你的管理者可以帮助你实现这些目标。正式的绩效考核环节是进行这种对话的好时机。
  4. 事情不顺时要采取行动:关系和工作各有波峰、波谷,可能会发生短期的摩擦,没必要频繁采取激烈的行动。然而,如果你感到持续的挫折、压力或不快乐,你应该说出来,可以使用SBI框架与你的导师、管理者、HR交流,以寻求帮助。你也可以要求变化或主动改变。
推荐阅读:
  1. 《技术管理之路:技术领导者应对增长和变化的指南》(The Manager’s Path: A Guide for Tech Leaders Navigating Growthand Change)
  2. 《优雅谜题:工程管理的系统》(An Elegant Puzzle: Systemsof Engineering Management)
  3. 《高难度谈话Ⅱ·感恩反馈》 (Thanks for the Feedback: The Science and Art of Receiving Feedback Well )
  4. 《向上管理:如何晋升,如何在工作中获胜以及如何与不同类型的上司一同获取成功》(Managing Up: How to Move up, Win atWork, and Succeed with Any Type of Boss)
  5. 《格鲁夫给经理人的第一课:英特尔创始人自述》(HighOutput Management)


第14章 职业生涯规划

迈向资深之路:

  1. 职业发展:各家公司的职级数量不尽相同,但通常有两个过渡表明资历发生了重大转变——从初级工程师到资深工程师,以及从资深工程师到主任工程师或首席工程师。
  2. 初级工程师:实现特性和完成任务。
  3. 资深工程师:处理更多的不确定性和模糊性,帮助确定工作内容、应对更大或更关键的项目,并且需要更少的指导。
  4. 主任工程师:承担更广泛的职责,甚至超出了团队本身的范畴,需要对工程战略、季度规划、系统架构做出贡献,并且要确保工程流程的运转和政策的实施。职业发展分化为个人贡献者和管理者两条轨道。

职业生涯建议:

  1. T型人才:软件工程有许多专业领域。前端、后端、运维、数据仓库和机器学习等,“T型”工程师既是能在大多数领域内都能有效工作的通才,并且至少是某一个领域的专家。
  2. 参加工程师训练营:招聘、面试、午餐会、研讨会、聚会、阅读小组、开源项目、学徒和导师计划都是可以参与的机会。
  3. 主导你自己的晋升:理想情况下,你的管理者会在完全正确和公平的时间点对你晋升。但这个世界很少是理想的,所以你可能需要主导你自己的晋升。找到公司的职业发展阶梯,确定下一个职级所需的技能,了解晋升的流程,确保你的工作是有价值的和可见的,进行自我评估和获得他人反馈,与你的管理者进行晋升谈话,回顾和制定计划。
  4. 换工作需谨慎:换工作可以拓展你的技能和人脉,但频繁地换工作会阻碍你的成长,在HR的眼里也很难看,没有充分的理由就不要换工作。
  5. 自我调节:工作可能很忙碌,竞争很激烈,技术发展又很快,而且总是有更多的东西需要学习。你可能会觉得有太多的事情发生得太快了。新工程师的反应往往是更加努力、工作时间更长,但这是造成倦怠的关键因素。休息一下,短暂脱离,不要让自己过度劳累。你的职业生涯是一场马拉松,而不是短跑冲刺——你有几十年的时间。给自己定下节奏,享受这段旅程吧!






来源  |  阿里云开发者公众号
作者  |  以牧


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5月前
|
缓存 安全 搜索推荐
看看我精心整理的 Go 面试干货,面试时候能帮到你
看看我精心整理的 Go 面试干货,面试时候能帮到你
63 1
|
7月前
|
程序员 Python
GitHub爆赞!最适合新手入门的教程——笨方法学Python 3
“Python 是一门既容易上手又强大的编程语言。”这句话本身并无大碍,但需要注意的是,正因为它既好学又好用,所以很多 Python 程序员只用到了其强大功能的一小部分。 今天给小伙伴们分享的这份手册以习题的方式引导读者一步一步学习编程,从简单的打印一直讲到完整项目的实现。
|
数据可视化 项目管理 C++
|
8月前
|
存储 测试技术 开发工具
软件测试/测试开发|GitHub怎么用,这篇文章告诉你
软件测试/测试开发|GitHub怎么用,这篇文章告诉你
|
测试技术 Go 网络安全
听说还不知道这几个 Goland 技巧
很多人使用 Goland 有很长时间的,却没有好好利用上 Goland 工具带给我们的遍历,今天咱们就来解锁一下新技巧
147 0
|
存储 编译器 开发者
Makefile基础教程:从零开始学习
在软件开发过程中,Makefile是一个非常重要的工具,它可以帮助我们自动构建程序,管理程序依赖关系,提高开发效率。本篇博客将从基础开始,介绍Makefile的相关知识,帮助大家快速掌握Makefile的使用方法
155 0
Makefile基础教程:从零开始学习
|
Devops 程序员 开发工具
C++——程序员的逼格神器-github
C++——程序员的逼格神器-github
|
SQL 关系型数据库 Go
postgres与golang点点滴滴 | 青训营笔记
postgres与golang点点滴滴 | 青训营笔记
136 0
|
Unix Linux Shell
Git 与 Linux基本命令 | 青训营笔记
上期简单介绍了Git的历史背景和功能,也简要说明了他的功能。但所谓不学无术,光学不练假把式,今天主要就聚焦于Git安装好之后需要的操作(以Linux版命令行为例)
Git 与 Linux基本命令 | 青训营笔记
|
前端开发 JavaScript 程序员
GitHub 标星 54K + 2K!这才是程序员写逼格满满的 PPT 的正确姿势!
GitHub 标星 54K + 2K!这才是程序员写逼格满满的 PPT 的正确姿势!
643 0