回首萧瑟处——软工学期回顾总结

试回答当初的疑问

提问题的博客:软工个人博客作业:阅读、提问与一些调研

但是经常地,用户并不能清楚地描述自己的需求,甚至会不断产生新的需求,因而长久不能达到“满意”。这导致每两周一次的迭代可能被大量重复,中间大量旧需求被新需求取代,因而需要推翻旧代码,这变相拉长了整个开发周期并造成了人力物力的浪费。我认为或许需要进一步对“用户满意”加以进一步的规范与限制,使得已经陷入病态的软件项目可以被及时终止?

是的,实际上正是因为可以近似地量化“用户满意”的程度来确定某些开发中的功能是否在逐渐取悦用户,我们才有机会采用这种开发模式。简单的用户行为分析,比如驻留时间、使用时间,常用功能等都可以帮助我们追踪迭代取得的成效。另一方面,用户的主观反馈永远是可取的参考。

然而,就我所知,目前大学计算机专业的课程设置很少系统地涉及商业与管理学,而开发人员在正常工作中对这两方面的接触也较浅显。如果想要成为这种能在两种语境中自如切换的,应当如何规划自我提升的路线呢?

多读书,多看报。实际上很多软件工程著作都是和管理学分不开的,比如经典的《人月神话》和《The Pragmatic Programmer》,这些书籍都从个人到团队角度细致地刨析了软件开发中的管理学精髓

但是另一方面,敏捷开发又鼓励日会制度,这两条经验似乎是冲突的。目前我实习所在的小组有周会汇报制度,主要内容也是总结工作,需要占用掉整晚的时间实现这种组内同步,并且为了准备会议材料实际上要耗费更久的时间——我理解的日会制度或许也会存在类似的问题。请问应该如何看待这种冲突,日会制度是否有反而降低团队开发效率的风险?

实际上开会并没有想象中的占据精力。另外在本学期的实践中,包括我所在的两个开发组在内的许多组都采用比较灵活的日会制度,让开会服务于协作而不是为了开会而开会,这实际上是非常高效的。

关于反思会:因而我的理解是,这样的会议讨论要点无非:1. 回顾已经出现的问题并总结可能的改进方法;2. 汇总各种建议并决策是否执行;3. 要求全员积极参与。看起来与日会同步团队进度的功能并没有本质的差异,为什么一定要单独分离为一个阶段性会议而不将讨论内容并入时效性更强的日会呢?

现在我更倾向于认为,反思会就是一种特殊形式的日会,它为满足阶段告一段落后团队自然形成的对总结与规划的需要提供了一个媒介。

新的问题

  1. 在敏捷开发的迭代中,每个小团队内的开发都是高度自洽的,但假如涉及跨团队的合作需要,应该以何种形式去协调与管理呢?大公司内各个事业群间的协作,使用敏捷开发的最佳实践可能是什么样子的呢?
  2. 对于敏捷开发的模式,团队成员对新问题、新技术的学习期会被接踵而至的新需求挤压,如何能够让团队成员在完成需求的同时保持学习?如果我参与一个采用类似模式进行迭代的团队,我应该如何在这个过程中保持自我提升而不止步于舒适圈,陷入机械性的重复劳动之中?

新知与心得

分阶段讨论。

需求。如何确定需求其实远远比想象的要难,因为对于一个中小规模的团队(20人以下),即使考虑团队中所有人的需求差异,也很难完整覆盖动辄百人、千人乃至(更常见的)更大数量级的目标受众的需求。因此,用户在没有用到产品的此时此刻、在点进下载页面的此时此刻、在注册账号的此时此刻、在学习如何使用杀手功能的此时此刻、在收到互动反馈的此时此刻、在一次使用后关闭软件的此时此刻在想些什么,对于重新理解自己正在开发的软件永远是最重要的(关键在于这是用户的想法而不是开发者的,这不能靠拍脑袋决定)。本学期的学习中涉及了良多方法调查用户需求,比如焦点小组AB测试等,同时也学习了NABCD模型来分析需求,这些先进的工具都是在解决这个重要的问题。而我所参与的两个项目在开发中也从用户反馈的需求中尝到了甜头——当开发者对软件的最初的灵感随着功能一步步完善而消磨殆尽之时,用户就是新的取之不尽用之不竭的动力源泉。

设计。在实践中我的感悟是,好的设计永远是高内聚低耦合的。这简直是工程学(对,不只是软件工程)的六字箴言,写代码时心里默念这句话的效果吊打般若波罗蜜多心经。让我们再重复三遍:

高内聚低耦合。

高内聚低耦合。

高内聚低耦合。

关于这个设计思想带来的好处,我已经在此前的博客中简单给出了个人理解。在团队项目的实践中,这六个字又时刻带给我便捷与——最重要的——快乐。

  • 前后端分离开发本身就是也就对高内聚与低耦合的思潮。想想以前的django服务器端渲染应用的开发,MV和T纠缠在一起,很难想象程序员可以被专门划分为“写逻辑的“和”搞界面的“。没有对业务的分层设计解放前端程序员,也很难有如今庞大的前端社区,不会有百花齐放的开发框架与UI库(至少,或许很难达到如今的高度),我们这些一知半解的本科生又要多熬几天的夜才能写出好看的界面呢?
  • 内聚的模块方便设计黑箱测试。低耦合的模块也方便黑箱测试。大量的数据驱动的黑箱测试方便自动化,自动化方便程序员。

但这些事情说起容易,却很难随着项目推进时刻被把控。一旦设计失控,往往需要花费大量的成本(代码重构)来掰回正轨(更糟但更常见的情况是得过且过,渐行渐远)。学习到的关于规格制定这些行为规范可以更好地帮助我们避免类似问题。

实现。软件开发不是算法竞赛,代码实现要为程序员服务,软件实现要为用户服务。从代码实现的角度来看,重要的是代码能时刻令自己(和未来潜在的维护者)知道自己在干什么。在性能瓶颈之外,永远使用优雅可读的实现方式。在性能瓶颈之中,尽量使用优雅可读的实现方式。优雅的代码令人精神愉悦从而思路清晰。尽量把令人作呕的复杂逻辑封装在一个函数内,相邻的几行代码比满篇的鬼画符要更容易令人摸清脉络。从软件实现的角度来看,不要高估用户的智力,也不要低估用户的智力——比如弹出提示框的时机和内容要拿捏好

  • 在用户操作成功时应当给出合适的反馈(不要再玩命按那个按钮了,你已经成功删掉了这条记录)
  • 在用户遇到错误时给出他能够理解的解释并尽可能为他提供简单有效的解决方案(你的奶奶更容易理解“401:鉴权失败”还是“登陆失败,错误的用户名或密码,请重试”?)
  • 永远不要在用户没有开启--verbose选项时喋喋不休(“恭喜你白痴,你刚刚把鼠标移到了正确的按钮上”,“好的现在你又移开了”)

测试。及时行乐,及时测试。正如《The Pragmatic Programmer》中的呼吁,无处不在乃至丧心病狂的自动化是一种十分舒适的测试实践,尤其利于边界条件覆盖和回归分析。之前我总是认为好的测试成就好的工程,但在实践中我发现好的工程也能成就好的测试。对于合理封装、脉络清晰、充分复用的代码,测试逻辑可以借助代码复用同样做到清晰、简单。这对于开发者并不容易把控,但一旦形成了良好的开始便形成了良性的循环。这里首先体现的是规范的重要性。试想,毫无接口规范的团队,对不同网络接口时的测试逻辑可能是这样的:

def test_api_a():
    # test auth
    r = req.post(...)
    if r.status == 400:
        if r.json().get('msg') == 'Auth failed!':
            ...
        else:
            ...
    else:
        ...
    
    # test function
    r = req.post(...)
    if r.status == 200:
        if r.json().get('msg') == 'object not found':
            ...
        elif r.json().get('list') is not None:
            ...
        elif r.json().get('obj') is not None:
            ...
        else:
            ...
    else:
        ...
    
    # other cases
    ...

def test_api_b():
    # test auth
    r = req.post(...)
    if r.status == 401:
        ...
    else:
        ...
    
    # test function
    r = req.post(...)
    if r.status == 200:
        if r.json().get('message') == 'success':
            ...
            if r.json().get('result') is not None:
                ...
            else:
                ...
        else:
            ...
    else:
        ...

如果有20个这样的接口测试,测试人员可能会花费大量时间检查每个接口的边界情况是否都覆盖了(这不能靠简单的覆盖率报告解决,因为即使覆盖率100%也不能保证边界情况被覆盖)。而假如网络接口设计合理,团队间存在良好定义的接口规范,测试代码可能只需要这样写:

@auth_required
@permit_required(perm=['admin', 'teacher'])
@allowed_methods(methods=['GET'])
@single_obj_query(cls=Student)
@no_side_effect
@repeat(n=3)
def _test_api_c(cases):
    pass

def test_api_c():
    _test_api_c(req_cases(meta=multi_case_generator('cases/check_student.txt'), repeat=3))


@auth_required
@allowed_methods(methods=['POST'])
@side_effect(obj_created(cls=Student, name='case.meta.schema.student_name'))
@repeat(n=10)
def _test_api_d(cases):
    pass

def test_api_d():
    _test_api_d(req_cases(meta=random_multi_case_generator(schema={'student_name': 'str'}), repeat=10))

进一步地,上述代码显然可以简化成配置文件测试样例池和仅仅几十行的测试引擎

另外在参与ddlkiller团队开发时,我参与测试时还遇到了一个新的需求,就是将一些不适合自动化测试的逻辑从代码中摘除。这是个很有趣的测试工作,最后我的解决方案是使用python的mock.patch实现对相关逻辑的动态覆盖,可以查阅之前的技术博客(TODO放链接)。这是我第一次把这个trick用在实战中,也算是学习并实践了一个小的知识点吧。最后,一份详尽的测试报告,既能帮助对测试覆盖情况的有效复审,又让我们易于发现哪里的测试还比较薄弱,进而可以进一步分析:是这里的测试本身不够充分,还是这里的代码味道坏了导致测试难以开展?

发布。在本学期中的实际感悟是,发布中很重要的一个环节是宣传需要利用一切条件让自己优秀的项目为人所知(当然在实际生产中这一块主要是交给销售和市场团队执行,开发团队的主要任务是把产品的利弊良好地介绍给销售同学和市场同学,或许还有PR同学)。

宣传是为了发展更多潜在用户,而黏住用户的前提是软件做好了准备,但是又不能等软件完全做好准备再发布,因为

  1. 不知道会出现什么问题,永远也不能做好准备
  2. 会错过时机

所以采用分阶段发布,先发布一个几乎做好准备的版本(alpha),告知用户这个版本潜在的问题并作出良性承诺,基于这个版本的反馈给出更好的版本(beta),从而实现发布的两级(或者三级、多级)入轨。

维护。软件的维护和开发是分不开的,快速迭代的开发实际上就是一个以战养战的过程。减少维护成本在我的实际体验中无非就是两条大路:规避问题与及时排查问题。规避问题是个大学问,与上文的设计、实现与测试环节都是分不开的,好的设计与实现就是对问题的最强规避,毕竟如果每一行代码都平易近人,隐晦的问题就很难出现,即使偶尔有漏网之鱼还可以被测试拦下来。排查问题则需要先发现问题——这通常源自用户反馈,然后最重要的是快速定位问题,这十分考察开发者的个人素养(项目熟悉程度、嗅觉、测试工具熟练度,以及发散思维),最后再解决问题并做回归测试,这通常是最简单的。

最后

感谢先后参与的两个团队:敏杰开发团队与软软软团队中所有的同学,本学期在和他/她们的愉快合作中充实度过,着实收获了硕果。感谢课程组全体老师助教们的辛勤付出与指导,为我撩开了软件工程学的神秘面纱。

英文有个词叫做cutting-edge,表示“前沿的、最先进的”的意思,我一直认为这个表述十分有趣。从专业学习的角度,大三的确就是整个大学的cutting-edge,再经过一年毕业设计的实战,我们就理应走入学界/业界写真正能够创造价值的代码,而不是hello world了。在这个节骨眼上被安排一学期的合作与思辨,我认为应当充分利用这个机会沉淀经验,总结方法。回顾这个学期,我既初尝PM,站在管理者的角度重新审视了原本熟悉的开发的每个流程,得到了许多新的感悟,同时实验了很多本没有机会实践的想法(从开发到协作的很多小细节),又在后半个学期重新拾起键盘,以开发者的身份体验团队协作的节奏。无论结果还是过程,自我评价都是满足的。希望接下来能够把这些经验真正地吸收,藉此写出有深度的代码,做有价值的工作,贡献有意义的研究。

posted @ 2020-06-17 21:05  MisTariano  阅读(291)  评论(4编辑  收藏  举报