如何更好地管理技术团队?

前言

PM与TL的区别

其实一个Team Leader的职责与Project Manager相像,但Team Leader更着重于技术开发方面,通常一个大型项目都会有一两个开发团队由Team Leader带领,负责开发核心部分,而其它部分分派给不同开发小组或者分派给外包公司。在网上常看到几句话,贴切地形容了PM与TL的区别:“技术人员乐于被领导;但他们不喜欢被管理,不喜欢像牛一样被驱赶或指挥。管理者强迫人们服从他们的命令,而领导者则会带领他们一起工作。管理是客观的,没有个人感情因素,它假定被管理者没有思想和感受,被告知要做什么和该如何做。领导是引领、引导,它激励人们达成目标。领导力是带有强烈个人感情色彩的,它不是你能命令的,也不是你能测量评估和测试的。”

TL对于团队内的member,必须在技术上能够进行胜任领导,他就像黑夜里的灯塔,引导和修正member前进的航向。因此TL也必需保持照亮团队,保持对member的充分了解,并在技术领域投入持续的学习热情,向团队成员传道,补齐短板,让大家的核心战斗力一起提高。

无论是PM与TL,对业务与技术都要有深入的了解,只是PM更侧重于业务的管理,盈利的多少,风险的大小等等,而TL则侧重于项目的成本,开发的难度,软件的架构等技术方面的问题。在某些人眼中,技术与管理就像鱼与熊掌,不可兼得,但依在下看来,两者却是秤不离砣,密不可分。只要及时提升自己对技术与管理的认识,不断地向深一层发展,要从程序员提升到技术管理人员只是时间的问题。

团队规模有限时,分工很可能没这么清晰,很多时候TL和PM是同一个人在兼任,角色的模糊,可能带来职责和权利的混乱,而且个人精力也有限,仅适合任务量较小时实行。

技术管理 VS 团队管理

技术团队与其它的团队的区别,可能在于技术人员的管理难度。一方面因为领域之间的差异性,譬如前端技术链与后端技术链的深与广,导致熟悉前端的同时,很难对后端保持足够的了解。另一方面在领域内的差异性,典型的是多年争论C语言与Java语言的优劣,忽视了技术都有局限性,脱离了使用需求的比较没有意义。如果技术人员比较狭隘,往往无法理解对方的差异性,从而造成不必要的争议。这一点在年轻的团队中,是很突出的问题,大家普遍经验欠缺,对上下游的技术无法做到足够认知,同时因为年轻而无法接受建议,行成内耗。

凤栖梧桐

技术团队管理需要划分为正切的两个方向:技术管理和团队管理。管理的目标是:提高协作时的执行力,执行效率,并约束进行规范执行。技术管理的目标是:保持并引导团队成员之间的技术协作一致性,如前端技术团队和后端技术团队。由技术负责人制定日常规范,推动规范的落地,以公有的强制约定来避免不必要的内耗。规范的含义是比较广泛的:设计规范、编码规范、安全规范、接口规范、部署规范。团队管理解决的问题是:如何持久降低团队之间或团队成员之间的合作成本?如产品团队和技术团队。除了leader的影响,往往需要合理的行政制度来作为基础。好的行政制度是公司最重要的部分,它就像生态系统的土壤,有了好的土壤,才可能生长出好的团队。在互联网时代,站在巨人肩上而诞生的伟大产品屈指可数了,很难有产品在一开始就100%贴合市场,更多的需要快速试错,进行小步快走的方式进行迭代,而在持续打磨的过程中,产品也就自我完善,可以产生商业价值了。我们很难想象在二流的团队中如何诞生一流的产品呢?管理者也需要深思,在渴求的一流产品的同时,是否给予了一流产品诞生所需要的环境呢?

能力与影响力同样重要

从纯粹的技术工作者转向管理者难点在于是否具备了管理能力?对与团队内的成员,是否一致的认为在你的带领下可以得到更好的发展?可以是经济利益上的或职业发展上的。对于上层决策领导是否认为你可担此大任?而如果太过于沉迷技术,缺少与决策层的互动交流,导致上层无法相信你已经具备管理能力,或确实缺少部分能力(可能是软实力,语言沟通能力),这在国外很常见,管理层大多是美国人或印度人,中国人由于文化上的本位差异,很难上升到管理层。

技术管理

设计规范

如何在低复杂度、可维护性、可理解性之间进行平衡?需要在需求分析和系统设计时制定一些规范来约束,帮组我们低维护成本的延长软件生命周期。没有人可以保证业务在长远未来的走向,因此软件设计也只需要在可预见的范围内进行权衡考虑,超出范围的要交给升级版的设计来解决,这可能与传统思维不同。典型的,以系统容量进行设计,比如在电商系统中,按照统计数据分析预测在未来5年内,系统用户数会逐步增加到五千万,日订单量约在五百万左右。那我们在做系统设计时,就可以参照这些指标来做,只考虑5年内系统如何满足要求即可,但是请保留5年后设计进行升级的可能性,毕竟推倒重来的代价在系统愈发庞大后会让人无法接受。

DRY

DRY(Don’t Repeat Yourself)的核心是面向可维护性,将性质重复的逻辑进行封装,抽象为一组对外调用的功能接口。

DRY 是一个最简单的法则,也是最容易被理解的。但它也可能是最难被应用的(因为要做到这样,我们需要在泛型设计上做相当的努力,这并不是一件容易的事)。它意味着,当我们在两个或多个地方的时候发现一些相似的代码的时候,我们需要把他们的共性抽象出来形一个唯一的新方法,并且改变现有的地方的代码让他们以一些合适的参数调用这个新的方法。

vs WET

违反DRY原则的解决方案通常被称为WET,指代“write everything twice”。

KISS

KISS(Keep It Simple, Stupid)原则,是指在设计当中应当注重简约的原则。不只是软件系统设计,甚至在用户体验,需求分析时也可以应用。产品设计中,堆叠功能是容易的,考虑如何做减法才能体现产品经理的水准。

KISS原则在设计上可能最被推崇的,在家装设计,界面设计 ,操作设计上,复杂的东西越来越被众人所BS了,而简单的东西越来越被人所认可,比如这些UI的设计和我们中国网页(尤其是新浪的网页)者是负面的例子。“宜家”(IKEA)简约、效率的家居设计、生产思路;“微软”(Microsoft)“所见即所得”的理念;“谷歌”(Google)简约、直接的商业风格,无一例外的遵循了“kiss”原则,也正是“kiss”原则,成就了这些看似神奇的商业经典。而苹果公司的iPhone/iPad将这个原则实践到了极至。

把一个事情搞复杂是一件简单的事,但要把一个复杂的事变简单,这是一件复杂的事。

SOLID

在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。SOLID所包含的原则是通过引发编程者进行软件源代码的代码重构进行软件的代码异味清扫,从而使得软件清晰可读以及可扩展时可以应用的指南。SOLID被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。

YAGNI

YAGNI(You Ain’t Gonna Need It):适可而止,只考虑和设计必须的功能,避免过度设计。只实现目前需要的功能,在以后您需要更多功能时,可以再进行添加。(You Ain’t Gonna Need It,YAGNI 原则)

  • 如无必要,勿增复杂性。
  • 软件开发先是一场沟通博弈。

WebSphere的设计者就表示过他过度设计了这个产品。我们的程序员或是架构师在设计系统的时候,会考虑很多扩展性的东西,导致在架构与设计方面使用了大量折衷,最后导致项目失败。这是个令人感到讽刺的教训,因为本来希望尽可能延长项目的生命周期,结果反而缩短了生命周期。

编码规范

命名规范

我们在编写任何程序之前,第一件事要做的就是命名。

形如:需求文档名称,概要设计文档名称,接口文档名称,软件系统名称,功能模块名称,类名称,方法名称,参数名称…

在认识陌生人时,第一次接触时互递名片,熟络后的沟通交流,都以名字来起始了。名字是否好记,影响会有多大呢?前公司有一同事丁某某,父母取名讨巧用了生僻字,导致每次登机之前,需要到机场值班经理盖章,白白浪费很多时间。而恰好他是销售经理,每周有2~3次的商务飞行。你在想他为什么不去换名字?想想30来岁的人生积累,需要多少成本改名字吧?想象不出来,问问自己更换使用5年以上的手机号码,成本是多大吧?

重要性

命名规范竟如此重要,但大多数开发者的命名习惯往往没有你想象的那么好。除了上面提到的生僻字问题,还有很多人喜欢用拼音去命名类、函数,甚至是变量。我不知道这是英文词汇量的问题还是个人风格,但我个人非常不提倡这样做。如何优雅地为程序中的变量和函数命名?很简单,把你的变量名拎出来,问别人,你看到这个名字会想到什么,他说的和你想的一致,就用,否则就改。改到基本上不懂程序的人都能大概看懂你写的是什么,就优雅了。

为什么重要?提高可维护性。不加约束的开发者写出来的代码,就像是大家临时拼凑出来的一桌菜,你上粤菜,他上川菜。虽然都好吃,但没有人全想吃,造成的局面是,爱吃粤菜的人总是夹粤菜,爱吃川菜的就不去吃粤菜了。在代码上来讲,各自的代码风格迥异,导致理解成本加大,你负责的代码需要修改时,就只有你能改了,团队其他人很难帮得上忙,如果修改工作量比较大,这岂不是把自己逼死吗?如果结合好的代码注释和设计文档,会不会理解成本下降呢?但就算是文档写得再详细,我们也要去读代码,所以文档主要是体现思路和反映需求和设计。在程序上,我们的命令应当和文档中的术语保持一致,而程序中的命名也应该是用和文档相同的风格,这样,我们可以少很多理解上的成本。好的代码在命名上是完全可读的,代码即文档,做到这点,可以极大的提高团队内协作效率。

比较常见的有下面三种命名方式:

  • 驼峰(Camel)命名法:又称小驼峰命名法,除首单词外,其余所有单词的第一个字母大写。GetOrderList
  • 帕斯卡(Pascal)命名法:又称大驼峰命名法,所有单词的第一个字母大写。gtOrderList
  • 蛇形(Snake)命名法:单词与单词间用下划线做间隔,看起来就像是上下扭动的蛇。get_order_list

一般在命名函数和一般变量时,多使用小驼峰,命名常量和枚举变量时,使用大写的蛇形;命名类名多使用大驼峰,不建议使用蛇形命名。如在命名数据库表名时,最好使用小驼峰以一致性的对应到pojo。

好的命名,还给代码审计、代码排错工作降低成本,在做code review时,如果缺失详细注释(在敏捷开发中很常见),规范化的命名使大家的理解成本降低,从而让审计工作更容易完成。我们回过头来想,很多公司很难推动代码审计的落地,到底是什么原因在阻碍呢?还有在线上代码深层排错时,我们可以采用SystemTap火焰图技术来做,由于函数的名称通常会包含语义上的信息,在输出的图表中就可以轻易的按照函数名来推断问题所在,这是非常了不起的。

如何做?

具体的施行,可以参考:Google Java Style

约定优于配置

约定优于配置的目标是,团队内各成员参照约定的策略来进行开发,减少无意义的说明项,从而降低沟通成本。为降低持续学习成本和保证团队新成员快速融入,约定项不宜过多,且应该放到团队成员随意备查的地方(media wiki?)。

过渡配置

Hibernate的早期版本中,将类及其属性映射到数据库上需要是在XML文件中的描述,其中大部分信息都应能够按照约定得到,如将类映射到同名的数据库表,将属性分别映射到表上的同名字段。这样的做法不仅繁琐易出错,而且可维护性低,后续的版本抛弃了XML配置文件,而是使用这些恰当的约定,对于不符合这些约定的情形,可以使用Java注解来说明。

Spring由于其繁琐的配置,一度被人成为“配置地狱”,各种XML、Annotation配置,让人眼花缭乱,而且如果出错了也很难找出原因。Spring Boot项目就是为了解决配置繁琐的问题,最大化的实现convention over configuration(约定大于配置)。熟悉Ruby On Rails(ROR框架的程序员都知道,借助于ROR的脚手架工具只需简单的几步即可建立起一个Web应用程序。而Spring Boot就相当于Java平台上的ROR

构建管理工具链中,有很多体现了这个原则的正确性,如从Ant到Maven的过渡,Grunt到Gulp的过渡。其他的在中间件产品中,redis和mongodb的配置文件也有体现,只需要很少的个性化配置就可以正常运行。

改进

约定优于配置(convention over configuration),也称作按约定编程,是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。
本质是说,开发人员仅需规定应用中不符约定的部分。例如,如果模型中有个名为Sale的类,那么数据库中对应的表就会默认命名为sales。只有在偏离这一约定时,例如将该表命名为”products_sold”,才需写有关这个名字的配置。

合理的设计

合理的设计,主要是面向软件可维护性、可扩展性提出,如果能兼顾性能和安全就更好了。设计是有成本的,如果软件的生命周期在预期内就不会太长,例如很多一次性的产品:企业内网IM系统,售卖后,几乎不再需要任何的维护。此时设计上就可以从简,没有必要为不会来临的维护去打基础,因此设计也是需要有一定的前瞻性。

适时分层

分层设计的好处主要是面向软件可维护性。合理的分层设计,往往将逻辑拆分为内聚的多层次,层之间的边界和职责是清晰的。例如典型的三层设计,展示层、逻辑层、数据层。带来的好处是显而易见的,对于数据层来说,只需要对逻辑层提供访问数据的接口,具体如何实现访问数据,是访问文件还是RDBMS、NoSQL,与逻辑层无关。相应的重构起来也是局部的,如更换搜索引擎:Solr –> Elastic Search,只需要重构数据层的具体实现即可,上层任何代码不需要改动,这对于高复杂度的软件无疑是友好的。

那分层对与软件可扩展性的影响呢?带来了一定程度的坏影响,新加的业务逻辑功能,可能涉及到很多层的改造,而在Maven mutilModule模式下,很可能层之间的代码是隔离的。这意味着,新加功能的代码量会有增多,相应的整体构建需要分布进行后融合。反过来想,这不失为一个好的过滤器,帮助我们分辨开发者的技能是否满足团队要求?

一般来说分层设计适用于大型软件的持续构建,它依靠团队内的其它高效率手段来弥补稍低的可扩展性。它对安全和性能,并没有什么影响。

再谈过度设计

简单来说,过度设计就是进行了过多的面向未来的设计,进行了不必要的抽象封装,为系统增加了不必要的复杂度。
举个例子,你要做一个功能模块,但你考虑到到这个系统里还有几个未完成的模块和你要做的东西类似,所以你决定为此额外做一些抽象和封装,以便将来复用。然而到后来你开发那些相似的模块时你才发现,可能是由于抽象不足或抽象错误,你不得不重新修改之前的封装才能完成复用,导致最终成本实际上还不如不做;或者你发现复用的部分所降低的成本实际上还不如包装花费的成本。 这些都是最常见的过度设计的例子。
程序员在掌握了一些基本的设计能力之后,最常见也是最难克服的设计问题往往就是过度设计。上面的错误我相信大多数人都一而再,再而三的的犯过。

与过度设计相对的就是设计不足。
虽然是两个相对的概念,但设计不足和过度设计绝大多数时候都是一起出现的。都是最常见的设计问题。设计不足不仅常见于新手,老手也常犯。甚至我还见过有一类老程序员在经历过多次过度设计的打击之后,转向另一个极端,否定抽象封装的作用,走上“反设计”的道路。

过度设计和设计不足的平衡问题没有很好的解决办法,只有依靠经验的积累和不断的总结思考。如何把握这个度是最能考验程序员的经验和价值的问题之一。

我所尝试过的软件方法中,有一种方法的思维方式对于解决这个问题帮助最大,就是TDD(测试驱动开发),这里简单说下为什么TDD能解决这个问题:
TDD的一个核心思想是小步增量,不断重构。具体说来就是TDD有两个状态(常见的说法是两顶帽子):
状态A:用test case描绘需求,并使用最简单的方式满足这个test case。注意要用最简单的方式满足这个需求,不能为任何test case之外的需求做任何设计。 test case通过之后进入状态B;
状态B:重构代码,让现有的代码在尽量保持简单性的同时足够优雅清晰。注意此时你只能对现有的实现代码进行重构,不能增加任何新的功能和test case。
整个TDD的过程就是在这两个状态间不断转换的过程。在状态A增加功能,在状态B优化设计。

TDD的这种思维方式走的稍微极端一点。它直接排斥任何对未来的设计,转而以优雅简洁的设计和test case来为未来需求的重构降低成本。 可以说严格遵循TDD做出来的设计必然在过度设计和设计不足方面都不会有太大的问题。

我严重推荐TDD。不管你最终会不会接受TDD这种开发方式,它独特的思维方式都必然会给你的设计观念带来很大影响。

过度设计的典型案例:代码重构的一个示例

模块化设计

模块化设计在重构时,可以将影响降到最低。达到低风险、低成本。它符合CCP原则(Common Closure Principle(CCP)– 共同封闭原则)。它对系统快速构建是一个阻碍,要求将可预见的性质相同代码,内聚到一个模块中。

例如读取和更新配置文件的功能,在软件中很多逻辑中需要调用。例如:读取定时任务的触发条件、读取接口调用频次限制。无论是从XML或JSON甚至在DB中读取,其实逻辑差别甚少,无非是:链接配置源、读取所需参数、解析正确性,返回并应用。如果重复这样的代码,将给我们带来麻烦,多类配置项产生依赖关系,读取和更新的交织将变得非常复杂。采用模块化设计,也是DRY原则的体现,对同性质的代码进行内聚为一个模块,对外部调用提供多个组合接口即可。

前轻后重

系统之间的调用链,前置系统轻逻辑。

上轻下重,核心逻辑内敛

模块之间的调用链,上层模块轻逻辑

基础优先

在构建软件时,保持基础组件优先稳定下来,定义好对外接口,并保留可扩展性。
例如安全性设计,在后期加比在前期加的成本高很多。

最小冗余

代码保持最少行数

1.少一行代码,就少了一个潜在的bug

2.行数少了,往往意味着重用性高了

3.不要过度牺牲可读性

保持简单

简单意味着快速

简单意味着灵活

简单意味着易扩展

简单意味着易重构

Fast fail

代码即文档

合理的注释

关键业务设计宣讲

保持一致的看法

小步快走

意味着版本迭代快速

意味着版本迭代稳定

意味着部署回滚成本低

重构与优化

紧急优先

优先对紧急需要改进的部分进行重构

计划性的重构

在敏捷开发时,往往时间不够,有些模块只能以非优雅的方式构建。但是需要在这些模块加上TODO以免重构时遗忘。

时机

团队一致认为模块维护成本高昂时,果断重构。

基于成本的优化

CBO(Cost base optimize)

引用数据库查询分析计划来说明:如果有人问你数据库的原理,叫他看这篇文章#查询优化器

数据支撑

给出重构前后的成本与收益报告

持续性

在时间不够或时机不成熟时,分阶段的进行

技术选型

开源优先

Facebook在推广MyRock持有非常开放的态度,所有的源码开发都是在外部的github上进行的,社区的大量使用可以带来软件的进一步成熟,进而反哺其本身的业务。

开源意味着安全,源码的公开维护,使得热门源码的安全漏洞被很大几率发现,进而修复。

开源不意味着任何人都可以修改代码,事实上这将带来隐藏bug问题。在github上,你可以对public权限的代码进行PR(pull request),而是否采纳你的PR,由维护团队来决定。

稳定优先

掌控度高优先

保留备选项

只有合适的技术,没有最好的技术。

避免一招鲜吃遍天

手里有锤子,看到什么都觉得像钉子

技术升级

系统容量与性能指标的要求,在业务发展的各个阶段是不固定的,技术需要保留一定前瞻性,不做被动跟随。

接口规范

内部接口

版本控制

访问控制

访问权限的验证
访问频率

流量控制

拒绝恶意访问

暴力破解

审查与监控

灰度发布

无状态

原子化

CQRS

命令查询职责分离模式,读不强依赖写。

APP

远程控制权

展示为主

少的逻辑

适度安全

外部接口

合理调用

同步VS异步
批量VS单个 批量:局部失败

保持怀疑

兜底方案

如果有可能,在外部接口调用失败时,读取本地上一次调用成功的缓存。例如:加载和更新广告,CDN访问失败进行回源。

进行兜底时,注意将失败的信息进行记录以便告警

部署规范

监控

降低升级时间窗

没有人需要等待

正式发布后,对于任何用户都是公平使用的。不需要牺牲部分用户利益。
常见于缓存autowarm时需要阻塞第一批到达的用户。

提前消除隐患

团队管理

识人、用人、管人、留人。(华为)

精细化管理

keep watch

工作与心理的交流分享

保持信任

保持怀疑

持续考评

KPI VS OKR

分享中提高

有一个著名的理论,叫木桶理论。意思是木桶能装多少水,是由木桶最短的那个板来决定的。团队也是如此。团队的力量有多大,很多时候也是由能力最差的那个成员来决定的。为了提高团队整体的实力,我们必须提高每个人的能力。我们的改善必须得是可度量的,所以我们也要数字化能力的标准。

提高工作效率

其实很多团队都在进行着这个工作。最常见的就是会做一些小工具,来节省我们的时间。比如代码自动生成工具,自动打包工具,自动的比较工具等等。我们应该制作尽可能多的自动化工具,来解放我们的时间。

为了让大家投入更多的热情来制作各种工具,团队可以制定一定的奖励规则,对制作工具的人给予奖励。

公司要把最好的人才放到工具开发那一块,因为工具做好了,可以达到事半功倍的效果,所有人的效率都可以得到提高,而不仅仅是工程师。

互相备份

互相支撑

向上管理

向下管理

了解队员的擅长与不足

合理的任务分配

适度挑战性的任务

给出完成任务需要的资源

激励中成长

眼前利益与发展前景

给出职业发展的机会

公开与公平中考评

保持团队稳定

团队成员的诉求:团队,产品,薪资,发展。

团队提供集体成长的氛围环境。

令人兴奋的产品,提供持续打磨的动力。

合适的薪资,使个人不过多为生活所扰。

良好的发展前景,对个人的职业生涯带来腾飞。

“穷得只剩钱”。小公司对企业文化不慎重视,开发团队的整体风气也没要求,招人过来只求出活,对团队个人发展是零投入的态度。这样的企业要想招到人、留住人,只剩钱了,但个人发展到一定高度,往往企业再也给不出足够的钱,这时再想留住人就很难了。这就是企业只依靠薪资吸引人带来的问题,对其他人才诉求不重视,穷得只剩钱。

当收入待遇和地位与自身才能不匹配时候,离开几乎是个必然的选择。

适时剔除不合格

技术梯队

不同层次的人员配比,完成不同类型的任务。注意人员的上升下降渠道。

Reference

Talk is cheap,show me the code.