极限开发的尝试:需求文档、TDD和代码实现

2021年3月8日16:41:23 发表评论 1,025 次浏览

本文概述

前言

极限编程简介

使用极限编程方式的开发,通俗的来说,其主要目标就是让开发更快、更顺利,并降低成本。更具地体,对于开发者或者程序员来说,知道怎么写代码,写得更快,满足需求又能尽快完成任务。

本文尝试谈谈极限开发,主要也是为了能够接近这个目标。对于我本人来说,我也在尝试找到更准确的开发方式,考虑的因素有:时间、成本、效率和质量。本文尝试以极限开发为目标,结合开发经验,谈谈实现它的可能性和一些可行的方式。

产品设计:需求文档

要开发任何一个项目,首先有一个IDEA,然后开始概念化这个产品,并开始着手分析该产品的所有需求,并得到一个详细的设计文档。

做这个产品需求文档需要的技术并不高,使用的工具主要有:画图工具、原型设计工具和UI设计工具,画图工具是供你自由分析的,原型设计工具例如有Axure,UI设计工具有Sketch。其输出结果主要有两个,一个是产品需求文档,使用文字(或结合表格)进行说明,另一个是产品原型图,原型图提供产品的最终实现效果。

个人建议,一般将产品需求文档及其相关的内容全部放到Axure中,这样你就不用一下子看下原型图,一下子看下Word文档。整个文档一般包括:

  • 概念化:产品的最初阶段,分析和描述清楚需求。
  • 简介:包括产品的简介,该文档的一些版本信息,版本管理建议使用Git。
  • 结构图:包括产品数据结构图和页面结构图,数据结构图描述该产品要用到的所有数据,包括其逻辑结构;页面结构图描述该产品界面的逻辑结构。
  • 页面交互原型图:文档的重点,一般逐个页面地做,完成后在对每个页面进行详细分点标注。这样做的好处是:后面不用再重新做一份需求文档了,所有需求就参考交互原型图就是了。
  • 产品用例图:非重点,描述一下用户对产品的使用示例,但是原型图的标注其实也可以体现出来。
  • 功能流程图:非重点,描述产品使用的整体逻辑结构。
  • 编码设计:具体的编码设计,主要是设计数据结构、类和接口。

但是需求文档有什么用呢?它是整个项目开发的主要依据,其它或后面的所有东西都要依据这个文档来进行——没错!是必须的!为什么这样说?因为如果你尝试在原型设计或其后的任一个阶段中改来改去,那么整个开发将会变得非常混乱,而且开发完成时间遥遥无期,没人能承受得了。

所以,要记得需求文档要尽量做好,交互原型图尽量做详细,每个标注要分点描述好,这个项目有什么新东西也要想好。一旦确定了,在开发新版本之前都不要改动它了,文档怎么说就怎么做,谁改谁负责!

需求文档就像一个最高命令,其他人都要跟着它的标准来做,或像一个灯塔,看着它来做,不然你会迷失方向!有人试图不按照文档来做,后患无穷。所以无论大大小小的项目,一定要记得:做好需求文档!

它的重要性在下面会具体提到,这是极限开发的第一步,值得多花一些时间去设计它,或学一些相关内容。

原型设计的简单例子:用户注册

原型设计的简单例子:用户注册

如上图是APP用户注册的原型设计,首先是界面的所有必要元素,这些元素负责完成这个界面的主要功能,至于其它一些你可能喜欢的功能,例如加一下动画效果什么的——不是必要的,为什么这么说?因为我们很容易在这些好看的东西上耗费过多的时间,不是说不能添加,而是说:先现实主要功能再说,而且建议还是简洁为好。

完成界面必要设计后,我们就要对界面元素进行详细分点标注,分点标注能够把这个界面所需的所有需求都体现出来了:图文并茂。以后我们主要参考这个原型图进行各种设计。

上面是一个界面的原型图,一个项目可能有很多界面,都是类似这个界面这样做的。个人经验看来,难的地方不是制图,而是把它描述清楚。首先是想到需要这样的功能,但是它的界面布局应该怎么样呢?然后是它是这样布局,但是它的操作逻辑是怎么样的呢?这就需要个人好好思考,发挥想象力了。

TDD:测试驱动开发

TDD:测试驱动开发

TDD是极限开发的主要方法论,是我目前看过最好的设计方法论(当然有和TDD类似的一些变体),它可以让你从代码海洋中脱离出来。

TDD的操作步骤如下:

  • 在拿到一个项目代码之前,先执行所有测试。
  • 在要实现某一个功能或添加任何代码之前,先写测试用例。
  • 写好测试用例,再一步步往下重构代码。
  • 重复以上步骤。

看似很简单!但是有些程序员可就是不想服从这个标准,自己想到哪里就写到哪里;或者想参考这个标准,却不大做得到。

对于前一个问题,个人深有体会,很久以前我也是不用TDD的,采取的是从下而上的设计方式,就是和一般人的那样:先想下怎么实现,实现了再写测试。等下就会发现:怎么我写的接口不方便测试的?或者不知道写到哪里去了,或者接口写得不标准。

而后一个问题,我的建议是,个人在编程行为上要有一个习惯,就是用使用的角度开始设计代码(下面会有一个例子)。先写测试用例,写测试用例一般是在设计一个接口,这个测试用例的依据就是需求文档了,用例的输入和输出分别是什么?看原型图和标志说明就一目了然了。

写好测试用例(设计好接口),就接着往下重构代码,尽量不要回头改动这个接口了。我个人觉得在学习的时候可以一步步写下代码运行测试,但是真的开发我觉得是没大必要。其原因:某些代码我们已经重复写过了,并且我们需要考虑数据结构和算法设计的问题,在测试中添加相应的测试用例就行了。

但是这样做,我发现是有点问题,添加用例覆盖相应的代码感觉不准确,并且写了多余的代码,测试用例并不能覆盖一些代码。这好像又忘记TDD了,还是其实我只需要考虑用例的这个接口就行了?

重构代码的接口设计

想一下,实现一个需求设计一个接口,不断往下重构这个接口的实现代码。首先你会发现似乎那么多代码,就这一个测试就行了(多个输入覆盖例如在JUnit5中有个批量参数化测试),这是没问题的,你可以这样做。

但是往下重构有一个问题,例如你可能用到网络或文件相关的数据和操作。那么这时参考的代码设计原则是什么?这个最好的方式应该是使用设计模式(例如Java中的12种设计模式)。

我目前的方法是在这里进行分包或分小模块,让一些一系列的类负责一方面的功能,例如HTTP请求、Socket请求、文件操作、数据库等。这样就不必要在一个类中写完所有的实现代码,因为你可能会发现这些代码在后面的实现中又需要重新实现了。

往下重构代码,其设计原则也是使用TDD:先设计接口,依据是数据来源。例如可能需要让一个模块专门处理一种类型的数据,那么这时需要按照设计接口的方式:new一个类,并调用一些函数——用以完成当前步骤的功能。

还有一种不需要依赖特定类的功能,则有可能放到一个类似xUtil的类中,就是常用的工具类了。除了这些,还有就是抽象类和接口的设计了,这就要依靠你当前的知识和经验来设计了。

TDD开发实例:用户注册

GUI开发的代码结构一般就是MVC或MVP,若你还不懂可以查看相关的书籍和博客的介绍。由于我们是使用TDD开发,创建一个界面用于测试,其成本太大了,所以最好是可以不依赖界面进行测试,那么使用MVP是可以做到的。

当前注册界面,例如在Android中,可以对应一个View,Activity或类似的View元素,IView用于和Presenter/Controller交互,Model是数据服务层,只和Presenter交互,Presenter是业务逻辑层,所有需求的表面逻辑都在Prenseter中。至于Model我觉得可以自由一些,分包主要在这里体现出来,它主要提供数据,例如数据库、文件、HTTP或一些复杂的服务。

那么对于用户注册的这个需求如何实现呢?

  • 首先,原型图的标注有5点,第一点是可以忽略的,logo和名称只在布局的时候参考。其它4点可以作为4个小需求。
  • 将这些需求再重新重新分类,分为输入和输出,也就是我们设计这个接口,需要提供什么样的输入,提供什么样的输出。
  • 界面的输入有:手机号码、图形验证码和6位手机验证码,界面的输出有输入框的红色和绿色提示、图形验证码刷新、发送验证码按钮更新。
  • 界面的输入作为Presenter函数的参数,界面输出使用IView的函数返回。

我们都知道,这里面要用到手机号码验证或验证码验证,比如一边输入一边验证。另外,点击注册按钮也同样需要调用验证逻辑的,那么这个界面目前只有一个主要的接口,就是登录,测试用例可以这样写:

@ParameterizedTest
@CsvSource({"
    "122334, 34T3, 594723",
    "122334, 34T3, 594723",
"})
void testLogin(String phoneNumber, String imageCode, String phoneCode) {
    ILoginView view = new LoginView();
    ILoginPresenter presenter = new LoginPresenter(view);
    presenter.login(phoneNumber, imageCode, phoneCode);
}

其中这个ILoginView是一个接口,这个接口一般用于负责程序输出,一般给当前真正的view实现。但是你可以看到这个View接口不依赖于真的UI View,显得非常轻量。

接着,我们来看看这个View接口的具体声明:

interface ILoginView {
    // 更新手机号码输入框
    onPhoneNumberError();
    onPhoneNumberRight();
    // 更新图形验证码和输入框
    onImageCode(Image newImage);
    onImageCodeError();
    onImageCodeRight();
    // 更新6位手机验证码输入框
    onPhoneCodeError();
    onPhoneCodeRight();
    // 更新"发送验证码"按钮
    onPhoneCodeWaiting(int second);
    onPhoneCodeFinish();

}

你可以看到,这个接口的所有函数都是用于更新界面的,这些都是作为输出的数据。

设计接口的依据同样是需求文档,这些设计不是我随便想出来的,不是想学编程时的那样,随便设计个函数好看的,这些设计都是正式而必须的。

设计测试用例的时候,那是最主要的需求接口,其参数也是依赖于该界面的输入数据类型和数量。除了输入的数据,另外就是输出的数据——程序或系统的一切响应都是输出数据。

下面我们来看下Presenter接口:

class ILoginPresenter {

    void login(String phoneNumber, String imageCode, String phoneCode);
    void validate(String input);

}

其中login函数是主要的,validate函数非必须,除非你一定要实现边输入边验证的需求,那可以把所有验证逻辑都放在validate中进行重用。

但是话又说回头了,我的需求文档中可没说要实现这个,所以不要管它了。

值得注意的是,设计完后,在你开始往下重构代码的时候,还有一点要注意的:检查需求文档,什么意思?就是对照你设计的代码和需求文档,看看这样的代码设计是否能符合需求文档的要求。这一点非常重要,如果你明白并且经常这样做,你大概就能懂得需求文档的强大作用了。

技术的难点:代码实现

以上:需求文档或TDD开发,仅仅是好的开发的第一步,或者说这给我们开发提供了一个好的开头。但是不管我们是否使用TDD,如果不能实现需求文档的需求,那么所有这些工作都显得无意义。有人可能说:我追求这个过程,但是必须现实地说,不能在代码上具体实现需求没有说话的权利,生活没有保障!无收入!

所以,有的人写的代码很丑,但是他能完整实现需求;有的人写的代码很漂亮,但是却不会具体实现。哪个最好?当然是前者了,不管什么,能实现才是最终的目的。

就个人经验来说,代码实现上主要遇到的困难有:

  • 选择第三方框架,不知道该选哪个,发现这个会耗费一些时间,选择困难症。
  • 处理复杂格式的文档,例如DOCX或PDF。
  • 处理一些更为复杂的数据,包括图片、音频和视频。
  • 使用一些复杂的框架,例如Spring或Hibernate。
  • 实现一些人性化的操作,例如内容识别,自动识别等,这里需求通常需要人工智能,或者自己实现,其结果是不大令人满意的。
  • 处理多线程任务,例如线程同步,线程间通信。

我学过一些数据结构和算法,所以就算法上的使用不会造成很大的困扰。我觉得最困难做耗费心机的倒是第三方框架了,首先是选择哪个框架。通常我会使用百度、谷歌和Github查一下,这么下来没准要半天或一天都找不到(要实现复杂或比较边沿的需求),这个过程非常烦躁。因为你在找的过程,这个框架到底能不能实现这个需求,你得至少看一下对方的文档,或一些使用示例。

目前可以建议减少时间的操作是:不要用使用百度和谷歌查框架,使用Github查框架——一个好的开源项目,肯定可以在Github上找得到;第二个是看该项目在Github上的Star数量和项目的最近更新时间,多人Star的表示是有不少人用,频繁更新可以说明该项目在不断改进。

另外一个就是类似Spring这样的强大框架了,因为大框架涉及到一般项目的方方面面。当然目前很多用了Spring Boot,困难度会下降,这个需要不断的使用和学习,积累更多经验,总的来说不算很困难。

最困难的地方:文档、音频、视频和图片处理,如果你不涉及这方面的工作,那么恭喜你,你的开发可以很顺利。为什么这么说?因为除了这四种文件外,其它处理的数据都是很简单的,至少不需要专业的知识,这个问题留在下面详细讨论。

最头痛的地方:第三方API

如果有什么让我特别头痛的,就是使用第三方API了,目前我遇到第三方API的问题,主要有:

  • 没有使用文档,或者文档很少(API Docs除外)。
  • 接口不规范,让使用者觉得很迷惑,有非常多冗余的操作。

就这两个问题?看起来很简单,但是问题非常大。如果一个第三方库只是处理一些简单的东西,或者只是对JDK的相关功能进行了一些扩展,例如类似OkHttp或Apache Commons IO,这就还好,因为我们在学习语言的时候已经涉及过了,大概知道怎么用的了。

但是,如果是一些需要处理复杂功能的第三方库缺少文档,这大概是一个噩梦!要知道,这个库不是使用者设计或开发的,没人为了使用要把整个开源代码都研究一遍。必须提一下,Apache的某些项目也是没有文档的,真是气死!没有文档可怎么用?我又不是神,可以预先知道这些API的用法,心里一万句MMP,特别是当实现这里需求的库非常少,而我又最好使用这个没有什么文档的库的时候,我目前就遇到不少。

所以,选择第三方库的时候,除了参考该库在Github上的一些指标外,例如Star数,提交数,最近更新时间等等。还要看一下该库是否有完整的使用文档,文档完整的库优先使用。即使你有该库的基础知识,你仍然需要文档,因为我们不是神,除非设计者提供一个完整的使用文档。

另外一个需要叨唠的问题是:接口不规范,即使是Apache的一些项目,我都发现有这个问题。所以这就是为什么我使用并推荐使用TDD了,一般来说,使用TDD开发的接口是最符合使用习惯的,除非你在乱设计。如果API设计是不考虑用户使用的,那简直是喷血!

我举个例子:使用Object作为一些数据对象的基类,例如如果我们要封装HTML元素,一些设计者会使用Object作为所有元素的基类,然后才是Div或Img。这时候如果没有文档,或者文档不怎么说明,简直头大!即使我想将Object转为具体类,我也很难一时找得到(特别是没有文档的时候),Object是所有类的基类呢!

要是我的建议:不要使用Object作为多种同族类的数据类型的基类!使用一个自己设计的统一基类,例如封装HTML元素,可以将这个基类设计为Element,它有其它元素的共同内容:例如标签名和属性,然后才是DivElement、ImageElement。这样的另一个好处是:在查看API DOC的时候,可以在上面看到Element的所有子类,然后你就可以知道,哦!原来是这样使用的。

问题在这里:数据结构

再次梳理上面谈到问题,其中最重要的就是这两个:

  • 文档、音频、视频和图片的处理。
  • 使用的API无参考文档。

对于第二点,我觉得是强制的、必须的,这个没有解决的办法。如果选择了一个库,那么首先就是查看参考文档,看是否能实现目标需求,然后再看一下文档中提供的用例,有必要的话可以详细看一下文档说明。

最难的就是第一点了,我目前就在文档处理上遇到一个难题。按照目前的经验,可以想到,处理这类问题不顺利的原因是:因为我对这些数据的基础知识不够了解。例如图片处理,虽然我偶然学过一些,但是相对于HTML文档的处理来说,我对图片的基础知识是相当缺乏,其它的音频、视频和文档类似。

文档、音频、视频和图片都是计算机的文件,它们以一种特定的文件格式储存在计算机中。文件格式是非常重要的,不同的文件格式,其储存的方式相差很大,例如TXT文本或HTML文档,这种储存还是很简单的,要解释它们基本不会花费太多力气。

但是如果要处理更为复杂的文档,例如DOCX或PDF,那就不同了。处理图片也是类似,这类文件通常是将各种简单的数据组合存储起来,例如文件可能会有一些文件头的信息,数据段分片储存等等。

假设我们使用C或C++原生读写这些文件的时候——是不是开始感觉头脑空白了?很明显,这是不能像处理TXT那样的,首先文件的格式的标准而文档的,第二,文件储存的主要数据通常不是原文,而是可能经过压缩了的,或者直接储存了字节码的数据。

这就需要我们对某一类格式的文件有专业的知识了,一种文件格式可以准确地对应于一个标准的数据结构,定义或开发该文件格式的公司或者使用者都是依靠这个数据格式来开发的。例如PDF来自Adobe,Adobe定义了PDF的标准形式,首先是数据结构的形式,然后是各项数据表征的意思和作用,最后是该数据结构一些操作上的说明(算法)。其它图片、音频或视频的格式也是类似的,虽然不一定来自某个公司,但可能是来自某个组织,由其定义标准并发布。

所以,这其中最重要的就是数据结构了,个人经验,遇到让我不知道如何使用的第三方API,除了没有参考文档,就是不懂这个数据结构了。为什么不说算法最难呢?相信我,如果你能预先了解对应的数据结构,你就不会实现不了这个需求。而算法难的地方在于实现一个接近O(1)或O(N)的算法,当然难了。但是,像前面说的,即使你实现的算法是O(N^6),那你也实现了不是吗?目的也达到了,比实现不了的程序员,相差一个天一个地。

回头说数据结构,你有没有发现数据结构类似一种东西?协议,例如HTTP或FTP协议,或者你定义的任何一种协议。协议通常是一种约定的标准数据通信交换格式,可以把它想像成一个模板,这个模板的结构是固定了的,例如头部尾部中间,实际数据,加密算法等等,都是标准限定了的。

有了数据通信协议,读数据的人知道怎么读这个数据——使用这个模板,写出数据的人知道怎么写——使用这个模板。而数据结构就是协议了,一个特定格式的文档的数据格式最初由一个公司或组织定义,最后无论是这个公司还是其它开发者,都是使用这个数据格式进行开发的。

那么,遇到类似这样的复杂数据格式的时候应该如何处理?(前提是你缺乏这方面的知识)

  • 首先,找到该格式的定义公司或组织的官网。
  • 在该官网上找到该格式的参考文档(看,参考文档又来了)。
  • 查看参考文档,并分析出该格式的数据结构(或文件结构)、一般操作方式,以及注意事项。
  • 尝试使用原生代码解释该格式的文件,或者尝试使用一些已经封装好的第三方API对该格式的文件进行处理。到这一步你就会发现,并不是算法复杂或者不懂编程,而是缺乏对这个数据结构(文件结构)的专业知识。

有人可能会说,我用第三方API不就好了?这就不用学习这些底层的知识了。除非你只需要处理一些非常简单的需求,否则不会这些底层知识是不行的。即使是使用第三方API,很多时候不了解这个数据结构也并不真的用得了这个API。

总结

最近在开发项目,本文是尝试把我在之前开发中遇到的问题一一详细说出来,并提出一些可能的解决方法。写文章是一个好的方式,任何问题和知识,如果不能使用语言详细描述出来,那表示我不知道自己在做什么——来自语言分析哲学。

首先是产品设计阶段的需求文档,因为我逐渐发现需求文档发挥了强大的作用,所以这里特别想再次讨论一下,可能以后还会有更特别的讨论。

然后是TDD,因为我发现我在使用TDD开发时,发现在往下重构代码的时候,代码会写得有点乱,后来发现重构的时候考虑分包可以解决这个问题。

最后是代码实现,上面也谈到了个人的最大问题:处理特定格式的文件,我想我有空需要学习一下相关流行的格式的文件结构。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: