软件开发

427 阅读24分钟

软件开发活动

设计的主要工作

  • 深入认识领域,分析和梳理需求
  • 领域建模,概念提炼,实体及关联
  • 选择合适的数据存储,解决关键技术问题
  • 选择合适的技术栈

编程的主要工作

  • 选择强大而可靠的库和框架,提升开发效率;
  • 注重实现细节,保证性能和稳定性
  • 增强健壮性,防御式编程
  • 遵循单一职责及小而美原则,创建可复用的工具与业务方法
  • 为扩展预留空间

写出好程序

  • 抽象与分层。一个流程里,engine, checker,fetcher, assembler, transformer, ... , 构建合适的抽象与分层,流程与交互会更清晰可读;
  • 关注点分离。将大量细小的业务点分离成单一职责的方法,更易测易复用而不易出错,也会更容易组合而扩展与配置。
  • 防御式编程。牢记:错误将无孔不入。一个流程或功能有哪些潜在的错误以及如何精细处理好,是区分合格程序员与优秀程序员的一大依据。一个文件读操作,可能会发生哪些错误呢?
  • 设计模式,数据结构与算法、函数式编程与元编程、IOC与AOP是增强编程内功的不二法门。

组成图

子系统

系统是具有层次性的。可以将整个系统划分为三个主要子系统:规划子系统、调度子系统和执行子系统。

  • 规划子系统:主要是将输入转译成具有优先级的目标和任务集合,形式是目标、任务、优先级及策略。
  • 调度子系统,主要是高效调度优先级任务、现有人力和资源,比如时间、精力、体力和智力、资源、工具等。
  • 执行子系统,主要是根据调度子系统分配的任务、人力和资源,完成指定任务,输出答复、软件、文档、信息等给外部。

流程图

系统功能通常可以通过“输入-输出” 模型来审视。软件开发活动的输入,通常是问题、优化、需求、项目、进度同步等,输出通常是软件增强、及时答复等;因此,软件开发活动管理的功能是,准确、高效地将问题、优化、需求、项目、进度同步转化等转换为可以解决问题和帮助他人的软件增强、及时答复、文档分享、信息同步等。

优先级策略

  • 愿景追溯法:重大目标和关键事项的根源往往来自于愿景; 为什么而存在,延伸向何方。比如,感受理性与美,科学、诗歌与艺术的熏陶。
  • 影响判断法: 完成该事项会产生非常积极重要的影响,积蓄优势,串联重要目标,产生有益成果;比如,提升核心竞争力,生活更稳定。
  • 目标贴近法:在逻辑上与最重要的近期目标紧密关联,完成该事项可以推进重要目标的重大进展;比如,增强健康和力量,瘦身等。
  • 形势分析法。 环境和形势变化亦是关键事项的一个风向标。如何更好地应对环境变化,是一个重要考虑点;比如,国家政策对某行业的倾斜,行业的选择。
  • 反向推断法: 如果被现实中的某个紧急事情打击得猝不及防,可以反向思考,究竟是什么没有做好导致了困窘的境地;比如,故障或意外。

沟通效率准则

  • 优先考虑使用点对点模式,使用异步消息沟通。
  • 若三言两语说不清楚,宜改用语音沟通。
  • 若沟通有较大分歧,且优先级较高,宜面聊沟通。
  • 方案设计和确认时,先找一两位有丰富经验的人提建议。
  • 讨论性会议,尽量控制在 3-5 人的范围内。
  • 参会人数达 7 人以上时,忌 20 分钟以上的会议。
  • 尽量仅与干系人沟通。
  • 仅当需要同步信息时,且须确定每个人都需知道时,人数可多,但时间要控制在 10-20 分钟内。
  • 避免在群里讨论有分歧的问题;与少数人单独面聊后,在群里同步结论。

效率提升

  • 提升单线程工作能力: 既然只能做一件事,那就选好和做好这件事。通过聚焦关键事项法则,尽可能保证所做的事情的影响最大化,这需要仔细做好规划;其次,保持高度的专注,尽可能避免各种打扰,尤其要滤除各种通信上的干扰。急事电话,非急事统一时段处理
  • 并发:
    • 需要识别出并行模式中的前置串行部分。多做这样的训练,有意识地识别可以提升为并行模式的工作。
    • 提升并行、并发、分布式程序设计技能,使自己具备”指挥千军万马“的能力
      • 学习系统思维和系统思考
      • 坚持第一性原理思考法。凡事多思底层逻辑

软件设计要素

  • 领域建模: 定义自身的定位,定义基础要素,定义完备的能力集合
  • 系统划分:将整个系统划分为若干正交的紧密关联的子系统,以及高内聚低耦合的小而美的模块与微服务,理清职责、交互与边界。划分的基本原则是“识别、分离和组合关注点
  • 应用模型:应用模型涉及到数据处理的全局流程和交互方式。 我所接触过的,主要有“请求-响应模型”、“流处理模型”、“分布式模型”、“人机交互模型”。
  • 架构流图:架构流图是系统组件及组件交互的动态流动图,决定了处理数据和领域对象的全局控制结构。组件化是使用架构流图的前提。
  • 存储设计:存储设计是领域建模的设计细化,决定数据的结构以及领域对象的表达与存取,是软件详细设计的关键环节
  • 模式系统:设计模式、业务模式、架构模式、分析模式。详见架构设计

现代软件应用的挑战

企业要生存,多样化、灵活的业务是直接驱动力;业务可扩展能力和低维护成本是应对业务变化的主要法宝,软件因为业务的扩张和拓展而快速成长和变得复杂;此时的复杂是多样化的业务流程与规则导致的;

  • 如何去认识行业领域以及业务?
  • 如何理清纷繁的事物关联, 把握重要的主线,做好规划?
  • 如何将业务层面的解决方案转化为技术层面的架构设计和实现?
  • 如何将软件使用中遇到的问题转化为重要的驱动力并保持软件和企业持续长久运行?

困难2

  • 缺乏全局的架构设计,代码逻辑散落在各处,相关文档缺失,无从追觅其缘由;
  • 作为一项团体协作的产出,不同人的代码风格不一致,导致阅读理解上的困难;
  • 市场业务的多样与演进,历史债务与当前需求的交织,设计与实现的不一致,导致系统容易成为一个难以理解和维护的大杂烩;
  • 持续保障线上服务的稳定运行,阻止负面影响的 BUG 和故障,需要严格的测试工程和流程保障。

策略

  • 规划各个应用和模块的职责与定位,收拢逻辑, 梳理业务的服务依赖关系。
  • 制定开发规范,在整体上保持一致的风格,可以允许个性化的创造,更多体现在设计能力上。
  • 进行持续而严谨的 CodeReview , 增强团队成员的代码质量意识和技能。
  • 迭代消减历史债务,用新方式替换老的方式;对于新需求,遵循设计先行原则,认真评估设计方案。
  • 与测试同学密切合作,学习测试方法和技术,保障测试充分。

刚与柔

  • 刚性能力:体现在软件的性能:单位时间内处理多少请求,每个请求的响应时间,占用多少资源,请求处理的成功率等;
  • 柔性能力:体现在健壮性和可维护性: 健壮性使得应用更友好地处理错误,减少用户烦恼和困惑; 可维护性良好的软件才有健康稳定成长的潜力。

方法论

分而治之 - 系统思考 - 抽象模型 - 科学规律 - 注重细节, 是从整体到部分,从局部看到全景,从具体深入抽象,从宏观到微观,从理论到实际的方法总论。

是从整体到部分,从局部看到全景,从具体深入抽象,从宏观到微观,从理论到实际的方法总论。

  • 分而治之。 复杂任务往往是由简单任务通过多层次组合而形成的;将复杂任务进行分解,分解到每个子任务都在能力掌控范围内。
  • 系统思考。 仅仅理解事物的子部分还不够,需要以系统思考的视角,去理解事物组成部分的关联,事物存在的支撑机制。从事基本的开发工作,也许只要分而治之,但要掌握软件工程之道,要有系统思考的全局观。
  • 抽象模型。 将事物赋予概念,对事物关联建立简要的模型进行处理。对事物的理解从具象的形式到抽象的意义。比如圆形意味着阻抗低;锁意味着隐私;房子意味着庇护;门意味着可授权的入口;盆意味着开放性容纳等。
  • 科学规律。 学习和探索事物与逻辑的性质和运行规则集合。比如数学性质;物理学定律;适当的缓存能提升效率。
  • 注重细节。 在宏观理解的基础上,还需要关注细节,解决好细节问题,才能做到更好。

业务的思维框架:数据模型+规则+语义

中大型业务系统中,往往多种业务相互交叉,错综复杂,使得系统变得难以理解。

要真正理解业务,需要从业务本身上去理解,严格区分出“业务规则”与“系统设计与实现”这两个不同的层面。工程代码,可以作为重要的参考。

领域建模

  • 领域建模是软件设计的初始点。 反复追溯事物的本质“是什么”
  • 从不同视角去理解事物的性质,理解事物之间的关联,梳理事物与活动的流程与环节,抽象出实体与关联,规则与约束
  • 它涉及到对现实事务、规则、流程的理解和清晰准确的提炼。在做领域建模时,切忌过早思考技术的实现。
  • 一个设计优雅的系统,必定含有一组核心概念集合,这些概念之间存在紧密的联系。

数据模型指的是,需要哪些数据项,数据项之间的关联,如何有序地组织这些数据项。数据模型是软件整体设计的导航图。确定良好的数据模型,设计就成功了一大半。

规则

规则指的是,数据项及流程要满足什么约束 ?为了什么目标而服务 ? 提取规则

  • 流程。首先,析取最通用的流程作为基础;接着,再看有多出来的或剪裁的或者定制的。
  • 判断。从代码中的各种 if-else-throw 可以提取出来。

语义

构建了数据模型和规则之后,为了更好地扩展和发展系统的能力,需要对数据项及规则进行清晰无歧义的语义定义

  • 状态类语义。比如订单状态,通常涉及到业务的流转过程,需要考虑可扩展性;当新增新的业务状态时,能够容易地支持。
  • 金额类语义。比如应付金额和实付金额。如果缺乏清晰的语义,在多种业务交叉的情况下,金额类字段就可能有多种理解和取值,很容易导致资金的故障。而资金的故障通常是最严重的故障类别之一。
  • 标识类语义。标识类字段用于唯一标识一个实体。 通过“数据模型+规则+语义”,可以勾勒出一个业务系统里的基本业务图景。

应用模型

  • 请求-响应模型: Web应用通常采用“客户端请求-服务端响应”模型。主要有“同步”和“异步”两种方式。
  • 流处理模型:海量数据的(准)实时计算应用可采用流处理模型
  • 人机交互模型:前端
  • 分布式模型

质量

  • 代码质量: 高质量软件的基石。 任何设计、测试和工程方法都无法挽救烂代码写出来的系统。
    • 遵循良好的编程风格和习惯、追求和学习编写优秀代码、熟悉和避免常见编程错误与陷阱、注重细腻的代码细节、持续小幅重构
  • 设计质量: 设计质量往往关乎软件的全局性品质,比如稳定性、可扩展性、健壮性等。
    • 丰富的开发设计经验、对健壮性、稳定性、扩展性、可维护性、高压力承载能力等系统质量指标的全面理解,更深入透明地理解系统的运行,以及仔细考量避开陷阱
  • 测试质量: 通过良好设计和实现的系统,需要测试质量来把关,保证代码没有重要BUG和变更不影响原有系统。
    • 单测、接口测试、对比测试、压力测试等
  • 工程质量: 在代码、设计、测试质量的保证下,还需要工程手段(比如CodeReview, 持续集成、线上错误巡检等)来聚合所有的环节,保证更好的输出。

代码质量

作用域

  1. 避免使用全局变量,共享变量。
  2. 使用时声明和定义,尽可能缩小变量的作用域。
  3. 尽量使用私有变量,缩小变量的暴漏范围。
  4. 延迟初始化,使用有意义的初始值,该初始值可以作为默认值。
  5. 循环变量作用域限定在循环内。

空处理

  1. 在操作对象引用或指针时,先进行非空检查。若为空,可以给出提示信息、返回默认值,或者什么都不做。 2. 获取API结果后,使用之前要对结果对象进行判空
  2. 将对象与原子值比较时,要先对对象判空
  3. 处理数组或集合前先判断是否为空

条件处理

  1. 尽早 return ; 使用卫述句解耦多层嵌套 if-else
  2. 使用 map 或 表结构 替换简单的多值 switch 分支
  3. 不要误敲操作符,比如将 && 写成 & 。== 写成 = 。
  4. 涉及多个与或非复合条件判断时要小心,正确使用与或操作符; 可以将条件拆解为多个含有业务语义的原子条件然后组合起来更可读。
  5. 不确定多个复合操作的优先级时,将操作加括号。

循环遍历

  1. StreamAPI > foreach > iterator > for (int index) > while 。
  2. 循环遍历小集合时,生成新集合,而不是修改原有集合。
  3. 如果遍历的时候需要删除元素, 使用迭代器。
  4. 如果要访问索引使用 for 循环时,注意使用半开半闭 0 <= i < len or size ;防止越界。
  5. 涉及范围(数组、集合等)的代码要特别注意空值及边界情况

注释与文档

  1. 注释简洁扼要。重点说明功能、用途
  2. 涉及特殊处理,重点说明原因。
  3. 使用已有算法时写明引用出处; 标注作者信息及联系方式以供交流。
  4. 做数据结构转换时,可以通过一个示例说明具体转换行为。
  5. 做抽象类时,可以通过一个实现类来说明抽象类的使用。
  6. 说明隐含的不易猜测的含义。
  7. 提示在特定场景下可能的坑。

函数编写

  1. 创建单一职责的短小函数,尽量控制在 60-80 行
  2. 对参数做严格校验,不做任何假设。
  3. 参数个数尽量不超过3个。
  4. 尽量避免将类型相同的参数排列在一起。
  5. 尽量不改变传入的参数,而是将结果返回。
  6. 总是习惯创建新的可复用函数,而不是将逻辑直接写在主流程中。

错误处理

  1. 区别错误与异常
  2. 遵循防御式编程。不能确定的变量,复杂逻辑、IO 操作捕获异常
  3. 调用API后要注意捕获异常。
  4. 打印适当的关键日志方便排查问题原因。日志信息带上可识别唯一实体的关键词。
  5. 使用正确的日志级别。对于排查有帮助的用info级别,有轻微问题但不影响结果和整体的用warn级别,影响结果和整体的错误用error级别,导致系统崩溃的用fatal级别。尽量避免使用 debug 级别。
  6. 捕获所有的异常。底层错误打印异常日志并转译成高层语义,保证高层视图的正确性;给出合理的提示信息及错误源信息。
  7. 将所有的错误码和错误消息集中到一个类或一个包下集中管理。
  8. 隔离易出错部分, 将复杂、晦涩、容易出错的手工解析代码隔离在系统的小角落范围。
  9. 解析JSON串或数值型字符串时要捕获异常,因为难以预料字符串格式是否合法及具体内容。

业务流程与逻辑

  1. 写代码前规划一下思路,做一点设计性的思考工作; 约束条件、资源、算法和流程先用草图勾画清楚。
  2. 考虑所有的输入,限定合法输入的范围,拒绝非法输入;
  3. 保证输出内容正确完整,格式清晰; 若输出依赖于某些变量,要保证当变量修改时能够同步到输出。
  4. 分离可变和不可变部分。通用功能提炼成工具箱,业务特定逻辑放在主流程中。

设计质量

明确痛点

  • 功能诉求: 竞争对手有拼团功能,赚了好多钱好多粉丝,我也要有!【新功能】
  • 稳定性优化: 时不时出现xx报错,真是令人烦躁!同时一波大流量来袭,系统波动有点大啊!【稳定性】
  • 性能提升:怎么这么慢啊 ! 这么多订单,得处理到什么时候?【响应速度与吞吐量】
  • 维护成本: 这方案得占双倍的存储资源,还有两个同步,理解起来多费劲!这个报错,没法看出问题在哪里,还得再打个日志看看。商家在等着修复问题,真急人!【资源/时间】
  • 弹性: 明年订单量要增加3倍,现在这个方案貌似扛不住啊!【容量扩展】
  • 数据: 这待发货订单数显示为2,怎么点进去没订单?【对比分析有困惑】
  • 体验: 要做完一个批量操作,要好多步骤,还容易出错,真耗费时间啊!【步骤繁琐,易错】
  • 及时: 更新一个内容,要马上生效,而不需要重新修改代码部署系统。【即时更新】
  • 安全: 啊啊,不小心把DB/重要文件目录数据删除了!【安全性提醒】
  • 扩展:实现一个需求,要改这么多代码?
  • 重构: 这么多新的业务需求,真没法改了!非得动大手术了!

有序思考

  • STEP0: 弄清楚问题的背景及来龙去脉。
  • STEP1: 明确功能或服务目标,确定硬性质量要求(通常是性能),或软性质量要求(通常是健壮性、可扩展性、可维护性);
  • STEP2: 确定重点关注者,数据的存储和分布;
  • STEP3: 思考最终方案形态应该是怎样,现状是怎样,能够做怎样的权衡取舍;
  • STEP4: 根据设计准则,思考处理对策。
  • STEP5: 设计沟通,寻求更有经验的帮助。
  • STEP6:确定设计方案,分离关注点,具体设计和实现。

容错处理

  • 减少了错误发生的可能性。
  • 错误发生时,更安全友好地处理。
  • 不会因为次要局部影响整体。
  • 减少了故障可能性,或可能故障级别。
  • 故障发生时,能够更快更安全地处理和恢复正常。

性能与稳定

  • 快速的响应时间和吞吐量;
  • 大幅减少任务运行时长。
  • 系统在大流量情形下的稳定运行。
  • 对外部依赖进行降级或熔断。

扩展能力

  • 底层模型统一。
  • 核心简洁而稳定,外围可扩展。
  • 适当地分离关注点,组合和组织关注点。
  • 组件化、配置化,通过增减插件来支持需求。

维护成本

  • 减少了存储资源占用。
  • 减少了多处同步。
  • 能更快速地定位问题,大幅减少了排查和解决问题的时间(秒/分钟/小时/天)。
  • 分离出了变化的部分,更容易识别变化和扩展。

配置化

  • 解决一个需求时,建立相应的配置,当后续可能发生细节变更时,只需要修改配置即可即时生效

一劳常逸

  • 建立良好的约定,解决一次,出问题只追溯源头

依赖弱化

  • 减少了不必要的依赖(API,apollo,NSQ, KV 等),或者至少不引入新的依赖。

最小复杂

  • 总是首先寻找简单、改动最小、比较彻底的方案。
  • 复杂度衡量: 少量顺序代码 < 一些条件分支代码 < 增加少量apollo配置 < 增加DB < 增加DB和缓存 < 增加一个模块。

举一反三

  • 发现一处,解决多处类似的问题,而不是发现一个解决一个。

整合能力

  • 发现多个需求点的关联,综合考虑和合并优化,避免来一个解决一个,导致解决方案比较松散。

设计细则

  • 文档说明: 当设计变更涉及较大变动时,建立文档说明改动点及缘由。
  • API :继承不可超过两层,避免嵌套;避免将不相关的东西混杂在 API 参数中;避免将底层实现细节暴漏在API 参数与传参中。
  • 分层: 提炼出一系列关注点,分离到不同的语义层次,分离到多个类的单一职责中。
  • 组件化: 将工程里的代码与功能实现抽象为组件接口与实现。
  • 策略模式: 使用策略模式分离同一个接口的不同实现,并根据场景选择适宜的实现。
  • 插件流程: 如果流程是可变的,那么将单个流程节点变成可配置的插件,并进行编排。
  • 启动检查: 当应用启动时,加载所有必要组件,任一不满足时及时报错退出,避免错上加错。
  • 使用切面: 当多个功能要复用同一个前置或后置逻辑时,使用切面来实现这些前置或后置逻辑。
  • 受控线程池: 切忌在应用里动态创建单个线程或线程池;使用全局受控的线程池来执行任务。
  • 重载函数: 使用重载函数建立适合的工具类。
  • 无状态: 除非必要,不要在实例间共享状态;不要让请求的处理结果依赖于某个状态。
  • 快速失败: 当前置要件不满足时,快速失败胜于自以为的智能容错处理。
  • 事务: 多个关联操作的原子性和一致性保障。
  • 幂等: 处理多个完全相同的请求时,与处理一次的效果相同。
  • 范式: 在关系型数据设计中,要遵循基本的规范范式。
  • 日志: 在开发时,打印合适级别的必要的日志(关键路径和关键状态),方便快速排查错误。
  • 来源监控: 如果有多个来源或类型,建立监控了解每个来源或类型的业务量及占比。

工程质量

发布准则

  • 【必须】变更代码或者新增代码,有单测覆盖; 捕获异常有异常单测;单测全部通过。

  • 【必须】提交代码Review, 通过应用Owner及另一位有经验的开发同学CR通过; 应当】

    • a. 对于任何CR意见,给予及时改进或合理的回应;通过代码优化及积极沟 【应当】对于任何CR意见,给予及时改进或合理的回应;通过代码优化及积极沟通确保CR及时通过;
    • b. 【应当】非紧急发布,CR在发布前一天完成; 紧急发布,CR与发布时间相隔半天。
    • c. 【应当】核心应用不应当有硬编码(魔数、写死的业务名)和重复代码;想办法消除它。
  • 【必须】有相应的单接口测试用例集合,并且确保QA环境接口测试用例集合全部通过;

  • 【必须】谨慎发布,密切留意日志与监控报警。

  • 【必须】线上发布完成后,至少停留半小时,校验线上无问题方可。

codereview问题

coderview流程

  • 改动点和改动描述: 代码提交者最好说明改动点和准确的改动描述,让CodeReview的同学有心理预期
  • 问题分类与优先级: 常见代码问题 > 可维护性问题 > 更难发现的问题 > 较轻微问题。
  • 熟悉相关业务:CodeReview第一要保证业务的正确性,需要Review者去熟悉相关业务,而不仅仅是从代码层面来推敲。如果改动比较大,还需要推敲设计是否合理,是否划分清晰了系统边界,有哪些遗漏的地方。
  • 影响面评估: 影响面评估非常重要!!!即使只有一行代码改动
  • 有针对性: 比如调用 API 接口,则要考虑依赖服务的可靠性,做好捕获异常的操作和日志记录,避免因局部影响整体功能;

code重点检查

  • 业务逻辑的实现是否未考虑到全局的设计或现有的某些业务细节(对业务不够熟悉的同学往往因为没有考虑到更大的业务范围或细节而犯错)
  • 是否有隐藏的细微错误或潜在的隐患(经验判断)
  • 代码的质量属性,性能、可维护性、可扩展性等,对需求和设计的代码实现方式

项目掌控

  1. 完成预先评估的任务和工作,为整体流程和功能质量提供保障;
  2. 在适当可接受的时间范围内完成,保障项目进度,减少延期时间。
  • 优先级与预先评估。仔细查看各项目的联调、提测、预发、上线时间,确定优先级,错开时间;留出缓冲余地。此时,并不涉及实际开发,而是涉及到评估。 评估意味着:1. 需求理解; 2. 现有实现; 3. 改动点评估; 4. 任务拆分与工作量评估; 5. 时间评估。 评估性的活动,与编程是同等有挑战的事情。

  • 关键点评估。 一定要事先评估好该项目的关键点、重点、难点,识别出风险点,及时报备,并考虑到估时中。

  • 充分开发自测。 要提升联调效率,要充分评估到改动点和开发自测,否则联调和提测的问题找上门来,一样让人吃不消。比较理想的情况是,自己的改动点都在掌控之中,只要部署好了,关注项目进度即可。

  • 时间管理。 为改动点比较大的项目安排整段的时间,专注地解决问题。减少不必要的任务间切换。

  • 申请资源。 再能干的人,多个项目同时挤压过来,也是很可能扛不住的。要及时申请资源,并说明充足理由。 很多事情,需要在做之前就想好。

  • 拓展项。 不局限于自己的模块,而是理解整个项目的技术栈,重难点问题及解决方案,收获更多。