写有价值的技术文档

简介: ## 背景 传统瀑布开发模式下非常重视文档,每个开发环节的衔接都通过文档实现。这种重视在CMMI达到了极致,软件开发的每一步从形式到内容都要求文档化,需要设计者花费大量的精力在文档的撰写和维护上。高度文档化需要投入巨大的成本,这种成本在相对固定,变化较少的问题域(如传统的制造、管理)可以从软件后期的维护收益上得到补偿,实践中也得到了较好的效果。但在变化较多的问题域(如互联网、创业企业),高度

背景

传统瀑布开发模式下非常重视文档,每个开发环节的衔接都通过文档实现。这种重视在CMMI达到了极致,软件开发的每一步从形式到内容都要求文档化,需要设计者花费大量的精力在文档的撰写和维护上。高度文档化需要投入巨大的成本,这种成本在相对固定,变化较少的问题域(如传统的制造、管理)可以从软件后期的维护收益上得到补偿,实践中也得到了较好的效果。但在变化较多的问题域(如互联网、创业企业),高度文档化会造成整个软件生产过程的反应迟滞,进而造成企业竞争力的下降。于是这些要求快速反应,快速迭代的行业逐步放弃了高度文档化的要求,开始追求原型设计、分步迭代以及“代码及文档”。

可是物极必反,实践过程中很多“敏捷”项目却从“高度文档化”走向了“无文档”:需求只有几句定性的描述,一个或几个开发自己鼓捣着就把功能完成了,最终交付的只有一个svn地址,基本没有任何文档,或者只有少数更新不及时的随笔。从结果看,绝大多数这样的项目无论技术上还是业务上,最终都是失败的。个别项目业务需求很大,技术后期满足不了,只能进行痛苦的重新设计和重写,这个过程往往耗时甚多,并对业务有或多或少的影响,背离了当初通过"敏捷"保障业务快速发展的初衷。

作为开发,我们的目标是正确认识文档的作用,制定合适的规范,撰写 必要而够用的文档。提升文档的编写和阅读能力,同时使文档为我们服务,为开发和维护过程创造价值。

文档的作用

  1. 帮助设计者克服恐惧

    面对新的业务需求,尤其是脱离了熟悉"甜区"的需求,设计者的内心多少都会有恐惧。

    新的业务需要全新的设计,需要通盘考虑各种情况如何处理,这个过程中往往还存在很多互相抵触的点,需要设计者作取舍。如果不写文档,设计者在脑海中构建一个初步的想法之后就会动手编码。这个初步的想法一般来说并不全面,但恐惧往往会驱使程序员尽快开始行动,试图看到一些产出,同时也能给(管理)上层一些响应。

    再差的设计者都能想办法满足眼前的需求

    可是未经全面考虑的产出往往有着较大的(设计)漏洞。幸运的情况下这种产出后续会被覆盖和改写成更有好的设计和实现,如果比较不幸,这些有缺陷的产出则会直接把后续的开发带歪。

    写文档能帮助设计者抵御立即动手编码的冲动。因为文档比代码的抽象程度更高,写文档促使设计者从更加抽象的角度思考问题。借助文档的抽象,设计者能从概念,而非实现的角度看待整个系统。脱离了实现细节,设计者更容易发现哪些概念属于错误的抽象(错误的抽象使某个概念和其它概念间存在不合理的依赖或交叉)以及整个设计拼图中有哪些缺失(概念间缺少必要的联系)。通过撰写文档,设计者为自己提供了一幅“全景图”,从而有勇气去作全局的设计。

    不谋万世者 不足谋一时
    不谋全局者 不足谋一域

    软件的设计者规划出模块、接口、服务等一系列概念,由实现者将其变成代码。这个过程中,设计者最重要的责任就是保证所有这些组件彼此间兼容,能够正常通信并实现需求,同时还要考虑到未来的可扩展性。设计者要不停的追问自己“如果发生了某种变化,现有的组件布局是否能够处理?如果不能,是否能快速定位要找到的组件,用最小最清晰的修改承载这种变化”。这个高度抽象的过程,脱离了文档的帮助,直接在代码层面进行效率会低很多,也更容易出错。

  2. 沟通和交流

    作为一个团队成员,仅仅交付功能是不够的,我们要交付的是 可理解,可维护的功能。为了这个目标,我们和各方的交流,把设计思路向他们讲清楚:

    • 设计评审者

      设计评审者通常对项目细节不会非常熟悉,他们关注的是整个项目的核心诉求,技术难点和实现方案是否自洽。他们比设计者(文档撰写者)考虑的更加抽象,看的往往只是几张图或者表格,但这几张图和表格并不会凭空出现,一定是从设计文档中抽象出来的最核心的设计要素。

    • 服务使用者

      按照“对接口编程”的思想,工作的边界应该落在接口上。接口上的文档通常有两类,一类是独立的接口描述文档和示意图,用于团队内部review;另一类是程序内文档(javadoc),作为接口说明(spec)供接口使用者参考。由于javadoc支持HTML,设计时可以先写interface,用详细的javadoc描述接口信息,再用工具抽取成独立的接口描述文档。这样即可以避免两份文档之间不一致,也更容易实现代码和文档的一致。当然,示意图这类更抽象的文档仍然需要手工整理。

    • 服务维护者

      包括进入项目的新同学和(项目交接过程中)的接手者。这些同学需要更加详细的文档,才能了解最初设计者的意图,并在后续设计中保持这个意图

      • 项目中有哪些状态,状态的格式,状态的物理分布
      • 项目采用什么原则进行模块划分,出于什么考虑(如果有多个方案时选择了其中一种)
      • 某些特殊设计是出于什么考虑,背景知识(性能、吞吐量、一致性、复杂性...)

      最后一点尤为重要。很多时候接手的同学通过翻代码能了解作者是怎么作的,但缺乏文档很难去了解作者是怎么想的。如果维护者不知道设计者的思路,再好的设计也无法得到贯彻。如果你作了一个正确的设计并为这个设计骄傲, 务必在文档中说清楚你的想法和目标 ,就像手工艺大师在作品上刺上自己的名字一样。

  3. 衡量产出

    种瓜得瓜 种豆得豆

    衡量程序员的产出是特别麻烦的事。各种衡量方式会带来不同的导向

    • 统计代码行
      这是外包经常采用的指标,统计代码行会造成大量的复制/粘贴。但实际上完成同样的功能,篇幅少的方案往往更清楚,也更易维护。所以代码行不适合我们的需要。
    • 看业务产出
      从更高层次衡量团队贡献时,业务价值毫无疑问是最重要的指标,但衡量单个程序员的能力和产出时,业务价值并不是一个很好的指标,毕竟很多业务因素不是程序员能控制的。
    • 看技术产出
      这要求能明确程序员的技术产出包括哪些方面,比较客观的指标就是看技术文档和代码。由于评审者实际不可能看完一个人产出的所有代码,技术文档就在这里起到了索引的作用。技术文档可以让评审者快速了解

      • 程序员代码中价值最大的部分
      • 设计者思路是否清晰,是否有原则性错误
      • 程序员是否有能力提交工业级别的设计和代码(重点在于合理、可读和可维护性)

      衡量产出时,文档和代码的比重通常会在三七开或者四六开。我们随后的考核中会采用文档占40%,代码占60%这样一个标准

必要的文档

增一分则太长 减一分则太短

我们需要文档,但不需要冗余的文档浪费程序员的生命和精力。我们希望程序员写的每份文档都是有价值的,有信息量的。

目前来说,对新功能需要提供以下设计文档

实体关系图(必选)

实体关系图是对功能抽象程度最高的文档,它包括

  • 新的功能要引入哪些(主要)实体
  • 新实体之间有什么关系(一对一,一对多,多对多,父子,组合,继承,。。。)
  • 新实体和原有实体之间有什么关系

通过实体关系图,可以尽快了解设计者的思路。实体关系图的重点是看 实体抽象是否正确 ,新的抽象能否正确实现所有用例。

//TODO 补充例子

状态设计(必选)

系统设计中很大一个工作就是规划系统状态(数据)的分布,通过状态分布可以大致了解实现能达到的性能、一致性和鲁棒性。这份文档包括

  • 新的功能会新增哪些状态(包括持久化状态和非持久化状态),会对已有状态造成什么影响。
  • 状态的格式(数据库的DDL或者no-sql的json/KV)
  • 状态的分布(集中式,分片,对分片要指明Sharding方法)
  • 状态的一致性方案(对不同状态的一致性需求,实时/定时, 推/拉, 读写分离等)
  • 状态的存取(状态通过什么方式存取和暴露给外界,直接访问,消息,API等)

一般Web Server无状态,系统扩展性多半取决于状态分布,所以需要专门的状态设计文档详细阐述。 状态设计关注的重点是 设计方案能否满足性能和扩展性需求 ,另外对C端系统还要考虑 是否有高可用性方案(放松一致性,提供可用性)

// TODO 补充例子

系统交互(可选)

新功能牵涉到系统交互时,需要提供系统交互文档。系统交互文档重点描述系统间的数据流,这份文档包括

  • 新功能牵涉到系统内部哪些模块,模块内的交互方式(API/MESSAGE/直接访问/etc.)
  • 和哪些外部系统发生交互,包括引入的新系统以及之前有交互的老系统,采用什么具体的交互方式
  • 交互接口是否有限制(性能/吞吐量/稳定性/etc.)
  • 外部系统哪些是强依赖,哪些是弱依赖
  • 数据流图,描述完成特定功能的闭环中,数据在各个系统(模块)间如何流转,从一个模块到另外一个模块的过程中,数据的形式如何转换。

通过系统交互文档,可以从更高的层次了解整个系统的复杂度和依赖。这里的重点是数据流转过程中是否暴露了过多细节或引入了不必要的依赖,评审的重点是数据流图有没有可能简化,将系统间的依赖降到最低

// TODO 补充例子

接口文档(必选)

接口文档是接口两端程序员的约定(Contract),任何需要多人合作的边界上都需要提供接口文档。

前后端接口文档

采用前后端分离的开发模式,前后端接口文档需要详细列明 每一个前后端接口的格式和说明。这个文档一般由前端提供,后端实现。形如

接口名称 listFoo
描述 查找Foo
Request:
{
    "id": 1, //主键,可以为空
    "keyworkd": "abc" //关键词,可以为空,需模糊
}
Response:
{
    {
        "id":1,
        "name": "Clinton"
    },
    {
        "id":2,
        "name": "Obama"
    }
}

后端接口文档

为了便于同步代码和文档,后端接口文档以javadoc为主,评审时抽取javadoc即可。javadoc也可以直接用IDE书写,更加方便。评审的以interface javadoc为主,当然对class/method也能有清楚的javadoc更好。

以下内容必须有接口文档

  • 所有HSF服务的接口
  • 跨开发者调用的接口 (提供给别的开发者使用的接口)
  • 有复杂实现的接口 (实现超过200行)

javadoc的目标不是应付评审,而是让别人了解设计者的想法。以下是对javadoc的一些要求

  1. 20个字以内写清楚这个接口是干嘛的。写完后站在接手者的角度读一下,描述是否清楚。如果没法在20个字内描述清楚,多半就是设计上有问题,不符合单一责任原则,需要考虑下是否要重新设计。
  2. 如有必要,用几句话描述下背景。这个一般出现在有特殊业务背景,进而需要某些特殊设计的场合。通过描述背景,接手者可以了解上下文,知道如何演进现有设计。
  3. 对入参和出参的描述。如果参数本身是专门的实体或bean,代码的类型已经很明确,不需要详细描述。但如果参数是泛类型(Object,集合类)。一定要详细说明具体的值是什么。
  4. 如果采用了特殊的选型或设计者有特殊的想法,要在文档中说明此决定所基于的前提,使用的场景,作了哪些折衷。避免接手的人踩坑。

这里有几个例子,考虑到脱敏,抹去了package name

/**
 * {@link ValveChainAuditor} consists of some valves, each valve may permit or deny an access request independently, an
 * {@link Permission} is granted only when all valves permit the access.
 *
 * @author lotus.jzx
 */
public interface Valve {
    interface AccessResult {
        /**
         * If the valve permit the request
         *
         * @return true if permitted, false else
         */
        boolean isPermitted();

        /**
         * Valve can attach an object to the {@link ValveChainAuditor}, this attachment will be returned when
         * releaseAccess is invoked. With attachment valve can store and fetch state in
         * auditor that itself can be designed as stateless service.
         *
         * @return the state needed when releaseAccess is invoked. Return null if extra state is unnecessary.
         */
        Object getAttachment();
    }

    /**
     * If valve to current time to make decision, current time will be passed in when tryAccess and releaseAccess are
     * invoked, or the now param will be null.
     *
     * @return true if need, false else
     */
    boolean needNowTimestamp();

    /**
     * Return result of access request
     *
     * @param key resource key
     * @param now current time, null if needNowTimestamp() return false
     *
     * @return {@link AccessResult}, can not be null
     */
    AccessResult tryAccess(String key, Date now);

    /**
     * release access
     *
     * @param key            resource key
     * @param now            current time, null if needNowTimestamp() return false
     * @param accessHappened true if all valves accepted the request (commit), false else (rollback)
     * @param attachment     the attachment returned in the {@link AccessResult}.
     */
    void releaseAccess(String key, Date now, boolean accessHappened, Object attachment);
}

/**
 * 数据项操作符,能对数据项进行操作
 * @author lotus.jzx
 */
public interface DataItemOperator {
    /**
     * 操作符知道如何解析数据项上用户需求(的字符串),将其转换为具体对象,供后续使用以及露出
     *
     * @param dataItem   数据项
     * @param attributes 校验用到的属性(最初是validateAttributes+sessionContext.params,经过FilterExecutor处理),
     *                   可以在这里添加需要露出的变量
     *
     * @return 需求对象
     */
    Object parseRequirement(QualificationDataItem dataItem, Map<String, Object> attributes);

    /**
     * 给定以下内容,操作符知道如何对其进行运算,得到资质项校验的结果
     *
     * @param dataItem                资质数据项
     * @param value                   资质数据项(从数据源)取到(经过Filter处理)的值
     * @param attributes              校验用到的属性(最初是validateAttributes+sessionContext.params,经过FilterExecutor处理),
     *                                可以在这里添加需要露出的变量
     * @param parsedRequirementObject 操作符自身parse后的对象
     *
     * @return
     */
    QualificationDataItemValidateResult runOn(QualificationDataItem dataItem, Object value,
                                              Map<String, Object> attributes,
                                              Object parsedRequirementObject);

    /**
     * 是否允许需求为空(一些表单操作符不是从活动上而是从sessionContext取requirement,允许活动上的requirement不配置)
     *
     * @return
     */
    boolean isAllowNullRequirement();


}

接口文档评审的重点主要有

  • 命名是否清楚,interface中的func与其所在的interface是否有 "has-a"关系
  • 入参、出参最小化,尽量针对接口而非实现,模块间暴露最少的信息,便于模块间隔离
  • 站在使用者的角度,说明文档是否易懂,能否无歧义的使用API

单元测试

单元测试也是文档的一部分,尤其是在持续集成中,单元测试除了验证正确性,自身也是一个(始终和代码同步)的说明文档。具体详见 《写有价值的单元测试》一文

此时此刻,非你莫属

由于业务、排期、环境等原因,很多开发都写过"脏"代码,可能也都接手过"脏"代码。每个接手"脏"项目的人都会吐槽没有文档的项目就是一堆坑,每次交接带来一堆问题。可是这样的吐槽并没有实际价值,尤其是在你并没有为项目的文档化作出任何贡献时。

临渊慕鱼 不如退而结网

在我们团队中,我们尝试改变"苦恼没有文档,又不生产文档"的困局,达成以下这些目标:

  • 我们要认识到

    文档并不是负担,而是帮助开发提升效率的工具
  • 我们要实现

    通过文档高效沟通,而不是通过代码低效沟通,提升所有人的效率
  • 我们要作到

    把文档能力作为评价程序员的重要指标。在评价体系中,把文档放到和代码相同甚至更高的位置上。
    

文档化,开始行动吧!

目录
相关文章
|
11月前
|
人工智能 自然语言处理 数据可视化
AI重新定义体育直播
2024年的AI体育直播正以前所未有的方式革新观赛体验:进球瞬间生成多语言解说、预判精彩镜头、实时战术分析、自动生成集锦。AI不仅取代传统导播,还提供风格化解说、情绪化运镜和防作弊辅助,让比赛更智能、互动更丰富。开发者可借助开源工具入局,未来或将实现全息解说与脑机直连,为体育注入科技魅力!
2025广东省增值电信ICP、EDI许可证申请流程
2025年广东ICP、EDI许可证申请流程包括工商注册、域名备案、网站搭建、材料提交、审核及领证等环节。企业需满足注册资本、人员社保、服务器部署等要求,通过工信部平台提交材料,经初审、复审后领取电子证书,并按时完成年报及维护工作。
|
人工智能 程序员 API
写作即思考:工程师如何用技术文档完成逻辑的『认知复利』
技术写作:在程序员的世界里,人们常说“Talk is cheap, show me the code”,但随着生成式AI的普及,“Code is cheap, show me the prompt”正悄然改写规则。
软件工程师,如何有效缓解工作压力
在这个快速迭代、技术日新月异的数字时代,软件工程师们常常站在技术创新的最前沿。他们肩负着构建高效、可靠软件系统的重任,同时也面临着紧迫的截止日期、复杂的技术难题和持续的学习需求,这些因素共同构成了巨大的工作压力。如何在高压环境下保持冷静、高效与创新,同时维持良好的身心健康,成为了每位软件工程师必须面对的课题,也成为了这一行业亟待解决的问题。
664 57
|
10月前
|
缓存 索引
基于Reactor模式的高性能网络库之缓冲区Buffer组件
Buffer 类用于处理 Socket I/O 缓存,负责数据读取、写入及内存管理。通过预分配空间和索引优化,减少内存拷贝与系统调用,提高网络通信效率,适用于 Reactor 模型中的异步非阻塞 IO 处理。
318 3
|
数据采集 机器学习/深度学习 人工智能
数据驱动智能,智能优化数据——大数据与人工智能的双向赋能
数据驱动智能,智能优化数据——大数据与人工智能的双向赋能
759 4
|
存储 传感器 监控
物联网:物联网卡为什么没有语音功能
物联网卡(IoT SIM卡)主要是为物联网设备设计的,这些设备包括但不限于智能城市传感器、可穿戴设备、工业监控设备、车联网设备等。与普通的消费者SIM卡相比,物联网卡在功能和设计上存在一些显著的区别,其中不包括语音功能是其重要特点之一。以下是物联网卡没有语音功能的几个主要原因:
|
存储 弹性计算 分布式计算
阿里云服务器租用价格:包年包月收费标准与月付、1年、3年活动价格
租用阿里云服务器3个月、6个月、1年、3年多少钱?云服务器收费标准是怎样的?根据目前的价格信息,阿里云特价云服务器价格38元、99元、199元、298元,本文分享阿里云服务器最新的租用费用,包括包年包月的收费标准和月付3个月和6个月以及1年、3年活动价格表。
|
网络架构
YOLOv5改进 | 2023主干篇 | 利用RT-DETR特征提取网络PPHGNetV2改进YOLOv5(超级轻量化精度更高)
YOLOv5改进 | 2023主干篇 | 利用RT-DETR特征提取网络PPHGNetV2改进YOLOv5(超级轻量化精度更高)
933 0
|
Java
Java中判断String类型为空和null的方法
Java中判断`String`变量是否为空或`null`需用`== null`和`.isEmpty()`。示例代码提供两种方法:`main`方法直接判断,`safeGetString`方法提供默认值。当输入为`null`或空时,返回默认值。代码包含三个测试案例,分别处理`null`、空字符串和非空非`null`字符串。
1127 0