文接关于编码的一切 ——《代码大全》 下

软件质量

在了解最基本的变量和语句组织后,这一部分围绕提高软件质量展开。

概述

软件的外在质量特性体现在:

  • 正确性
  • 可用性
  • 效率
  • 可靠性
  • 健壮性
  • 适应性
  • 完整性
  • 精确性

内在质量特性体现在:

  • 可维护性
  • 可扩展性
  • 可移植性
  • 可读性
  • 可测试性
  • 可理解性

而改善软件质量可以有很多技术:

  • 确定目标
  • 测试策略
  • 非正式走查
  • 正式技术复查
  • 外部审查

开发过程中可以通过量化结果、制作原型、控制变更等手段提升质量。

  • 不同的质量保障手段效率各不相同,多种缺陷检测办法结合、人工和计算机检测结合,效果会比单打独斗要好;然而没有任何一种错误检测办法可以解决全部问题
  • 越早引入软件的问题,修正代价越大,尽早开始代码质量保障工作
  • 需求或架构上的错误相比编码阶段会带来更广泛的影响
  • 改善质量可以降低开发成本

协同构建

协同构建即在同行的帮助下完成代码构建。方式包括但不限于结对编程、正式检查、非正式技术复查、文档阅读等等。结对编程技术性复查可以缩短开发周期,排查出更多错误,减少代码的维护时间。同时,同行间的协同构建也有助于快速提升公司开发者的开发水平。

  • 结对编程
    • 结对编程中,一名程序员敲代码,另外一名注意有没有出现错误,以及考虑策略性问题
    • 成功秘诀
      • 事先确定编程规范,避免琐碎争吵
      • 不编程的成员不能变成旁观者
      • 不需要在简单问题上使用结对编程
      • 灵活对结对成员轮换,让大家熟悉不同系统
      • 避免新手组队
  • 正式检查
    • 一种特殊的复查,与会主持人负责组织事宜,按计划、概述、准备、与会人(非作者)阐述代码、详查报告、跟进等步骤推进
    • 针对代码而非作者
    • 最终由作者负责如何处理缺陷
  • 走查
    • 走查是形式和流程都比较宽松的代码复查方式,时间较短,重点也在检查错误而非修正它们
  • 代码阅读
    • 类似Code Review,关注点主要在代码本身,而非会议
  • 公开演示:类似showcase

开发者测试

测试分单元测试、组件测试、集成测试、回归测试、系统测试,前两部分通常由开发者进行,后三部分由专门的测试人员进行。测试按是否了解测试对象内部工作原理也可以分为黑盒测试和白盒测试。最后要注意,测试 ≠ 调试

对于开发者而言,测试天生有些特别:

  • 测试的目标是找出错误而非实现功能
  • 测试绝不可能彻底证明程序里没有错误
  • 测试无法改善软件质量,它本身只是一个指示器

开发者测试在整个项目时间中大概占8% - 25%,在测试时有一些tips:

  • 写代码前先写测试用例,不会比后写多花功夫
  • 不止进行“干净测试”,也要考虑“肮脏测试”
  • 很容易对测试覆盖率过度乐观

在现实世界中,要穷尽所有可能的输入是不可能的,测试不可能完全,有些方法会起到作用:

  • 使用路径数计算计算用例的最少数目,它可以保证所有代码的执行
    • 通过子程序时,开始记1
    • 遇到ifwhilerepeatforandor或等价物时,+1
    • 遇到每一个case语句,+1;没有缺省分支时,再+1
  • 数据流测试可以覆盖到数据的变化情况
    • 数据有已定义已使用已销毁状态
    • 子程序控制流有已进入已退出状态
    • 测试所有的已定义已定义-已使用组合,注意其他的组合顺序
  • 一个好的测试用例可以覆盖可数据数据的一大部分
  • 用启发式方法去猜测错误
  • 留意边界值,如数组边界的off-by-one错误
  • 考察好数据坏数据
    • 好数据:期望输入、最小正常值、最大正常值、旧数据兼容性
    • 坏数据:没有数据、过多数据、无效数据、长度错误、未初始化
  • 使用容易验证结果的测试数据

关于错误,也有一些先验的规律:

  • 符合八二法则、绝大多数错误通常和几个具有严重缺陷的子程序有关
  • 大多数错误影响范围有限
  • 大多数构建错误来自编程人员的错误,多从自身想问题
  • 笔误是一个常见的问题根源
  • 错误理解需求也是常见原因
  • 大多数的错误都较易修正
  • 业界经验是,平均1000行代码发现1-25个错误,发布产品大概是1000行代码0.5个,
  • 同样留意,测试用例本身是否有误

在当前的编程环境和工作条件下,容易找到很多测试框架,它们会包含脚手架、diff工具、测试数据生成器、覆盖率监测、日志记录、系统干扰器等。另外,自动化测试、测试问题复盘等对测试质量也有提升帮助。

调试

调试(debug)是寻找错误根本原因和纠正错误的过程。它和测试一样,本身不是提升代码质量的方法,而是诊断代码缺陷的手段。

  • 调试の误区
    • 纯靠猜测找到问题所在
    • 不去花时间理解程序和问题
    • 暂时性的掩盖问题
    • 把问题推给硬件,而不思考是不是出在自己身上
  • 寻找缺陷的步骤
    • 让错误状态稳定下来 => 稳定复现
    • 收集相关数据,构造错误原因假说
    • 通过测试或检查代码证实或证伪假说
  • 一个无法稳定重现的问题,可能和初始化或和时间有关系
  • 寻找缺陷的tips
    • 构建缺陷假说时,要能合理解释所有测试用例
    • 定位缺陷困难时,及时补充更多的测试用例复现问题,用多视图的方式盲人摸象定位缺陷
    • 测试用例过于发散时,及时用用例否定一些假说
    • 二分法缩小嫌疑范围
    • 检查最近修改最近出过错的代码
    • 小黄鸭调试法
    • 暂时休息一下
    • 蛮力调试
      • 抛弃有问题的代码,从头设计和编码
      • 抛弃整个程序,从头开始设计和编码
      • 不放过任何一个编译器错误
      • 手动遍历所有的循环
      • 更换编译环境或比那一起
      • 持续自动化测试
      • 显示代码中所有的打印日志信息
    • 给启发式调试法一个deadline
    • 调试中避免心理惯性:人们总期望一个新现象类似他们见过的某种现象
  • 修正缺陷
    • 修正问题前确保已经很好地理解了
    • 理解程序而不仅是问题
    • 验证对错误的分析或理解
    • 保留最初的源代码
    • 治本,而不要治标
    • 一次只改一个地方
    • 搜寻代码中还有没有类似的缺陷
  • 调试工具
    • 源代码diff
    • 编译警告信息
    • lint和代码自动修正
    • 性能剖测(profile)
    • 测试脚手架
    • 调试器

重构

更多重构介绍可以参考这篇blog重构 —— 代码的实用性与艺术性

重构即在不改变软件外部行为的基础上,改变其内部结构。即便是管理完善的项目,每个月都会发生需求变化,稳定不变的需求是个童话。

代码出现以下“坏味道”(smell)时,代表需要重构了。

  • 代码重复
  • 子程序冗长
  • 嵌套过深
  • 内聚性差
  • 参数列表过长
  • 类和继承关系不合理
  • 基本数据类型过多
  • “流浪数据”传递
  • 无所事事的类
  • 命名不当
  • 难理解的注释
  • 全局变量
  • 子程序需要前置或后置处理
  • 过早设计或过度设计

重构分级别有下面一些手段

  • 数据级
    • 具名常量
    • 更可读的变量
    • 函数替代表达式
    • 中间变量
    • 减少重复使用变量
    • 类型码转成类或枚举类型
    • 类封装
  • 语句级
    • 分解布尔表达式
    • 用可读名字的布尔函数替代布尔表达式
    • 合并条件语句中的重复代码片段
    • breakreturn替代循环控制变量
    • 多态替换条件语句
    • null对象替代空值检测
  • 子程序
    • 内莲花
    • 提炼子程序
    • 转化为类
    • 增/删参数
    • 合并/拆分子程序
    • 读写操作分离
    • 传递成员/类
  • 类实现
    • 值/引用对象转化
    • 成员函数/成员数据位置移动
    • 相似代码提炼到基类
    • 差异代码拆分到派生类
  • 类接口
    • 类拆分/合并
    • 删除类
    • 去掉中间人
    • 继承替代委托
    • 委托替代继承
    • 引入外部成员函数
    • 引入扩展类
    • 封装不使用的成员函数
  • 系统级
    • 为无法控制的数据创建索引源
    • 工厂模式
    • 异常/错误处理代码选型

要想让重构不影响日常功能开发,需要考虑

  • 有一个代码版本管理工具
  • 重构步伐小一点
  • 同一时间只做一个重构
  • 重新测试
  • 增加测试用例
  • 检查代码更改
  • 根据重构风险选择重构方法
  • 不要把重构当成糟糕设计的挡箭牌
  • 避免用重构代替重写

重构可以在修改代码的时候进行,不论是增加修改子程序还是类,或者是修复缺陷。对于从未重构的糟糕代码,可以用一部分混乱的代码隔离复杂度,把理想规整的代码和混乱不堪的真实世界隔离开。

代码调整策略

代码调整指出于性能考虑,对代码进行实现上的调整。本章主要讨论策略。

  • 动手前的考虑
    • 性能≠代码速度
    • 想清楚你确实在解决一个需要解决的问题
  • 调整考量
    • 程序设计:设计架构时考虑整体性能,再为每个子系统、类设计要达到的资源占用目标
    • 和操作系统的交互
    • 代码编译
    • 硬件
    • 类和子程序设计
    • 代码调整
  • 帕累托法则,又称八二法则,程序中20%的子程序耗费了80%的执行时间
  • 一些错误认知
    • 减少代码行数就可以减少资源占用
    • 特定的写法会比其他的更快,代码也会更小(要看编译环境
    • 应当随时随地优化(不成熟的优化不如不优化
    • 运行速度和正确性同样重要
  • 先提升代码可维护性,在程序完成且表现正确后,再去提升系统性能
  • 常见的低效率来源
    • IO操作
    • 内存分页
    • 系统API调用
    • 脚本语言
  • 性能测量:没有准确的性能测量就不要去做优化
  • 代码调整需要反复尝试,才能达到理想的性能提高

代码调整技术

文接上章,本章讨论具体调整手段。

  • 代码调整和重构相反,大多数情况下是以牺牲程序可读性为代价换取更高的性能
  • 调整手段
    • 逻辑
      • 知道答案后停止判断
      • 按照出现频率调整判断顺序,把容易为真的判断放在最前面
      • 表驱动法代替复杂表达式
      • 惰性求值
    • 循环
      • 把判断提出循环体
      • 展开小循环
      • 合并循环
      • 减少循环体内的操作
      • 用哨兵值提前结束循环
      • 把最忙的循环放在最内侧
      • 用低强度的计算代替高强度计算,如加法替代乘法,乘法代替幂运算
    • 数据
      • 使用整型而非浮点数
      • 减少数组维度
      • 使用辅助索引
      • 使用缓存
    • 表达式
      • 利用恒等式简化代码复杂度
      • 削弱计算强度
      • 编译期初始化
      • 小心系统函数,为了兼容最糟情况,系统函数会比较复杂
      • 事先算出结果
      • 删除公共表达式
    • 子程序
      • 子程序改为内联
    • 用低级语言重写
  • 再次强调,没有性能优化测量就没有代码调整

系统考虑

这一部分站在系统的角度考虑构建过程中的程序规模、集成、工具等问题。

程序规模带来的影响

软件规模的扩大可能会为你带来意料之外的大量问题。

  • 如果你习惯于开发小项目,那么你的第一个大型项目很可能会严重失控
  • 沟通交流:项目成员的扩大带来的交流路径不是加性的,是乘性
  • 错误:项目规模的扩大也会带来更高的缺陷密度
  • 生产率:大项目会带来更低的生产率
  • 工作量:软件构建的工作量和项目大小是线性关系,而其他活动的工作量则是非线性增加
  • 不同规模的代码从小到大可以称作程序、产品、系统、系统产品,没能意识到它们间的不同也是估算偏差的出现来源
  • 项目越正规,就越重视方法论,不得不写的文档也会更多,撰写的文档也会更正规

构建管理

构建管理是软件管理中的一部分。

  • 鼓励良好的编程实践
    • 逐行复查
    • 代码签名
    • 鼓励最佳实践
  • 配置管理:系统化定义项目工件和处理流程
    • 需求和设计变更
      • 遵循系统化的变更手续
      • 成组变更
      • 评估变更成本
      • 坏味道:频繁大量变更
    • 软件变更:版本控制软件
    • 机器配置变更:机器镜像
  • 评估构建进度表
    • 评估项目规模和工作量是软件项目管理中最具挑战性的部分,平均水平的大型软件都要超时1年,超预算100%才能完成
    • 清楚说明软件需求
    • 使用不同方法评估再对比
    • 定期更新评估
    • 以下因素会影响软件开发进度,但不易被量化
      • 开发者的经验和能力
      • 团队的动力
      • 管理质量
      • 可重用的代码数目
      • 人员流动性
      • 需求变更
      • 文档量
      • 分级安全环境
    • 如果进度落后了要怎么办
      • 扩充团队在项目任务不能分割并一一击破时,只会增加项目交流复杂度,并加速项目延期
      • 缩减项目范围,有限保证核心功能
  • 度量:对项目特征进行度量可以评估项目进度和风险,当然保证ddl比收集度量数据更重要
  • 把程序员当人看
    • 程序员1天大概有30%时间花费在“对项目没有直接好处”的非技术活动中
    • 不同程序员间的努力和能力差异很大,不同团队在软件质量和生产率的差异上也很大,好的和坏的程序员都倾向于聚集在一起
    • 在代码风格的信仰问题上,使用“建议”或大多数人达成统一的指导规范
    • 优良的物理工作环境有助于提升程序员的生产率和生产质量
  • 管理你的管理者:向上管理

集成

集成方式也会影响到集成的质量。从频率上分,有阶段式集成增量集成,在阶段式集成中,分为单元开发和系统集成两个阶段。增量集成中,程序是一点一点写出来并一点点拼接起来的。对比阶段式集成,增量集成有下面一些好处:

  • 易于定位错误
  • 更早在项目中取得系统级结果
  • 对项目结果更好的监控
  • 能在更短的开发计划中建造出整个系统

而增量集成有下面一些常见策略:

  • 自顶向下集成:先集成系统设计,再集成具体实现;优点是能更早有整个系统的大局观,缺点是在前期会加入很多底层的mock代码,且将调试过程推迟到项目后期
  • 自底向上集成:和自顶向下相反,优点是很容易定位错误,缺点是丧失全局的认识,系统设计的问题在后期修改成本较高
  • 三明治集成:先集成顶部的高层业务对象和底部的基础工具类,再集成中间层代码,整合了自顶向下和自底向上
  • 风险导向的集成:鉴别不同类的风险级别,先集成风险高的
  • 功能导向的集成:一次集成一组功能,它基本上不需要脚手架,且每次集成都能增强系统的功能性,且和面向对象设计比较好协同工作
  • T型集成:在集成高层对象后,先选中某个特定功能块,完成一次所有类的集成,这样可以作为MVP演练整个系统

结合冒烟测试的“每日构建”(daily build)是软件集成的一种最佳实践。它能让产品每天都有进步,且让项目保持一个固定的脉搏。做好daily build,有下面一些建议

  • 不放过失败的build,保证每次build都能通过冒烟测试
  • 每天进行冒烟测试
  • 冒烟测试需要和代码一样“与时俱进”
  • 让daily build和冒烟测试自动化
  • 要求开发人员构建前进行冒烟测试
  • 将修订保持合适的合并节奏,不要太密,也不要太疏
  • 在早上发布build,给潜在问题留下修复时间
  • 顶住需求压力,保证daily build和冒烟测试

在daily build的基础上,可以很轻易地做到1日多次的持续集成

编程工具

工欲善其事,必先利其器

现代化的编程环境下,有很多可以采用的编程工具:

  • 设计工具
  • 源代码工具
    • IDE
    • 文本替换工具
    • diff工具
    • merge工具
    • 源代码美化器
    • 接口文档生成
    • 代码模板/代码生成
    • 命令行
    • 代码质量分析
      • linter
      • metrics报告
    • 重构代码
      • 重构器
      • 代码翻译器
    • 版本控制工具
    • 数据字典
  • 可执行码工具
    • 目标码生成
      • 编译器、链接器
      • build工具,如make、ant
    • 程序库/第三方库
    • 代码生成向导
    • 安装指引
  • 调试
  • 测试
  • 代码调整
    • 性能剖测
    • 汇编和反汇编

在Unix这样的工具导向环境下就更容易孕育编程工具,如grep、diff、sort、make、tar、line、sed、awk、vi等。几乎所有的大型组织都有自己的内部工具和支持团队,不少比市面上的还要优秀。针对特定项目,有时候也会开发特定的项目工具,如航天、保险、医疗等。对于个人开发,也可以使用脚本这种自动执行重复性杂务的工具。

最后要澄清一个事实,编程工具并不能消灭人在编程里的核心地位,只是不断重塑(reshape)编程的含义。连接到其他软硬件的复杂接口,规章制度、业务规则这些计算机编程之外的复杂之源还是要人来应对。而被用来填补真实世界和解决问题的计算机之间鸿沟的人,被称作程序员。

软件工艺

编程是硬件与艺术的融合体,软件工艺是编程美学的一种体现。

布局与风格

编排出色的代码会带来视觉上和思维上的愉悦。

  • 基本原则
    • 好的布局可以凸显程序的逻辑结构,也更符合人类直觉
    • 傻子都会写计算机理解的代码,而优秀程序员写的是人能看懂的代码
    • 高手的机型并非天生优于新手,而是高手具备某种知识结构,这种结构有助于高手记住特定类型的信息;因此当信息符合这些结构时,就可以被轻易的理解
    • 布局非信仰,要保持头脑开放,接受已被证实更好的方法
  • 布局技术
    • 空白
      • 分组
      • 空行
      • 缩进
    • 括号
  • 布局风格:同一层级的语句缩进相同
    • 纯块结构
    • 模仿块结构
    • 花括号指定边界
    • 行尾布局(不推荐)
  • 控制结构布局
    • 段落间的空行
    • 复杂的表达式拆分条件到多行
    • 不用goto
  • 单行语句布局
    • 控制长度
    • 使用空格
    • 后续行缩进统一
    • 后续行结尾统一
    • 一行一条语句
      • 减少复杂度
      • 读代码仅需自上而下
      • 不要在单行中多个操作
    • 一行一个声明
  • 注释风格
    • 缩进和代码一致
    • 用空行和代码隔开
  • 子程序布局
    • 空行分段
    • 参数按标准缩进
  • 类布局
    • 头部注释 -> 构造函数/析构函数 -> public子程序 -> protected子程序 -> private子程序和成员
    • 文件布局
      • 一个文件一个类
      • 文件命名和类有关
      • 在文件中清晰分隔各子程序

自说明代码

本节专注于文档的特殊补充形式,即“注释”。

  • 在代码中起主要作用的并非注释,而是好的编程风格
  • 注释的哲学
    • 注释能提供更高层级的抽象
    • 重复注释根本没用
    • 注释写的不合适只会起反作用
  • 注释类别:在代码完工后,只允许出现后三种
    • 重复代码(Bad case)
    • 解释代码:当代码过于复杂到需要解释时,最好是改进代码,而不是添加注释
    • 代码标记,如TODOFIXME
    • 概述代码
    • 意图说明
    • 传达代码以外的信息,如版权声明、保密要求
  • 高效注释
    • 用伪代码法减少注释时间
    • 将注释如何到开发风格中
  • 注释技术
    • 注释单行
      • 去掉无关注是
      • 减少行尾注释
      • 行尾注释只用于数据声明、维护标记、标记行尾等场景
    • 注释代码段
      • 应表达why而非how
      • 代码本身应尽力组做好说明
      • 注明非常规用法
      • 错误或语言环境独特点要加注释
    • 注释数据声明
      • 数值单位
      • 允许范围
      • 输入限制
      • 全局数据
    • 注释控制结构
      • 循环结束的行尾注释是代码太复杂的征兆
    • 注释子程序
      • 注释要靠近说明的代码
      • 用简短的话进行说明
      • 注释声明参数
      • 可以使用Javadoc这种工具
      • 说明子程序的全局作用
    • 注释类、文件、程序

个人性格

软件工程是纯粹的脑力劳动。软件工程师研究工具和原材料的本质时,实际上是在研究人的智力、性格这种无形的东西。

  • 编程工作本质上是项难以监督的工作,你也需要对自己负责
  • 聪明和谦虚
  • 求知欲
    • 形成自我意识
    • 实验
    • 学习成功项目
    • 阅读文档
    • 和同行交流
  • 不屈不挠的诚实感
  • 交流和合作:编程首先是与人交流,然后才是和计算机交流
  • 创造力和纪律
  • 懒惰:避免“实在懒”,追求“开明懒”和“一劳永逸的懒”
  • 可能不那么明显的性格
    • 坚持:要时不时抬头开清方向
    • 经验:不同于其他行业,软件开发行业的经验比书本知识价值要小,基础知识变化很快,不存在越老越吃香的情况。不持续学习跟上潮流,仅靠经验吃饭,会被逐渐淘汰。
  • 习惯
    • 好习惯很重要
    • 不要用“没有习惯”替代“坏习惯”

软件工艺探讨的话题

《代码大全》全书都着重于软件构建的细节,本章从抽象的关注点出发,看看哪些方面会影响软件的工艺。

  • 软件开发的核心是致力于降低复杂度,管理复杂度是软件的核心使命,之前各章节提过了很多具体办法。各种形式的抽象都是管理复杂度的强大工具。
    • 划分子系统
    • 仔细定义类接口
    • 保持接口抽象性
    • 避免全局变量
    • 避免深层次继承
    • 避免深度嵌套和循环
    • 不用goto
    • 子程序短小精悍
    • 使用清晰明了的变量名
    • 使用规范和约定减少理解负担
  • 软件开发和其过程密不可分,在多程序员参与的项目里,组织性的重要性超过了个人技能
    • 坏的过程只会损耗脑力,好的过程则可以开发脑力到极限
  • 首先为人写程序,然后才是机器,强调代码可读性,便于与同行沟通
  • 深入一门语言去编程,不浮于表面
    • 杰出的程序员会考虑他们要干什么,然后才是怎么用手头的工具实现目标
  • 借助规范集中注意力
  • 基于问题域编程
    • 将程序划分为不同层级的抽象
      • 第0层:操作系统的操作和机器指令
      • 第1层:编程语言结构和工具
      • 第2层:底层实现结构,如算法和数据结构
      • 第3层:低层问题域,这一层已经有问题域相关的操作原语可以使用
      • 第4层:高层问题域,你的非技术用户某种程度也应该可以看懂你的代码
  • 编程是科学和艺术融合的一门工程学科
  • 迭代在软件开发中是很正常的现象。软件设计是一个逐步精化的过程。
  • 将软件和信仰分离开
    • 不要盲目跟风
    • 保持折中态度
    • 权衡各种技术,再做决定
    • 基于实验,保持开放心态

–END–

拿到它的时候,它已印刷了13年之久;能在京东上找到,也是极大的幸运。

《代码大全》是一部又大又全的工具书,它涵盖了关于编程各个环节的充分经验,可以作为日程编程工作的指导。将其他教我如何编程的书中的内容做了一个打包。数月研读下来,难免有所遗忘。这里将其中的精华尽量记录下来,也作为对全书内容的一个回顾。

打好基础

第1部分主要围绕构建为读者讲解什么是构建,和关于构建我们需要了解和准备的。也为后面展开具体编程细节和设计艺术打基础。

走进软件构建

构建是软件开发的核心,构建的质量对软件质量有实质影响。

隐喻理解软件开发

用隐喻可以帮助理解软件开发的过程。

  • 一个好的隐喻应该是简单的,忽略了不必要的细节,对概念进行内化和抽象,让人从更高层面思考问题,从而避免低层次错误
  • 隐喻更像启示,而非算法
  • 对于编程来说,还是将问题概念化
  • 有一些常见的软件隐喻
    • 写作/耕作:这些隐喻不太合适
    • 养殖:增量、迭代、自适应、演进的成长概念
    • 建造:规划设计文档,使用现成组件
  • 组合各种隐喻,不要过度引申隐喻,带来误导

提前准备

提前准备,降低风险。

  • 前期准备的必要性
    • 降低风险
    • 通过逻辑、类比、数据说服项目经理
      • 开始大项目前需要制定计划
      • 程序员是食物链的最后一环,架构师吃掉需求,设计师吃掉架构,程序员消化设计
      • 发现错误的时间要尽可能接近引入错误的时间,可以尽量降低修复时间
  • 判别你所在的软件领域
    • 在软件开发中,适用迭代式开发法比适用序列式开发法的情况多得多
  • 先清楚定义问题
  • 再正确认清需求
    • 正式详尽地描述需求,是项目成功的关键
      • 面向目标、契约式编程
    • 稳定需求是可望而不可即的
      • 开发过程会帮助客户更好地理解自己的需求,这也是需求变更的主要来源
    • 应对需求变更
      • 核对当前需求的质量(需要有一个需求质量的核对表),及时回退到需求设计环节
      • 确保每个人都知道变更的代价
      • 建立变更的控制流程
      • 要放弃么?
      • 考虑项目的商业价值
  • 考虑架构设计
    • 架构指整个系统的设计约束,不会细节到子系统或类的设计约束
    • 架构的组成部分
      • 程序组织
      • 主要的类和类的继承体系
      • 数据结构设计
      • 业务规则描述
      • UI设计
      • 资源管理:数据库连接、线程、句柄
      • 安全
      • 性能
      • 可扩展性
      • 国际化
      • 错误处理:纠正还是检测、主动还是被动
      • 输入输出
      • 容错性
      • 过度工程:明确设立期望目标
      • “买”还是“造”:如果架构选择自己做,那么一定要证明自己定制的组件在某方面胜过现有的
      • 变更策略:如何应对变更
    • 架构的总体质量
      • 和所解决的问题和谐一致,看起来自然
      • 描述所有主要的决策动机
      • 优秀的架构很大程度和机器与编程语言无关
  • 投入的时间一般在20%-30%

关键的构建决策

选择语言、技术、构建实践。

  • 高级语言表达力更强
    • 你思考的能力取决于你是否知道可以表达该思想的词汇
  • 提前讲好使用的编程约定,去统一编程语言的细节
  • 找准在技术浪潮中的位置
    • 如果在浪潮后期,就可以持续使用稳定的功能;在浪潮前期,则需要花时间找到文档中没有说明的编程语言特性
    • “深入一种语言去编程”,程序员现决定想表达的思想是什么,再决定如何使用特定语言的工具去表达这些思想
  • 选择构建实践

创建高质量代码

这一部分主要讲解类和子程序的设计和编码。

如何做设计

一些启发式准则和idea

  • 设计的挑战
    • 有的问题需要“解决”一边,才能明确定义它,然后再次解决
    • 设计成功应该是组织良好且清爽的,不过设计过程却并非如此
    • 设计需要取舍,受到限制
    • 设计需要启发式思维,但也是不断评估、讨论、调试实验中诞生的
  • 设计的关键概念
    • 管理复杂度
      • 软件开发的本质复杂性来自复杂无序的现实世界,精确完整地识别依赖关系和意外情况,设计完全正确而不是部分正确的方案
      • 软件需要管理复杂度,在组织程序的时候便于在一个时刻专注于一个特定的部分,另外不遗漏暂时忽视的部分
      • 应对复杂度:减少本质复杂度到最小、避免偶然复杂度的无谓增长
    • 理想的设计特征
      • 最小复杂度
      • 易于维护
      • 松耦合
      • 可扩展、可重用
      • 高扇入(类被其他类大量使用)、低扇出(类少量使用其他类)
      • 可移植性
      • 精简性
      • 标准化
    • 设计的层次
      • 软件系统 > 子系统或包 > 类 > 子程序 > 子程序内
      • 常用子系统:业务规则、用户界面、数据库访问、OS抽象层
  • 设计构造块:启发式方法
    • 寻找现实对象:想想系统要模仿什么
      • 辨识对象和其属性
      • 确定可以对对象做的操作
      • 确定对象能对其他对象进行的操作
      • 确定对象的可见范围
      • 定义对象接口
    • 形成一致的抽象:让你关注某概念的时候忽略不必要的细节
    • 封装实现细节:封装帮你掩盖不需要你看到的复杂度
    • 继承能简化设计就继承
    • 隐藏秘密信息
      • 保证接口最小且完备
      • 隐藏复杂度和变化源
    • 找出容易改变的区域
      • 业务规则、硬件依赖、输入输出、非标准的预演特性、状态变量、糟糕或复杂的设计
      • 将容易变化的部分隔离开,让变化的影响范围和变化的可能性成反比
    • 保持松散耦合
      • 耦合种类:简单数据参数、简单对象、对象参数、语义耦合(过多假设)
    • 了解常用的设计模式
      • 设计模式提供了现成的抽象来减少复杂度
      • 设计模式将抽象SOP化
      • 设计模式可以起到启发性作用
      • 设计模式将设计对话提高到更高层次来简化交流
    • 其他启发式方法
      • 高内聚
      • 契约式设计
      • TDD
      • 创建中央控制点,集中管控
      • 拿不准时,使用蛮力突破
      • 画一个图
      • 设计模块化
    • 使用启发式方法的原则
      • 先理解问题
      • 找出现有数据和未知量之间的联系
      • 寻找之前的类似问题,或者解决一些相关问题
      • 执行计划
      • 回顾解
  • 设计实践
    • 迭代:第二个尝试往往会好于第一个
    • 分而治之,增量式改进
    • 自上而下设计和自下而上设计
    • 建立试验性原型:原型要足够简单可抛弃,又足以验证效果
    • 记录你的设计成果:wiki、邮件、UML图

设计类

  • 类是一组数据和子程序的聚合,有内聚的明确定义的职责
  • 抽象数据类型(ADT)
    • ADT可以让你像现实世界一样操作实体,而不必在底层实现上摆弄实体
    • ADT的好处
      • 隐藏实现细节
      • 改动不需要影响整个程序
      • 接口语义更强
      • 更容易提高性能
    • 在非面向对象环境,也可以使用ADT
  • 良好的类接口
    • 好的抽象
      • 类接口应该有一致的抽象层次
      • 要理解类的抽象是什么
      • 考虑提供成对的服务,如打开/关闭、添加/删除
      • 尽可能让接口可编程,而不仅是表达语义
      • 谨防在修改时破坏接口抽象
      • 同时考虑抽象性和内聚性
    • 好的封装
      • 封装比抽象更强,它直接阻止你看到细节
      • 尽可能限制类和成员的可访问性
      • 不要公开暴露成员数据
      • 不要将实现细节暴露在接口上
      • 不要对类的使用者做任何假设
      • 让阅读代码比编写更方便,代码的阅读次数比编写多得多
      • 不要透过接口来编程,仅仅看类的接口文档无法得知如何使用一个类的话,正确的做法不是拉出类的源代码,查看内部实现,而是联系类作者。对于类作者来讲,正确的做法不是面对面告诉答案,而是去修改类的接口文档
  • 设计和实现
    • 通过包含来实现“有一个”的关系
      • 警惕超过7个成员的类
    • 通过继承实现“是一个”的关系
      • 用public继承
      • 要么使用继承并详细说明,要么就不要使用它
      • 遵循Liskov替换原则,即对基类的子程序,在它的所有派生类上含义都应该是相同的,在调用时只用看基类无需考虑是哪一个派生类
      • 只继承需要继承的部分
      • 只有一个实例的类值得怀疑
      • 只有一个派生类的类也值得怀疑
      • 派生中覆盖了某个子程序,但是其中没做任何操作,也值得怀疑
        • 很可能修改了基类接口的语义,慢慢地从基类接口很难理解派生类上的行为
      • 避免过深地继承:降低复杂度
      • 尽量使用多态,避免类型检查
      • 适度使用继承
        • 多个类共享数据而非行为 => 创建类包含的公用对象
        • 多个类共享行为而非数据 => 都从基类派生,在基类中定义公用的子程序
        • 多个类既公用数据也公用行为 => 都从基类派生,在基类中定义公用的子程序和数据
        • 通过基类控制接口 => 继承
        • 自己控制接口 => 包含
    • 成员函数和数据成员
      • 减少子程序
      • 进制不必要的成员和运算符
      • 减少对其他类子程序的间接调用
    • 构造函数
      • 尽可能早构造函数中初始化所有数据成员
      • 用私有构造函数来实现单例数据
      • 优先使用深拷贝,除非需要,才使用浅拷贝
  • 创建类的原因
    • 为现实/抽象世界的对象建模
    • 降低/隔离复杂度
    • 隐藏实现细节
    • 限制变动的影响范围
    • 建立中心控制点
    • 将相关操作包装在一起
    • 避免的类:万能类、无关紧要类、动词命名类
  • 超越类:包

设计子程序

  • 子程序是为了实现特定目的编写的方法或过程
  • 编写子程序的正当理由
    • 降低复杂度
    • 引入中间、易懂的抽象
    • 避免代码重复
    • 支持派生类覆盖
    • 隐藏指针操作
    • 改善性能
    • 增加可读性
  • 子程序上的设计
    • 一个子程序只做一件事
    • 考虑靠近纯函数或纯副作用函数
    • 内聚性
      • 功能上的、顺序上的、通信上的
      • 避免临时的内聚性(只是需要同时执行才放在一起操作的子程序),如贫血的startup()方法
      • 避免逻辑上的、巧合的内聚性
  • 起个好名字
    • 描述所做的事情,而非做事情的过程
    • 避免使用模糊的动词
    • 不要仅用数字区分子程序名
    • 函数名不要过长
    • 考虑描述返回值
    • 使用预期强烈的动词 + 宾语
    • 使用对仗词,如add/remove
    • 为常用操作统一命名
  • 子程序长度:最好少于100行,可以接受100 - 200行
  • 子程序入参
    • 按输入、修改、输出顺序排列参数
    • 如果子程序使用了相似的参数,考虑让他们的排列顺序一致
    • 删掉没有使用到的参数
    • 不要给入参重新赋值
    • 限制入参数(有的说3个,有的说7个)
    • 考虑给参数名增加前缀、后缀
    • 入参和子程序需要在一个抽象层级下
    • 使用具名参数
  • 宏子程序和内联子程序
    • 将宏表达式整个包含在括号内
    • 一般来讲,是不会用宏代替子程序的
    • 节制使用inline子程序,在确认有性能改进后再使用inline子程序

防御式编程

防御式编程让错误更容易发现和修改,并减小破坏。

  • 断言
    • 主要用于开发和维护的阶段
    • 用错误处理代码处理预期中的状况,用断言处理绝不该出现的状况
    • 避免把需要执行的代码放在断言中
    • 对于高健壮性代码,应该先断言再处理错误
  • 错误处理技术
    • 返回中立值,如空串、0
    • 换用下一个正确数据,如获取温度
    • 返回上一个正确数据,如屏幕重绘
    • 使用最接近的合法值
    • 打印警告信息到日志文件中
    • 返回错误码
    • 显示出错信息
    • 关闭程序
    • 平衡正确性和健壮性
  • 异常
    • 通知程序其他部分,发生了不可忽略的错误
    • 只在真正例外的情况下才抛出异常
    • 避免再构造函数或析构函数中抛出异常
    • 在恰当抽象层次抛出异常
    • 在异常信息中加上导致异常的所有信息
    • 避免空的catch语句
    • 异常标准化 & 异常报告机制
  • 辅助调试的代码
    • 进攻式编程:让问题更早暴露
    • 方便地移除调试代码
  • 保留防御式代码的程度
    • 保留检查重要错误的代码
    • 去掉检查细微错误的代码
    • 保留让程序稳妥崩溃的代码
    • 记录错误信息

伪代码编写

  • 创建一个类
    • 创建类的总体设计
    • 创建类中的子程序
    • 复审并测试
  • 伪代码
    • 使用类似英语的用法描述准确操作
    • 避免使用特定编程语言的语法元素,防止陷入到代码本身的层级上设计
    • 在略高于代码的层次上进行设计
  • 通过伪代码创建子程序
    • 检查先决条件
    • 定义子程序要解决的问题
    • 决定如何测试
    • 在第三方库中搜寻可用功能
    • 考虑错误处理
    • 编写伪代码
    • 将伪代码转为高层次的注释
    • 在注释下填充代码
    • 检查代码是否需要进一步分解
    • 使用lint或编译器检查错误
    • 去掉冗余注释

变量

这一部分深入到代码细节,围绕如何正确使用变量展开。

一般事项

  • 初始化的一些建议
    • 声明的时候初始化
    • 靠近变量使用的时候初始化
    • 考虑对常量使用finalconst
    • 注意累加器和计数器的重置
    • 用可执行代码初始化
    • 检查合法性
  • 作用域
    • 将变量引用点集中起来可以提高可读性,这样可以减少大脑缓存
    • 减少变量的“存活时间”(从第一条引用语句到最后一条)
    • 减少作用域的一些原则
      • 循环开始时初始化循环变量
      • 变量使用前再赋值
      • 对于变量先采用最严格的可见性,再逐渐放宽
  • 持续性
    • 子程序内/手动回收前/程序运行时/持久存储
    • 为变量选择合适的持续性
  • 绑定时间
    • 编码时/编译时/加载时/实例化时/运行时
    • 越晚绑定越有灵活
    • 选择合适的灵活度
  • 和控制结构匹配的数据结构
    • 序列型数据 -> 顺序语句
    • 选择型数据 -> if case语句
    • 迭代型数据 -> 循环语句
  • 变量单一用途
    • 只用作一件事
    • 使用所有已声明变量

取名

取名是个学问。

  • 几个原则
    • 信达雅
    • 以问题为导向,面向目的而不是手段
    • 适当的长度,小于20个字符,大于8个字符
    • 作用域越小,变量名越短;使用较少的变量或全局变量适用较长的名字
    • 使用限定词(如min、avg、max)和对仗词
  • 特定类型的变量名
    • 循环下标:i,j,k,在嵌套循环时建议使用表意的变量名
    • 状态变量:取个比xxxflag更好的名字
    • 临时变量:避免用临时名字
    • 布尔变量:名字要蕴含真假的意义
      • done/error/found/success
      • 不建议使用is前缀
      • 使用表示肯定的名字,理解成本低
    • 枚举变量:缺少组前缀的需要加上前缀
    • 常量:不使用magic number或magic string
  • 组内需要确定一个命名规范
  • 标准前缀
    • 用户自定义类型缩写,UDT缩写
    • 正交化、便于检索
  • 如何缩写
    • 使用标准缩写
    • 去掉非前置元音
    • 去掉虚词,and
    • 去掉无用后缀
    • 使用每个单词的第一或前几个字母
    • 不提倡语音缩写
    • 缩写要能读出来
    • 避免容易看错或读错的字符组合
  • 应该避免的名字
    • 令人误解
    • 具有不同含义但有相似名字
    • 发音相近
    • 出现数字,这是不好的征兆
    • 拼写错误
    • 仅靠大小写区分
    • 使用易混淆的字符,如0o1l
  • 代码阅读次数要远远多于编写次数

基本数据类型

  • 数值
    • 避免magic number
    • 避免除0
    • 避免混合类型比较哦
  • 整数
    • 检查整数除法
    • 检查整数溢出
  • 浮点数
    • 避免数据级相差巨大的数之间的加减
    • 避免相等比较
    • 避免舍入误差
  • 字符串
    • 避免magic string
    • 考虑国际化
    • unicode支持
    • C语言的字符串
      • 注意字符串指针和字符数组的差异
      • 注意字符串长度声明为CONSTANT + 1
      • null初始化避免无结束符
      • 建议使用字符数组
  • 布尔变量
    • 使用布尔中间变量简化复杂判断
  • 枚举类型
    • 带来类型提示和提升可读性
    • 简化修改
    • 作为布尔变量的可扩展性方案
    • 枚举类型的第一个元素留作非法制
  • 具名常量:“参数化”程序
    • 统一使用
  • 数组
    • 确认数组下标
    • 顺序访问元素,不建议随机访问
    • 数组边界点
  • 自定义类型:typedef作为类的轻量级方案

不常见的数据类型

  • 结构体:数据组合,没有行为的类
    • 用前一问:可以用类么
    • 简化数据块操作
    • 简化参数列表
  • 指针:灵活但容易出错
    • 用前一问:有访问器子程序或防御式编程么
    • 标识内存中某个位置某种内容
    • 一般技巧
      • 同时声明和定义
      • 使用前检查
      • 使用前判断内存是否损毁
      • 在提高代码清晰度上,不要节约使用指针
      • 简化指针表达式
      • 正确删除链表中的指针
      • 删除或释放前设为空值
      • 删除前检查是否非法
      • 统一跟踪分配情况
      • 统一在子程序里,集中实现上述策略
    • C++指针
      • 理解指针和引用
      • 指针用于“按引用传递”,const引用用于“按值传递”
      • 使用shared_ptr
    • C指针
      • 使用显式类型
      • 避免强制类型转换
      • 遵循参数传递的*规则
      • 内存分配时使用sizeof()确定变量大小
  • 全局数据:风险较大
    • 用前一问:有更好的方法么
    • 常见问题
      • 多线程重入问题
      • 阻碍代码重用
      • 破坏模块化和智力上的可管理性
    • 使用理由
      • 简化极常用的数据使用
      • 消除流浪数据(调用链中间的子程序不使用数据)
    • 用访问器子程序取代全局数据
      • 在访问前锁定控制
      • 在访问器子程序里构建一个抽象层
      • 对数据的所有访问限制在一个抽象层

语句

在了解了数据视角的变量元素后,这一部分围绕语句组织展开。

直线型代码

  • 直线型代码即按先后顺序放置语句和语句块
  • 必须明确先后顺序的语句
    • 想办法明确展示语句的依赖关系
      • 组织代码
      • 使用子程序名/子程序参数凸显依赖
      • 使用注释
      • 通过断言或错误处理来检查
  • 顺序无关的语句
    • 使代码易于从上向下阅读,避免跳来跳去
    • 将相关语句组织在一起

条件语句

  • if语句
    • if-then语句
      • 先写正常代码,再写不常见情况
      • 不要在if后跟随空子句
      • 看看是不是不需要else子句
    • if-then-else语句
      • 利用布尔函数简化复杂的检测
      • 把常见情况放在最前面
      • 检查是否考虑了所有情况
  • case语句
    • 选择最有效的排列顺序,如执行频率
    • 简化每种case下的操作
    • 最好能搭配枚举类型一起使用
    • 使用default子句检查默认情况或错误
    • 注意有些语言的case会有fallthrough,需要加break
  • 循环语句
    • 分为计数循环、连续求值循环、无限循环、迭代器循环。分别适用forwhileforeach语句
    • 循环控制
      • 应该把循环体看作黑盒子,外围程序只知道它的控制条件
      • 合理判断使用forwhile的地方
      • 尽量避免空循环
      • 循环内务(包括索引增加)要么放在循环开始,要么放在循环最后
      • 让循环终止条件看起来明显
      • 不要为了终止循环改动for循环的下标
      • 小心散布了很多break的循环,小心谨慎使用breakcontinue
      • 检查循环端点是否会有off-by-one的问题
      • 在嵌套循环中使用有意义的变量名增强可读性
      • 循环要尽可能短,便于一目了然
      • 把嵌套限制在3层以内

不常见的控制结构

  • 多处返回:指程序中途的return或exit
    • 只在能增强可读性时,使用中途的return
    • 用防卫子句提前退出,简化复杂的错误处理
    • 减少程序中的return数目
  • 递归:将复杂问题分而治之
    • 确认终止条件
    • 使用安全计数器防止出现无穷递归
    • 把递归限制在一个子程序里,避免循环调用
    • 留意栈空间
    • 可以用循环结构等价式的先考虑循环结构,如阶乘和斐波那契数列
  • goto
    • 反对随意使用goto
    • goto灵活度太高,不容易用好,在可以使用其他控制结构时,不使用goto
    • 在错误处理中,可以用状态变量、try finally语句实现跳出正常流
    • 如果在那1%的情况下需要使用goto,注意以下几点
      • 尽量一个子程序只使用一个goto
      • 尽量向前跳转而非向后
      • 确保所有的goto标号都被执行到
      • 确认goto不会产生执行不到的代码

表驱动法

表驱动法是空间换时间的一种编程模式,使用数据结构模拟逻辑结构,将大部分复杂度放到容易被理解的数据结构中,从而提升代码可读性。下面是一个代码范例。

1
2
3
4
5
6
7
8
9
10
11
12
13
if ((('a' <= inputChar) && (inputChar <= 'z')) ||
(('A' <= inputChar) && (inputChar <= 'Z'))) {
charType = CharacterType.Letter;
}
else if ((inputChar == ' ') || (inputChar == ',') ||
(inputChar = '.') || (inputChar == '!') || (inputChar == '(') ||
(inputChar = ')') || (inputChar == ':') || (inputChar == ';') ||
(inputChar = '?') || (inputChar == '-')) {
charType = CharacterType.Punctuation;
}
else if (('0' <= inputChar) && (inputChar <= '9')) {
charType = CharacterType.Digit;
}

使用一个查询表建立每个字符和它的字符类型的关联后,代码可以简化为

1
charType = charTypeTable[inputChar];
  • 查表方法
    • 直接访问:如查询每月天数,或不同年龄对应的保险费率
      • 有的时候键值要预先处理后才能直接使用,如可能很多年龄对应相似的费率,这时最好先将年龄换算到一个更好的key上
      • 进一步,我们可以把键值转换提取为独立的子程序
    • 索引访问表:和直接访问的区别在于,对于不易换算到键值的情况,提供一个额外的索引表,先映射到索引表再查到数据
    • 阶梯访问表:主要针对表中的记录是对数据范围而非数据点生效的情况,使用端点作为key
      • 留心端点带来的off-by-one情况
      • 可以使用二分查找代替顺序查找
      • 也可以使用索引访问技术

一般性问题

布尔表达式

  • 使用truefalse作判断
  • 简化复杂的表达式
    • 使用中间变量或布尔函数
    • 使用决策表替代复杂的判断逻辑
  • 编写肯定的布尔表达式,会让布尔表达式更易理解
  • 用括号分割较长的布尔表达式
  • 注意短路求值或惰性求值的情况
  • 按照数轴的顺序编写数值表达式,类似MIN_VALUE <= i and i <= MAX_VALUE,可读性好很多
  • 在C语言中最好把常量放在左边
  • 注意区分Java中a==ba.equals(b)

空语句

  • 小心使用
  • 使用doNothing()函数或noop()函数
  • 考虑能否换用非空的循环体

优化深层嵌套

  • 优化重复的if检查
  • 使用break简化嵌套if,如防卫子句
  • 转换成一组if-then-else结构
  • 转换成case语句
  • 将深度嵌套的语句抽离成子程序
  • 借助多态
  • 借助异常来跳出正常流

结构化编程

结构化编程的思路是仅使用顺序选择迭代的思路描述程序流,避免使用breakcontinuereturntry-catch来打断。

降低复杂度

  • 程序复杂度的一个衡量标准是,为了理解程序,必须在同一时间记忆的智力实体数目,即理解程序花费的精力
  • 控制流的复杂度和不可靠的代码以及频繁出现的错误息息相关
  • 可以通过计算子程序的“决策点”粗估子程序的复杂度
    • 从1开始,遇到ifwhilerepeatforandor加一,为每一种case加一

2018-2019: https://shenlvmeng.github.io/blog/2019/01/03/2018-to-2019/
2019-2020: https://shenlvmeng.github.io/blog/2020/01/04/2019-to-2020/
2020-2021: https://shenlvmeng.github.io/blog/2021/01/05/2020-to-2021/

从19年初写下第一篇总结到现在,已过去近3年,每年写总结时,都会发现计划永远赶不上变化,惊喜总在发生,也总能体验这辈子从未经历过的一些事情。就像在超市购物时推着一辆磕磕巴巴的小车一样,正反馈的前进路线需要不断地人为纠正。最终走出一条弯弯曲曲却通向想去方向的路线。

世事难料,2021年尤其是个例外。我目送了朋友的离开,见证了忠贞的新人携手相拥,告别了早该舍弃的人,与老朋友重逢,来到陌生却熟悉的新城市,拥抱了所爱也爱我的人,认识又立马和新朋友告别,开启看起来模糊却又无比清晰的未来道路。生活逐渐健康,生活更加明亮,生活逐渐充满期望。不过总有些不变的,譬如固定的长途骑行,以及在间歇性踌躇满志和间歇性混吃等死中间徘徊。

2021回顾

比照去年目标,在内在上,看完了4本技术书籍中的2本,以及剩余10本小说中的8本:

剩余的2本,devops读至一半,深感信息量之少,收获甚少,弃之。《代码大全》则由于过厚暂时搁置。不过,在某leader的启发下,对管理学产生了兴趣。从推荐的《领导梯队》和《赢》入手,接触到德鲁克的一些观念:

经过这几本的洗礼,感受到管理学绝不仅仅是日常的事务性管理动作。而有一整套价值观和方法论,如依人办事等。看完之后大开眼界,颇有些屠龙之术的意思。这也许只是我屁股不在目标人群中吧。于是剩余的两本德鲁克及时叫停,回到追求实用的现实世界。在技术以外了解一些其他领域的知识,启示挺大。

个人形象上,除了改成推平的毛寸(以及发胖)之外,几乎没有任何变化。发胖这点,也不能完全赖我,和对象也有一定关系。早睡早起这点倒是因为和对象同住坚持得很好,这大概是今年最大的进步了。锻炼频率从来了上海之后有所下降,等到对象回到附近之后预期会好很多。整体看,发胖这点还是要重视的。

社交上,来到上海后,除了老朋友外,倒是拾起了一些很久未联系的“新朋友”。情感上,很庆幸地,经过身边亲朋好友的轮番劝导和自己的冷静思考,让我及时从糟糕的关系中摆脱出来。谢天谢地,又让我遇到现在的她。说来奇妙又激动。2021年似乎在5月份和6月底开启了两次新篇章。之后整个下半年局面大为不同。经过艰难又忐忑的4个月异地,辛苦却又自然地合体。一切仿佛都是最佳的选择。

生活上,业余时间基本都是和对象一同。不过俩人的兴趣爱好相仿,骑车和看电影都能一起,因此这两点并没放下。在今年5月份开启新篇章后,斥巨资换了现在的新公路车,大大提升了骑行体验,不得不说真能买到快乐。之前的公路车刚好可以给对象骑。尽管11月才结束异地,已经一同骑了快10次了。魔都的骑行路况尽管比不上帝都,气温相对帝都还是要温柔一些。照例,今年完成了环太湖之旅,一路上经过苏州、无锡、宜兴、湖州、南浔,也算是满足了圣僧一路江南城市的愿望。清理完已有的洋酒库存后,小酌频率的确下降很多了,可惜对象酒量很差,平日只能看我表演。旅行上,在偶发疫情的情况下,和对象国庆去济南青岛转了一圈,十月底被痛仰两放鸽子后,得到了宝贵的杭州一日游。搬到和对象同居之后,俩人分工,一人负责干净一人负责整洁,整个房间倒是窗明几净、井然有序。平日周末一同下厨或探店或社交,不亦乐乎。livehouse倒是计划了好几次,可惜疫情人算不如天算,至今未能看到。

事业上,借助大方向的发展势头,年初得到了意料之外情理之中的提升机会。赶在行业腰斩个人选择导致的发展减缓前,获得此等良机,可以说很幸运了。而后随着来到魔都,第一次感受landing流程和新leader磨合,另外团队也有所扩张,十分难得。可惜由于公司政策调整,结合个人意愿选择,放弃了这个良机。去向了一个完全陌生的领域,极大提升通勤体验的同时,牺牲了先发优势。正如那位leader所说,“既然是去一项成熟的业务,最好提前降低下预期”,因此也做好了发挥空间有限,要重新建立信任的准备。有挑战才有意思嘛。也希望在来年能有所成果。结合管理学阅读还有某leader的1on1,在职业规划和发展路线上有了一些粗浅的想法。在和对象的交流下,了解到内卷业务的可怕,还有外企的香甜,不由得内心也心向往之。作为一项国际化业务,这里倒是也可以当做未来的跳板吧。何时才能成长到在外企大厂带人做事的程度呢?

至于落脚城市,就这里吧,也挺好。下一阶段,就是房子、车子、票子的事情了。有了可以一同踏踏实实的人,也放心去想踏踏实实的事儿了。

2021总结

  • 仰望星空的同时,脚踏实地也很重要
  • 保持身体健康
  • 减肥要提上日程
  • 生活中总会发生不公平的事情,不过整体来看,它还是公平的
  • 少吃一口饿不死,多做一点累不死
  • 迈出第一步,事情就会慢慢发生,就像在山顶推一个石头
  • 事业上,成果导向,用人所长,因人成功
  • 工作中有些事情,功夫在诗外,人际关系也很重要
  • 要开始考虑一些更长远更现实的事情,比如买房、养老、医保,做一个合格的父亲

2022展望

好了,写计划和期望的时候总是心情舒畅。下面列出一些期望,

  • 内在
    • 完成代码大全和至少一本专业书籍的阅读
    • 完成剩下所有小说阅读
    • 探索一个新的领域,形成积累
  • 外在
    • 控制体重回到80kg
    • 坚持早睡早起早饭
    • 坚持一项骑行以外的运动,如游泳
    • 升级对象骑行体验
  • 生活
    • 至少1次和对象的旅行
    • 至少1次户外长途骑行
    • 学习3种硬菜
    • 至少1次livehouse
    • 控制饮酒频率
    • 享受探店
    • 更好地融入魔都
      • 社保、户口
  • 社交
    • 和对象相互认识老朋友
    • 回一次家 & 去一次沈阳
  • 事业
    • 做出一项成果
    • 能在所在业务下带小团队

凡是经历没经历过的事情,都是好事。预知后事如何,明年拭目以待。

Bye~

deep link是指可以从任何渠道,包括短信、应用内、网页中直接跳转到应用app内的特定页面的技术。它一定程度上打破了移动端app间“孤岛”的局面,实现了快捷的跳转。根据是否安装app,主要有下面两种行为:

  • 已安装,唤起app并跳转到特定页面
  • 未安装,跳转到App Store或下载页面引导用户下载安装,在下载后的第一次打开自动跳转到特定深度页面,这种情况也叫deferred deep link

deep-link-info

场景

deep link可以应用在很多常见的场景下,如

  • 社交分享
  • 广告引流
  • web和app互通
  • 裂变活动
  • 短信、邮件营销

在web和app互通场景下,可以很方便地实现从外部回流页回到app内特定位置,如:

  • 电商类app:分享H5跳转到特定商品页
  • 游戏类app:分享H5跳转到特定房间或任务
  • 直播类app:分享H5跳转到特定直播间

deep link缩短了操作路径,减少了用户操作成本,从而降低用户流失率,帮助app拉新和留存。

实现方式

常见的实现方式有下面几种

  • URL scheme,iOS和Android下的通用方式,打开前会询问是否打开某app
  • Chrome Intent,Chrome 25+无法通过URL scheme 唤起 App,必须使用Intent
  • Universal Link,适用于iOS 9及更高版本,点击http/https即跳转,操作丝滑
  • App Links,适用于Android 6及更高版本,点击http/https即跳转,操作丝滑

除了上面几种,还有Smart App Banners、剪贴板等方案

URL scheme

格式形如[scheme:][//authority][path][?query][#fragment],一般使用在iOS 9和Android 6之前。方法通用,但有以下问题:

  • 本身没有规范,难以获知要跳转的path或query
  • 功能不全,app越复杂,scheme就越复杂
  • 会被拦截,包括浏览器或应用的webview,如微信
  • 打开app失败后,iOS会有错误弹窗,体验不好
    • 提示网页无效
  • URL scheme可能重复

常见Scheme URL

微信 电商 浏览器 系统 其他
weixin://dl/scan 扫一扫
weixin://dl/moments 朋友圈
weixin://dl/settings 设置
淘宝:taobao://
支付宝:alipay://
美团:imeituan://
Chrome:googlechrome://
UC 浏览器:ucbrowser://
邮箱:mailto://
短信:message://
App Store:itms-appss:// macappstores://
飞书:lark://
微博:sinaweibo://

Chrome Intent

Chrome 25+无法通过iframe的src启动Android app。改为构造形如下面的intent锚点。

1
2
3
4
5
6
7
8
9
intent:  
HOST/URI-path // Optional host
#Intent;
package=\[string\];
action=\[string\];
category=\[string\];
component=\[string\];
scheme=\[string\];
end;

end前添加S.browser_fallback_url=[encoded_full_url]可以指定唤起失败的地址。一个构造好的intent形如:

1
intent://path#Intent;scheme=xxx;package=com. xxx;S.browser_fallback_url=https://xxx;end

官方介绍

Universal Link,即通用链接,是iOS 9推出的通过HTTPS链接来启动app的特性。既可以打开app,在没有安装时,则打开特定网页。对比URL Scheme优势在于:

  • 无缝切换,不弹窗确认
  • 兼容性好,未安装时直接打开网页
  • 使用通用的HTTP协议,不用担心scheme重复
  • 通过网站的配置文件和app关联,保证安全

在网站下根目录或.well-known路径下需要有apple-app-association(无后缀名)JSON文件。例如,知乎的配置文件为https://oia.zhihu.com/apple-app-site-association。在配置时,

  • 需要保证有一个HTTPS的域名,最好和web网页域名区分开,保证web网页的正常访问
  • 在开发者中心 ,Identifiers下 AppIDs 找到自己的App ID,编辑打开Associated Domains服务
  • 打开工程配置中的 Associated Domains ,在其中的 Domains 中填入你想支持的域名,必须以applinks:为前缀
  • 配置apple-app-site-association文件,不带任何后缀,上传该文件到服务器的根目录或者.well-known目录下

在网站下配置好Universal Link后,用户点击网站链接后,即会直接跳转到App,而不需要经过浏览器。当然,微信等app还是会拦截Universal Link的行为,需要在微信注册自己的应用id和Universal Link。

官方介绍

App Links,类似Universal Link,是Android 6(Android M)及以上操作系统中适用的HTTPS路径链接。可以直接将用户带入到Android app内的特定页面中,实现上,网站配置文件名为assetlink,只能放在.well-known目录下。如https://vt.tiktok.com/.well-known/assetlinks.json。不过,国产的安卓版本以及微信等app依旧会拦截。

其他

  • Android webview本身支持唤醒第三方app,但如果设置了自定义WebViewClient则需要自己处理,具体是指在shouldOverrideUrlLoading方法中决定如何处理
  • Smart App Banners,在网页里增加一个<meta>标签,可以在打开页面时,检测手机是否安装某个app,并且在页面顶部显示一个Banner显示App的基本信息,安装时显示打开,未安装时显示安装

相关产品或开源库

结合点击行为、重定向行为、来源去向分析、落地页等,还可以做很多事情,市面上也有下面一些产品:

另外,也有一些开源库实现web跳转app

–END–

与其说德鲁克是管理学鼻祖,他更像在研究社会、组织、企业存在的合理性与最佳实践。德鲁克认为人性是不完美的,那人设计出来的社会也不可能完美。他抵制极权专制,这里的极权不同集权,所追求的是全面彻底地操纵和控制人类的每一个成员。德鲁克视管理学为一种“博雅艺术”(liberal art)。它既不像人文文化,也不像科学文化。它关心人的价值成长和社会组织的影响,但又关心管理工作的应用的成果。这使得管理学不像世俗观点中的那样,是一个人或一个机构的成功学。它只在让每个人在社会和社群中更健康,自由地选择并履行承担的责任,从而是人类社会和社区更健康,人们受到更少的伤害和痛苦。

而这本书即从管理者的成效出发,探讨一个管理者如何能管理好自己。毕竟让一个自身成效不高的管理者管好他下面的下属,几乎是不可能的事情。而卓有成效的管理者也逐渐成为社会中的关键资源。

卓有成效可以学会

卓有成效重要且可以学会

  • 首先端正思路,管理者为什么必须有效
    • 有效即“做对的事情”,而不是纯执行的“把事情做对”
    • 当下知识工作者逐步增多,他们产出的更多是抽象的知识、创意或信息,无法用传统衡量体力活动的方式衡量,因此需要自己管理自己,自觉做出贡献,追求工作效益
  • 谁是管理者
    • 如果在组织内,一个知识工作者,能够凭借职位或知识,对组织负有贡献的责任,因而能对组织的经营能力或成果有实质性影响。那他就是一位管理者。
    • 衡量知识工作,主要应看结果而不是工作繁杂程度
    • 一般的经理人员工作性质和董事长、政府机构的行政领导是相同的,那就是计划、组织、整合、激励和考核
  • 管理者的不利现状
    • 组织的管理者会面临下面一些压力
      • 时间往往属于别人,不属于自己,如经常有外部人员拜访
      • 往往被迫忙于“事务性工作”,除非他们敢于采取行动改变周围的一切
        • 管理者需要一套判断体系,去找到真正重要的事项
      • 管理者的贡献能被其他人利用时,才算有效
        • 组织是使个人才干能够增值的工具,一个人的知识可以被组织吸收,作为其他知识工作者工作的资源
        • 贡献不能被有效利用时,有效性也会打折扣
      • 管理者会受到组织的局限
        • 受到的信息会被组织过滤
        • 组织需要为外部服务,因此需要得到外部的信息
        • 组织是社会中的人为产物,和生物体类似的是,成长得越大,消耗的资源和复杂度就越大
    • 对外部信息的收集,人从现象中的抽象能力往往优于机器(类似深度学习)
  • 提高有效性,让工作达到令人满意的程度
    • 不要期望万能的天才来达到绩效,通才难求,学会善用专精于某一领域的人
    • 通过从各种有效的管理者归纳来看,他们各个方面都不完全一样,人人都具有做好该做事情的能力,即有效性
    • 下面一些习惯是成为卓有成效管理者所必须的
      • 善用时间,知道时间都去哪儿了
      • 重视对外界的贡献,目标导向
      • 善于利用长处
      • 集中精力到少数重要领域
      • 敢于、善于做决策

掌握自己的时间

时间是最珍贵的资源,先摸清自己的时间分配,再统一管理和安排。

  • 所有资源里,时间不像资金、人力,是最稀有毫无可替代性所有工作必须消耗的资源,而人往往不善于使用这种资源
  • 管理者面临的时间压力
    • 管理者作为知识工作者,他的大多数任务,都需要相当多的整块时间
    • 管理者要与他人一同工作,需要各种信息,需要讨论、需要指导他人,需要协调人际和工作关系,这些都是费时的
    • 只有在脑力上多费时,才能在体力上少费时
    • 如果时间短促,一个人就只能考虑他已经熟悉的事,做曾经做过的事,无法创新和变革
  • 你诊断过你的时间么
    • 第一步是记录时间的耗用情况,一定要当即记,事后回忆往往不准确,然后问下面一些问题
    • “什么事情根本不必做”
    • “哪些活动可以由别人代为参加”,学会授权
    • “有浪费别人的时间么”,如过多的会议
  • 消除浪费时间的活动
    • 找出可以通过制度或远见避免的时间浪费
      • 一个平静无波的工厂,必然是管理上了轨道,因为所有危机都已预见,并变成了例行工作
    • 人员过多会带来人际关系的时间浪费
    • 组织不健全,表现就是会议太多
      • 之所以要开会,是因为某种情况的所需的知识不能装在一个大脑里
      • 要开会,也要有计划
    • 信息功能不健全,上下文不透明,或信息表达不当
  • 统一安排可以自己支配的时间
    • 高级主管可以自由运用的时间,可能只有1/4,组织越大,维系组织运行而不是发挥组织价值的工作越多,从而不由自己掌控的时间越多
    • 可以给自己的时间划分最小单位,如90分钟,聚合碎片时间
    • 对时间的控制和管理不是一劳永逸的,要不断记录时间消耗并分析

我能贡献什么

关注产出,目标导向,可以帮助自己thinking out of the box。

  • 重视贡献,注意对成果负责,才能看到整体的绩效
    • 只有这样,一个人才能考虑自己的技能、专长和整个组织以及组织目标的关系
    • 管理者如果不自问“我可以做出什么贡献”,在工作中就不会有远大的目标,而偏向执行的角色
  • 一般机构对成效的要求往往有下面三个方面
    • 直接成果
    • 梳理新的价值观或对价值观的重新确认
    • 培养和开发明天所需的人才
  • 职位越高,对外所需的贡献就越大
  • 对于专业人员,必须使自己的“产品”——即他的只是可以为别人所用;同时,肩负贡献的责任,要求他知道应该了解别人的需要、别人的方向,别人的理解,从而使别人能够应用他的成果
  • 有效的人际关系有4项基本要求,而着眼贡献正可以满足
    • 以目标为导向,可以很方便对齐期望,有助于互相沟通
    • 强调贡献有助于横向沟通,促成团队合作
    • 个人发展,一定程度上也要看是否重视贡献
    • 重视贡献的管理者必然能启发他人寻求自我发展,从而培养他人
  • 重视贡献,才能使管理者的视线从“内部事务”转向“外部世界”

发挥他人长处

人无完人,有效的管理者可以使人发挥长处,减少短处带来的限制。

  • 世界上从来没有发生过下属的才干反而害了主管的事
  • 人的精力有限,卓越通常只能表现在某一或者某几方面
    • 是用人来做事,不是用人来投自己所好,或投主管所好
    • 有效的管理者从来不问“我和他能合得来吗”,而是问“他能贡献什么”,从来不问“他不能做什么”,而是问“他能做什么”
  • 要坚持因事用人而不是因人设事,这样才能减少组织变动,同时保证以任务为重心,而不是以人为重心
    • 只有极少数例外,譬如有特殊才干,从事非一般工作,取得杰出成就的人
  • 能建立一流经营体制的管理者在公事上,通常会和周围同事以及下属保持一定距离,避免个人好恶挑选人才
  • 如何做到发挥长处,同时避免因人设事的4个原则
    • 一个职位,如果先后多人担任都失败了,那肯定是常人无法胜任的职位,需要重新设计
      • 国际部副总裁的压力,需要由按产品类别调整组织,或是按市场的社会经济背景调整组织来合理规划
      • 只有让“平凡人做出不平凡事”的组织,才是好组织
    • 职位要求要严格,涵盖要广(想象空间大)
      • 从而保证下限高,人才又有能充分发挥的机会
      • 知识工作者的职位设计,还应该能够让人及早发现自己是否适合该职位
      • 与体力劳动者不同,知识工作者的贡献产出不仅和本身的知识技能有关,和组织的目标、价值观也有很大关系
    • 用人时,先考虑某人能做什么,而不是职位的要求是什么
      • 在考评、绩效制度上,更多从“某人能完成什么”的视角看问题
        • “哪方面的工作他确实做得很好”
        • “哪方面的工作还可以做得更好”
        • “为了充分发挥长处,他还可以再学习什么知识”
        • “如果我有了子女,愿意让子女在他的指导下工作吗,为什么?”
      • 正直的品格很关键
    • 必须能容忍人的缺点
      • 结合具体任务来寻找别人的长处
  • 用人所长是有效管理者的必备素质,也是组织能否有效的关键
  • 卓有成效的管理者还会设法发挥上司的长处
    • 不能唯命是从,需要协助上司发挥所长,也能给自己更大的空间
  • 也要了解自己的长处,寻找适合发挥自己长处的工作方式,做出自己的绩效
  • 管理者的任务不是去改变人,而是在于运用每个人的才干

要事优先

时间有限,做重要的事;而立足现在看未来才可看到真正重要的事情

  • 时间总是不够,只有几种个人、组织的所有才干,才能获得成果
  • 一次只做好一件事,恰恰能加快工作速度
    • 我们往往会低估完成任务的时间
  • 很多组织都容易错误地看待过去的成功和失败
    • 当下的很多计划和制度,是基于过去的经验确定的,时间久了已不能产生成果
    • 及时重构,去肥增瘦;只有推陈才能出新
  • 对于新工作,应责成确能证明有能力的人来负责
  • 所谓压力,总是偏爱机构内部的事务,偏爱已经发生的事而忽视未来,偏爱危机忽视基于,偏向急功近利而忽视现实世界
  • 真正的难度不在决定做什么,而是什么可以缓一缓
    • 被搁置一般等同于被取消,因为外部世界变化极快
    • 下面有一些可以确定事情优先顺序的原则
      • 将来 > 过去
      • 重视机会,不要只看到困难
      • 选择自己的方向不盲从
      • 目标要高,要有新意,不能只求安全和容易
  • 化机会为成果,肯定比解决旧问题更有生产性。解决旧问题,不过是恢复昨天的平衡而已

决策的要素

管理者要做对组织绩效有效果的决策,决策是观念性、战略性的,不是细节的、解决问题的。

  • 一项决策如果不能付诸行动,就只能算作想法
  • 贝尔公司的费尔和通用汽车的斯隆,解决问题,都着眼于最高层次的观念性认识,所做的决策不是为了适应当时的临时需求,而是战略性考虑
  • 决策有下面一些要素
    • 判别问题性质
      • 一般遇到的问题可以分为四类
        • 表面现象之下的真正经常性问题
        • 特殊情况下偶然发生的实质上经常性问题
        • 真正偶发的特殊事件(“黑天鹅事件”)
        • 第一次出现的“经常事件”
      • 对于经常性问题要有经常性的解决办法——一种规则、一种政策或一种原则
      • 判断问题性质并没那么简单,可能遇到一些常见错误
        • 把经常性问题视为一连串的偶发问题(“实用主义”)
        • 将真正的新问题视作旧病复发,从而采取错误的方法
        • 对根本性问题界定似是而非
        • 只看到问题局部,没看到全貌
      • 一个有经验的管理者总会先假定问题只是表面现象,从最高层次的观念方面寻求解决方法。他要找出真正问题,不满足表面现象,从更基本、更理性、更广泛的观念上谋求解决办法
    • 找到解决问题的限制条件或假设前提
      • 边界条件往往不容易找到,因为每个人的视角不同
      • 错误的边界条件,比没有更误事
      • 边界条件清晰时,有助于决策人在情况变化时,更快用新决策取代旧决策
      • 依赖太多边界条件去决策是危险的
    • 考虑问题的正确方案,之后再考虑必要妥协或让步事项
      • 如果一开始就问“什么是能让人接受的决策”,那永远不会有结果
    • 决策要兼顾执行措施
      • 必须能准确无误地回答下列问题
        • 谁应该了解这项决策(object)
        • 应该采取什么行动(what)
        • 谁采取行动(subject)
        • 行动如何进行(how)
    • 重视反馈,验证决策的有效性
      • 决策是会过期的,需要建立反馈机制更新决策

有效的决策

一项决策若要保证有效,需要考虑各种方案,甚至包括反对意见。另外,电脑不能取代管理者做决策,反而会让更多人参与到决策中。

  • 决策不是从搜集事实开始,而是先有自己的看法和立场,再去搜集事实证明
    • 搜集事实本身就会预设立场,做不到客观
    • 大胆猜测,小心求证:假设和见解不必辩论,鼓励大家提出,但是必须经过深思,必须经得起验证
  • 如何衡量决策,需要的时间和经历也极多
    • 使用反馈的制度
    • 只有有多项方案,从中选择一项,才能称得上判断
    • 管理者一定要先有若干种不同的衡量方法,再从中选择最合适的一种
  • 好的决策,应该以互相冲突的意见为基础,从不同观点和判断中选择
    • 反对意见相当重要
      • 唯有反对意见,才能保护决策者不至沦为组织的俘虏
      • 反对意见不深,也可以作为决策的“另一方案”
      • 反对意见可以激发想象力和创造力
    • 当然世上有蠢材和恶作剧的人,不过有效的管理者会假定,某人意见纵然错了,也是由于此人看到的现实不同,或者关注的问题不一样
  • 是否真的需要一项决策
    • 满足下面两项原则即可
      • 利益远大于成本或风险就该做
        • 不要做鸡毛蒜皮的小事,也不要墨守成规
      • 行动或不行动,切忌只做一半或折中
  • 决策前,想清楚规范、不同方案、得失也衡量了,采取什么行动也想好了。尚未想清楚前,不要冒冒失失地决策,但都想好了,也绝不会优柔寡断
  • 电脑只是工具,人类则不是逻辑的,而是具有感官的;电脑所做的决策是预先安排的,不能随机应变,都是基于硬性原则
  • 每一位知识工作者有效决策能力的高低,决定了其工作能力的高低

总结

全书内容无非两点:

  • 管理者的工作必须卓有成效
  • 卓有成效可以学会

做到卓有成效需要从几个方面努力:

  • 记录好时间的使用情况,分析时间记录,消除不必要浪费
  • 集中眼光在贡献上,从执行进入到观念,关注个人目标和组织目标的关联
  • 充分发挥人的长度,融合个人能力和组织成果
  • 做重要的事情
  • 有效的决策,合理的行动

今天的组织需要的是平凡人做不平凡的事业。组织取得好的成效,要靠组织中的人切实进行系统化、专门化的自我训练,成为有效的管理者。在如今,知识工作者为组织服务,除了物质上,在心理需求和个人价值上也需要从工作和职位上获得满足。因此,一定要使组织绩效和个人成就结合起来。管理者在卓有成效上的自我提高就是唯一解法。

0%