如何提高对自己情绪的控制力?

情绪的亏

我吃过太多被情绪控制,大多数人应该都和我一样。

和家人相处中,明明因为一个小事而已,因为自己的烦躁和不安,表达出厌恶和不喜欢,情绪的传染导致争吵。当冷静下来只有发现完全没有必要争吵。

在工作中,因为同事或者客户一些对自己不好的地方,比如客户无端的额外需求。心生抱怨,除了暴露出去不专业的态度而外,对事情并没有好处。

还会给人留下喜好抱怨的印象。当情绪开始主导人的生活时,自己就成了情绪的奴隶,很难掌控生活的的方方面面。在情绪影响自己的过程中,就好像丢失个人意识,处于一种昏头的状态,只有当事后才能发觉自己当时的行为和决策不是在清醒的状态下做出的。

我相信在生活中因为情绪 “失智” 的状态越来越少时,生活也会更加如意。理解情绪其实还会带来收益,有一些岗位,工资中带有一些对抗情绪压力的成分。因为容易挨骂,所以工资低没人愿意来,只能提高待遇,如果意识到只需要抵抗不良情绪就有收益的,心态是不是马上不一样?

本我、自我、超我

情绪一直是心理学研究的对象之一。

虽然弗洛伊德的理论已经不再是心理学的主流方向,但是他的 “本我、自我和超我”理论还是影响着很多的人。在这个模型下,

  • 本我代表着感性、情绪是一个人做出的自然反应。
  • 自我代表着理性、逻辑是一个人做出利弊分析后的反应。
  • 超我在西方世界观中,代表着神性,用中文语境来说就是贤者模式。

对于情绪的机制来说,情绪是客观的,一直存在的,不因为个人意志而转移。只能说在一定程度上可以被控制、消化,不影响人做出理性的决策和行为。

当一个人的自我和超我大于本我时,他就能掌控自己的情绪。甚至能拿起情绪作为武器达到自己的目的。

利用情绪作为武器,是各种故事不错的桥段。

在三国演义的故事里面,诸葛亮骂王司徒,王司徒被激怒,最终情绪崩溃,一口气上不来倒地了。诸葛亮在他的表现里,自我和超我达到了极致,目标明确,情绪稳定,因此能把情绪作为武器使出。

在《让子弹飞》中有多次利用情绪武器的地方。胡万通过激怒六爷,然后利用六爷的正义感情绪,诱导六爷剖腹取粉,这种情绪的利用非常歹毒。最后麻子利用发钱,黄老爷抢钱唤起群众的反抗情绪,又通过枪毙黄老爷替身的方法消除民众的恐惧情绪,最终推翻黄老爷。

六爷的自我弱于本我,麻子的自我强于本我,事情发展的结果也不一样。

识神、元神

在西方世界中有本我、自我和超我,在中国古代也有类似的理论。就是识神和元神。

中文语境下的“神”代表着意识、精神,比如下笔如有神、失了神等说法

  • 识神,识是佛教用语,六识是“眼耳鼻舌身意”。识神可以简单理解为来自感官的感受和一个人情绪的自然反应。
  • 元神,识神之外的意识,也就是不受外界影响下的清醒理智。

古代的修行的人早就意识到识神和元神构成人的意识,所以需要不断排除识神,而让元神显现。

酒色财气都会让元神受到麻痹,识神被放大。所以古人认为在酒色财气之下是做不出正确的判断,并常常有失格的表现。酒色财气这四样东西都是很危险的。

训练

掌控情绪用中国古代的说法就是识神退位,元神显现。用西方心理学的说法就是强化自我、超我,抵抗本我。

作为一个依然被情绪无时不刻掌控的俗人,给不出好的方法,只能拾人牙慧,随便聊一聊。

世俗的历练

想要成为一个能掌控情绪的人,最终处变不惊,需要有来自大量的世俗经历,见的多了,也就看的淡了。人生经历就像一次一次脱敏训练,情绪反应阈值不断提高,也不会那么咋咋呼呼。

所以那些能在酒桌子上还能清醒自如,实现酒局的目标和价值的人多么厉害。见到过一些政商老江湖,喝的半醉不醉,借着酒劲把话说开,但是内心还是极其清醒。

每次经历都会沉淀为宝贵的人生经验,这是任何情商训练难以达到的。

危险性

对抗情绪,是有危险的。

曾经有一段时间陷入一定的强迫症行为,于是触发了我的好奇心,留意身边的人是否也有类似的行为。发现很多人的强迫行为是因为和情绪对抗造成的。

明明是一件很生气的事情,需要装作没什么事,但是从他的表情中知道并不是真的没事。如果细心观察到的话,会发现大多数人抵抗他们的情绪时显得并不轻松。

后来反思让我走出强迫症的原因,其实非常简单。认识到了自己能力的边界 ,不良情绪是客观存在的,到了一个边界就不再是我的能力可以完全控制的了。撑不下去还强撑,不仅对事情于事无补,而且对身心造成极大损害。

所以学会放弃成了治疗强迫症最好的方法,用流行的话说叫做和内心和解。

more >>

软件工程师的"吵架"文化

吵架的团队更有生命力

看一家软件公司是否靠谱很简单,在开放式的办公室中有没有随处可以使用到的白板就行。

一个好的软件开发团队往往看起来没那么”和谐“,程序员和产品经理之间,程序员和程序员之间是不是会发生”争吵“。好的软件公司没有不吵架的,不吵架的软件公司要么大家都在划水,要么大家为了维护 ”职业形象“处处小心翼翼不敢献计献策。

刚开始和华为的人合作可能会有一些不适应,他们说话的方式简单而直接,往往让人不知所措。这种直接的背后是一种撸着袖子干的企业文化,把”职场文化“变成了团队协作。

软件公司吵架不是管理不善,往往代表着开放、包容的文化,以及团队成员心无芥蒂的通力合作。

回想一下,我们是不是往往都是和陌生人和和气气,和信任的人才大大咧咧呢。

文吵和武吵

不过吵架这事儿还有一些讲究。在公司吵架这事儿分为文吵和武吵:武吵是比谁的嗓门大,谁说话的分量重;文吵讲究的逻辑、推理和归纳。

而武吵和文吵各有用处。

武吵需要的是一个敢于担责,能在关键时候拿出主意的人。在很多讨论会中,群众容易陷入只对自己认知匹配的细节,然后各执一词,都很有道理,也各无过错。这个时候,文吵的逻辑难以派上用场,因为现实世界中没有完美的、正确的方案,只有不那么差的方案。那就需要武吵的人发挥他的影响力,给团队拿个主意,先结束这次讨论,得到一个基本可用的方案来,才能让事情继续。

文吵则不一样,软件开发毕竟是一门科学,大多数情况下需要用到逻辑,通过分析、归纳、总结得到方案。需要收集足够的信息,给出足够的理由,并对方案中的概念给出清晰地定义,最后通过因果分析,证明自己的想法。在大多数情况下,我们更多的需要文吵,通过分析和证明解决遇到的问题。

不管是文吵还是武吵,都需要讲逻辑。只有当团队对这些基本的逻辑规律达成共识的时候,”吵架“才能高效,并且避免落入谬误的陷阱。

逻辑学的三个基本规律

逻辑学的三个基本规律可以让吵架更加准确,避免无意义的争论,减少逻辑矛盾,让吵架有所产出。这三个重要的规律是:同一律、矛盾率、排中律。

同一律

在同一段论述(命题和推理)中使用的概念含义不变,这个规律就是同一律。形式化的表述是 A -> A。

”概念“在逻辑学中的意义非常重要,概念有两个逻辑学规律:内涵和外延。内涵指的是这个概念区别于其他概念的本质属性,例如大熊猫是指的生物学中某一个物种。外延指的是这个概念所能描述的事物的范围,比如白马比马这个概念外延要小,是不同的两个概念。

所以白马非马的本质争论在于自然语言的不确定性:

  • 从概念上说,白马这个概念不是马这个概念。所以白马非马。
  • 从谓词(”是“ 这个谓词)逻辑来说,白马这个概念代表的事物集合属于马这个概念代表的事物集合。所以白马是马(白马属于马,但是白马这个概念不是马这个概念)。

同一律描述的是在一段论述中,需要保持概念的稳定,否则会带来谬误。我在大学辩论赛中利用了这个规律,赢了一次辩论。

当时的论题是”网络会让人的生活更美好吗?“,两个论点主要的论点是:

  • 网络让人们的生活更方便。
  • 网络让人们沉溺虚拟世界。

我们选择的论点是 ”网络让人们的生活更方便“。在辩论赛的前期,另外一方为了论证 ”网络让人们沉溺虚拟世界“,描述了打电话、写信也可以让人生活很美好,并不会沉溺。这刚好落入了我们的逻辑陷阱。我们指出,邮政、电话网络也是网络的一种,对方的逻辑不攻自破。

这属于典型的 ”偷换概念“,我们偷换了“计算机网络”和“网络”这几个概念。

矛盾律

矛盾律应用的更为普遍,几乎所有人都能认识到矛盾律。它的含义是,在一段论述中,互相否定的思想不能同时为真。形式化的描述是: ”A 不能是非 A“。

矛盾律这个词的来源就是很有名的 ”矛和盾“ 的典故,出自《韩非子·难势》中。说有一个楚人卖矛和盾,牛皮吹的过大,说自己的盾在天底下没有矛能刺破,然后又说自己的矛,天底下的盾是不能穿透的。前后矛盾是一个众所周知的逻辑规律,但是并不是一开始马上就能看出来,需要多推理几步才能看出来。即使如此,在同一个上下文中,出现了矛盾的逻辑论述也被认为是不可信的。

具有矛盾的论述有时候又被称为悖论。尤其是宗教领域充满了大量的悖论,例如是否存在一个万能的神,做一件自己不能完成的事情。

矛盾律的用处可以驳斥不合理的论断,也可以用于反证法。在软件开发过程中,我们时常遇到这种情况,需要开发过程中才能发现矛盾。这个很难避免,除非有充足经验的工程师。

需要注意的是逻辑学中的矛盾律和毛泽东思想中的矛盾论不是一回事,前者是逻辑学规律,后者是辨证唯物的一种方法。

排中律

排中律是逻辑规律中最难理解的一个规律。它的表述是:同一个思维过程中,两个互相否定的思想必然有一个是真的。用形式化的表述就是,A 或者非 A。

排中律的意义在于,明确分析问题的时候不能含糊其辞,从中骑墙。比如有人讨论:人是不是动物。不能最终得到一个人既是动物又不是动物,这种讨论是没有意义的。

比如在一次技术会议中,需要选择使用的数据库,只能使用一种数据库。如果采用了 MySQL 就不能说没有采用 MySQL。

排中律看起来好像没有意义,但具有非常大的价值,让讨论最终有结论,而不是处于似是而非的中间状态。

如何诡辩

在争吵中,人们会下意识的引入谬误,从而主动或者被动的诡辩。诡辩的方法非常多,下面聊几个有意思的诡辩方法,认识到诡辩的存在,让吵架的输出更可信。

偷换概念

偷换概念是一种利用同一律的诡辩方法。往往是利用一个词语的多义性来制造诡辩,这种例子相当常见,再一次日常对话中:

朋友:为了让自己的判断和认知更为客观,我们应该同时学习多个学科的东西。

我(故意抬杠):人不能同时学习多个学科的东西。

朋友:为什么,学生不都是同时学习数学、语文、英语么。

我:你现在正在看手上这本书,能同时看我手上这本么。

朋友:。。。(感觉被套路)

我偷换了概念,把 ”同时“ 这个词的时间精度调低了,导致这次对话变了味。

偷换概念在生活中无处不在。《武林外传》

中的秀才利用 ”我“这个概念的偷换,让姬无命莫名其妙并自杀了。

相关性不等于因果性

这个是一个不得不提的诡辩手法,我们从小深受其害。

最经典的例子是,很多父母信佛,然后娃高考的时候天天去求神问佛。如果小孩考上了大学,那么就是拜佛的功劳,如果没有考上,那就是小孩不努力。多么完美的逻辑闭环,完全无懈可击。

同样的桥段在各种电视、电影中存在。某一伙人闯入了一个村子,然后这个村子发生了瘟疫,群众认为是这些人带来了不详。

程序员圈子也会有类似的议论,因为大公司都用的 Java 而不是 PHP,所以 PHP 是一个垃圾语言,我们要成为大公司,所以要把 PHP 换成 java。所以很多公司明明可以苟一下,然后因为折腾死掉了。

我们需要时刻记住,相关性不等于因果性,才能认识到一些微妙的逻辑关系。

因果倒置

”可怜之人必有可恨之处。“ 这是很多人挂到嘴边的话,支持者甚多。

我小的时候对这句话记忆深刻。小学的时候被年长的同学欺负,后来因为打架老师知道了,其他同学都表明我是个被欺负的可怜鬼,老师还是对我们都做出同样的处罚。

说出了一句举世名言:“为什么欺负你,不欺负别人”。

为什么只欺负你,不欺负别人,所以你也不对,同样要受到惩罚。这是典型的强盗逻辑,从结果推导出原因,但是这个原因并不成立,因为我们知道原命题为真,逆命题不一定为真。

归纳法的局限

逻辑学上把个别的知识推广到一般的知识规律叫做归纳推理。归纳推理是一种朴素的认识方法,在逻辑学中,归纳推理有其意义,但是需要注意的是逻辑学从来没有把归纳法得出的结论当做真理。

归纳法的问题和类比谬误类似。古人认识的到了一个规律,鸡叫三遍天会亮,但是后来出去旅游发现其他地方的鸡不是这样的,真的是应了那句,“东方不亮西方亮,黑了南方有北方。”

中国太大了,甚至二十四节气的规律都不能适用于每一个地方。归纳法只能有限的反应某种规律,不能广泛、绝对的得到真理,也不能从个体推出一般。

算命先生希望从四柱八字、面相分析、掌纹、笔迹这些中归纳真理,如果认识到归纳法的局限性,就不会平白无故交这些智商税了。

责任转移

证明神存不存在,保健品有没有功效,壮阳药有没啥作用是科学界三大难题。

从逻辑上证明有其实很容易,只需要找出一个例子即可,比如证明天鹅是白色的,只需要找出一个白色的天鹅即可。但是证明黑色的天鹅不存在,是非常困难的,除非穷举世界上所有的天鹅,才能得出这个结论。

人们的思维中,天生偷懒,所以人们才会有 “宁可信其有,不可信其无”。

所以有一种诡辩,我姑且称之为责任转移,就是在辩论中把举证的责任推给别人,然后再来挑对方的毛病。这是一种非常高级且隐晦的诡辩手段。

比如有神论要求无神论者给出证据,证明神不存在,但是证明无非常困难。对方只能举出一些例子,但是这些例子非常脆弱,如果再结合偷换概念就更无懈可击了。

大师:神会保佑你的。

无神论者:神不存在。

大师:你怎么证明神不存在呢。

无神论者:我从来没看到过神。

大师:没看到过神,不代表神不存在。

无神论者:看都没看见,怎么能说神存在呢。

大师:神是一种信念,它无处不在,慢慢体悟吧。

无神论者:。。。

责任转移大法是不断把举证的责任推给对方,然后在挑错,让对方自顾不暇。

总结

逻辑学中的内容非常多,还有很多有趣规律,比如三段论、命题和演绎等。

但对生活来说,本文介绍的一些方法用于吵架足够了,当然不是为了学习怎么制造诡辩,而是为了分辨诡辩。当我们在工作中交流时,能注意概念的统一和尊重同一律、矛盾律、排中律等逻辑学基本要素时,沟通会变得更加高效,吵架也更加有理有据,并从中得到成长。

more >>

如何克服害怕失败的心理

害怕失败是所有人的共性

”失败是成功之母“。从高中每个月蹭同桌的《青年文摘》中没少看到这类鸡汤。

为什么还想写一篇这样的文章呢,因为我想揭露一个事实,说这话的人不是矫情就是个棒槌。

为什么家庭背景好的人创业人数和成功的几率大很多呢?很多书籍中给出的答案是富人思维、穷人思维等等。其实答案非常简单,就是家庭条件好的人能出去折腾,并有人能兜底。

有人兜底是一个非常重要的因素,比各种 xxx 思维实在很多。正是有人兜底,知道创业失败了还可以回去啃老,不至于流落街头。我毕业时,其中一个室友就被 xxx 国际创业培训坑的没边,毕业后带着几千块跑到沿海城市做互联网思维创业,失败后灰溜溜的又跑回来。所幸家庭条件不差,在家里人的安排下混的不差。

换做当时的我是没这个胆子,毕竟失败了还得租房子、还助学贷款,所以害怕失败是必然的。

害怕是从远古人类就遗传下来保护我们的优良基因,毕竟胆子肥的人都没能活下来。男性对异性的害怕是来源于被种群首领揍的记忆,虽然现在谈恋爱不容易被揍了,但是这种基因还在,所以害怕失败是每个人共同的心理。但现代社会的背景已经发生了变化,原始基因中害怕失败的因子在现代社会中并不是对我们都有利,它让人不敢做出更多的尝试,并失去很多机会。

我一直是一个害怕失败的人,因为我是一个爱多想的人,每一次多想就会放大失败的后果。有趣的是当失败发生在生活中别人的身上时,发现后果并没有那么严重。

故事一,搞出线上事故的程序员

刚毕业工作时候,每次上线都心惊胆战,害怕弄出一个线上问题,然后就被扫地出门了。

这个怂样让当时的老板看不下去了,说了一句至今难忘的名言,”没折腾出线上事故的程序员都还不成熟“。当时不以为然,一个专业的软件工程师怎么能弄出线上事故呢?

工作的阅历告诉我,线上不出事故的软件无非两种情况:1. 软件基本没什么人用,搞出事故也没人发现。2. 技术选择极其保守,稍微不放心的东西都不往生产丢。生产环境的稳定性是很重要,但是过度的保守,害怕新技术的失败会导致项目一潭死水,无法成长。

害怕失败和害怕改变去重构代码的是同一种人。但有一些人的心理素质非常好,不得不佩服,也值得承担更大的责任。

几年前在一个项目上,有一位同事因为上线过程中误操作,造成服务中断,虽然事后也进行了事故回顾,但对他并没有额外的惩罚。实际上其他人也不会对他怎么责备。

其背后的逻辑很简单:

  • 能经常进行生产环境操作的往往都是比较重要的角色,因为生产环境操作比其他同事多,那么造成工作失误的概率自然更大。
  • 换一个人不见得就不会出错。
  • 不害怕出错敢于生产环境操作比其他人更能担责,毕竟有风险的操作总得找一个人做。

所以现实中对失败往往没有那么苛责,完全没失败过的人更可能是没有承担过有风险的工作。敢于直面风险,比逃避得到的不失败更可贵。有时候失败的代价并没有那么大。

故事二,玩垮苏联的戈尔巴乔夫

如果人人害怕失败,将不会有外科医生,因为随便一点失误代表一条命丢在自己手上。

如果人人害怕失败,将不会有国家领导人,可能因为一个错误的决策导致一个国家的命运被葬送。

在读世界的历史中感到非常困惑的一件事,戈尔巴乔夫宣布苏联解体,一个国家的消失,随之带来的是社会的动荡和人民生活的困苦。但是历史上并没有对戈尔巴乔夫追责,甚至他自己也不觉得这种巨大的失败有多了不起。在苏联解体后,戈尔巴乔夫生活的很好,甚至活到现在,并投身政治经济的研究,出了自己的书,还代言了几个著名的品牌。

了解更多苏联的历史后才认识到,戈尔巴乔夫并不是导致苏联解体主要原因,当历史的车轮滚滚向前时,换一个人并不会比戈尔巴乔夫做的更好。戈尔巴乔夫只是让苏联在解体的过程中平滑的失败,虽然苏联最终解体,但是并不剧烈,否则会带来内战。

我们往往把客观的责任主动或者被动的揽到自己身上,一个公司的失败,一个软件项目的失败,往往有它内在的原因。

人们往往以为失败是某一个决策,某一个决策是因为自己做出的,实际上是一种典型的认知错误。失败往往是各种因素的综合结果,决策往往也是因为在当时的历史背景、生活背景下做出的。

过度自责是害怕失败的罪魁祸首。

故事三,害怕失败的技术分享者

软件开发项目往往都会培养技术氛围,会做一些技术分享。有一个项目一个毕业生找我聊天,说自己害怕做分享,觉得要是做得不好太丢人。我问为什么呢?他说其他人的分享内容多么有深度、PPT有多么精美,他自己的分享达不到这个水平,自己一对比相形见绌。

然后发生了一段有意思的对话:

我:你每一天出门都洗头么?

他:不会吧。为什么问这个?

我:你看哈,如果你们组别人洗头了,你某天没洗头,其他人会注意你并笑话你吗?

他:应该不会吧。。。

我:是吧。其实每个人都只关心自己的事情,对别人没那么上心。就算你讲的特别不好,别人也只会在心里笑话你一小会儿,出于礼貌不会笑话你。即使实在是忍不住笑话你,过了第二天大家也就忘了。

他:。。。

然后他还是勇敢的做了一次自己的分享,当然不可能很差了,越是害怕失败的人,鼓起勇气的时候肯定做了最充分的准备。

不要过于在乎别人对自己的评价上,实际上,不洗头出门也没有太多人注意到你。

总结

通过三个简单的小故事,总结下害怕失败的原因和克服害怕失败心理的方法。

害怕失败的原因

  • 没人兜底,确实无法承受失败的后果
  • 生性敏感,过于在乎别人的评价
  • 过于自责,主动或者被动的归责

克服害怕失败的心理的方法

  • 故意犯错,提高害怕的阈值(不是阀值,也不是阙值)
  • 尝试一次不洗头出门,感受下其实别人没那么在乎你
  • 预演失败,如果失败确实是自己无法承受的失败,这种情况下害怕是对的

more >>

软件建模中的逻辑学基础

大家都知道编程需要软件工程师具有很好地的思维逻辑,但是有意思的是,在讨论需求和业务建模过程中,会出现互相指责对方没有逻辑。在我经历过相当多次的同事的争吵中,发现了一个规律:每个人都有自己的思维方式和“逻辑”,并排斥其他人的逻辑。

大多数有经验的开发者、系统分析师都具备一定的辩证思维和方法,要说谁没有逻辑,这件事情很难说得过去。如果每个人都是用自己的思维方式和 “逻辑”,让软件建模的统一语言非常困难。我疑惑的是每个人都相信逻辑是很重要的,但几乎没有文章讨论过软件设计和开发过程中如何使用现代逻辑学。

“每个人都有自己的逻辑“ 这件事情从古希腊、先秦、古印度时期就有了,各种思想家、哲学家用自己的逻辑体系互相辩论,他们的争辩和我们普通人在日常讨论中的争辩没有特别的不同:概念的含混不清、归因谬误、偷换概念等。

本文抽取一些基本的逻辑学知识,讨论其软件建模中的应用。公认的逻辑学之父是亚里士多德,但我们现在使用的逻辑学基础来源于弗雷格、黑格尔以及莱布尼茨等人的工作。

什么是概念?

这个”设备“ 和你说的 ”设备“ 是一回事吗?

曾经参与一个物联网系统的模型设计,”设备“ 这个词在不同的开发者眼里有不同的概念,为此,讨论 ”设备“ 这个词花费了不少的功夫,最终依然没有怎么定义清楚。

逻辑学中非常重要的一项就是 ”概念“,将概念梳理清楚,软件设计也就非常清楚了。我们来讨论下 ”概念“。

”概念是反映对象特有属性或本质属性的思维形式。“

概念不仅是自然世界中的花花草草、风雨雷电等事物,还有社会经济中的商品、货币、阶级和国家等抽象事物,以及人类精神和思维活动中的思维、意识和情感的等。通俗的来说概念就是人们可以认识世界的对象。

柏拉图的 “洞穴神话” 可以让我们对概念的产生进一步认识。假设有些人住在地下的洞穴中,他们是一群囚徒,生来就在地下,坐在地上背对洞口,不能转头看到洞口,只能面向洞壁。在他们身后有一矮墙,墙后面有些人形的生物走过,手中举着各种不同形状的人偶。人偶高过墙头,被一把火炬照着,在洞穴后壁上投下明明灭灭的影子。这些人终生都只能看到这些影子,会认为这些影子就是具体的事物。

“洞穴神话” 告诉了我们我们每天争论的概念,都是每个人技术、生活背景投射的影子。概念会随着人们对事物认识的加深而变化,尊重逻辑的人不会强行要求某个概念比如按照自己的解释(类比中世纪教会对经书的解释权)。

通过属性认识概念和区分概念

正是因为朴素的概念是来源于个人背景和理解,因此概念统一。后来哲学家认识到人们认识概念是由一些更为基础的属性构成的,那可以认为概念就是由属性组成的。比如 ”人“ 这个概念,属性有四肢、直立行走的行为、皮肤光滑等属性。

这些基本的属性又是一些更基本的概念,如果我们对这些基本的概念达成共识,那么我们就有机会对概念进行统一。类似于 Java 中的类有各种属性,这些属性最终都可以通过 8 种基本的数据结构描述。

因此属性是认识概念非常重要的一件事。属性包含了事物自身的性质、行为,比如黑白、高矮,是否能飞行,是否独立行走。事物除了自身的性质外,还与其他事物发生一定的关系,比如大于、相等、对称、属于等。事物的性质、行为以及和其他事物的关系,统称为事物的属性。

通过属性就能找到概念的边界。 具有相同属性的概念是同一个概念,即使是叫法不同也不应该分为不同的概念。比如土豆、马铃薯,一旦属性的增加和移除都算作不同的概念,比如小土豆是土豆吗?通过属性就就能发现生活中的命名谬误,比如小熊猫并不是小的熊猫,而是小浣熊。

概念的表达

概念只是我们对所认识的事物起的一个名字,词语是概念的自然语言形式,概念是词语的思想内容。

一个概念可以具有多种表达方法,对于软件设计来说,我们可以用自然语言描述概念。也可以通过定义一个类来描述,并在程序运行时实例化这个概念。通过数学或者数理逻辑,我们可以使用集合来描述一个概念。

比如 ”商品“ 这个概念,可以通过不同的方法表达。

自然语言中,商品是指可以通过货币或者其他物品交易的物品,可以是自然实体,也可以是虚拟物品。这是社会经济中对商品的描述,商品具有一个核心属性就是价格,有价格意味着可以交易。

自然语言中,概念和词语之间并不是一一对应的,这是需要日常特别注意的。

  1. 自然语言中,任何概念都必须通过词语来表达,但不是所有的词语都表达概念。在语言中,基本上都会将词分为虚词和实词两大类,只有实词(注意不是名词)可以表达概念。
  2. 同一个概念可以由不同的词语表达,比如前面提到的土豆、马铃薯。
  3. 一个词在不同的的情况下(上下文),可以用来表达几个不同的概念,多义词就是这样,同一个词表达不同的概念,叫做这个词的词项。

如果通过计算机语言来描述一个概念,其实就是面向对象中的一个类,这里定义商品有两个属性名称和价格:

public class Goods{
  private String name;
  private int price;
}

如果用集合的枚举法来表述就是商品就是:

Goods{name,price}

计算机语言和数学语言是一种形式化的语言,是可以精确的描述一个概念,但是自然语言只能通过模糊的给出概念的描述。自然语言翻译成计算机语言的不确定性,带来了无休无止的争吵,但这也是软件设计者的主要工作。

概念的内涵和外延

正是因为自然语言的这种模糊性,为了更加具体的描述一个概念。哲学上概念的共识是概念有两个基本的逻辑特征,即内涵和外延。概念反应对象的特有属性或者本质属性,同时也反映了具有这种特有属性或者本质属性的对象,因而概念有表达属性的范围。

  • 概念的内涵是指反映在概念中的对象特有属性或本质属性。
  • 概念的外延是指具有这些属性的所有对象,即囊括对象的范围。

例如商品这个概念的内涵是 ”能进行交换的商品“,本质属性是能进行交换,从本质上区别产品。它的外延就是投入市场能卖的所有事物。

对概念的外延的清晰描述对我们设计软件产品的定位非常有帮助,我们购买软件服务无非两种情况,生活娱乐使用,或者工作使用。马克思社会经济学精妙的描述为生产资料、生活资料。这其中的逻辑完全不同,按照生活资料的逻辑设计一款生产资料的产品注定要走弯路。

概念的内涵和外延在一定条件下或者上下文中被确定的,这取决于参与人的共识。严格锁定概念的内涵和外延,不能帮助我们讨论问题和改进软件模型。随意修改内涵和外延这是典型的偷换概念和诡辩。

概念的内涵和外延是一个此消彼长的兄弟。当内涵扩大时,外延就会缩小,概念就会变得越具体。当内涵缩小时,外延就会扩大,反映的事物就会越多。

在面向对象软件建模中的影响非常明显。对象特有属性或者本质属性越少,那么这个对象能被复用的场景越多,也就是内涵越小。反之,特有属性越多,能被复用的情况就越少了。软件建模过程中随意修改概念往往意识不到,但是每一次属性的添加和移除都带来概念的内涵和外延发生变化。

非常典型的一个例子发生在订单模型中。一般来说,我们会把支付单和订单分开设计,订单的概念中没有支付这个行为,但有时候觉得支付单的存在过于复杂,会将支付单的属性放到订单中,这个时候订单的内涵和外延变了。

内涵和外延发生变化但是设计人员没有意识到,会使用同一个词语。一旦使用同一个词语就会产生二义性,二义性的存在对软件建模是致命性打击。比如用户维护的地址、地址库中的地址、订单中的地址,这三个 ”地址“ 虽然名字相同,但是内涵和外延不同。

意识不到概念的内涵和外延,是无法设计好逻辑良好的软件模型的。

对象命名就是下定义

变量命名和缓存失效是编程中最让人头疼的两件事。

变量命名其实就是在给一个概念下定义。定义是揭示概念的内涵和外延的逻辑方法,一个准确的定义需要反映出对象的本质属性或特有属性。下定义困难普遍有两个痛点:

  1. 不懂好的下定义的逻辑方法。
  2. 对业务概念或者领域不熟悉。

对于第一个痛点,根据概念的属性、内涵和外延,逻辑学中有一些很好地下定义方法。

属加种差定义法。这种下定义的方法通俗来说就是先把某一个概念放到另一个更广泛的概念中,逻辑学中将这个大的概念叫做 ”属概念“,小的概念叫做 ”种概念“。从这个属概念中找到一个相邻的种概念,进行比较,找出差异化本质属性,就是”种差“。比如,对数学的定义,数学首先是一种科学,和物理学处于同类,它的本质属性是研究空间形式和数量关系。于是可以得到数学这个概念定义:数学是一种研究现实世界的空间形式和数量关系的科学。

用这种方法给订单、支付单、物流单下一个定义:

  • 订单是一种反映用户对商品购买行为的凭据。属概念是”凭据“,种差是”反映用户对商品购买行为“。
  • 支付单是一种反映用户完成某一次支付行为的凭据。属概念是”凭据“,种差是”用户完成某一次支付行为“。
  • 物流单是一种反映管理员完成某一次发货行为的凭据。属概念是”凭据“,种差是”管理员完成某一次发货行为“。

在逻辑中可以参考下面的公式:

被定义的概念 = 种差 + 属概念

对于第二个痛点,这不是软件建模能解决的问题,需要充分和领域专家讨论,获取足够的业务知识。人们对概念的定义或者认识是随着对事物的认识不断加深而变化的。一个完全对某个领域没有基本认识的软件工程师很难做出合理的软件建模,例如银行、交易所、财会等领域的软件需要大量的行业知识。

我们做消费者业务的互联网开发时,往往因为和我们的生活相关,所以这种感受并不明显。当做行业软件时,领域知识对软件模型的影响甚至是决定性的。

总结

软件是现实世界在计算机领域的投影,而面向对象建模是软件的骨架。理解面向对象实际上就是在理解哲学、逻辑学,理解概念在现实世界中内涵和外延。也需要理解概念在现实世界中的上下文,也就是逻辑学中的 ”论域“。

对业务模型明确的给出定义,就能给出清晰地对象设计,划分出软件模块。

more >>

程序员的美德:克制、简单和笨拙

Perl 语言的发明人 Larry Wall 一个经典叙述:优秀程序员应该有三大美德:懒惰、急躁和傲慢。

在他的世界观里,懒惰可以驱使你善于利用工具,减少重复劳动;急躁可以驱动你写出高效运行的程序;而傲慢可以让你写出让别人挑不出毛病的程序。

这几项是我进入编程行业一直信奉的真理,这些 “美德” 对于新手来说是很好的提醒。工作很久之后,见识过各种项目以及所谓的最佳实践,对编程有了不同的认识。

我想加上程序员需要坚持的另外一面:克制、简单和笨拙。

克制

技术发展的如此之快,JSP 已经几乎被淘汰了,取而代之的是前后端分离、微服务系统等时髦技术。面对这些新技术时,克制是让你远离很多麻烦的第一步。

曾经有一个客户需要一个简单的信息录入系统,用于管理一些基础的业务需求。我们准备构建一个后端服务来提供 API,团队中一名同事提出想要使用微服务,并坚信所有的新项目都不应该使用 “传统”的单体系统。

后来我们花了半个月的时间构建了后台服务,又花了一周的时间构建了 BFF 为浏览器提供 API 调用,同时以为这两个服务分离开来,又不得不做出一些额外的工作完成鉴权能力。

那位同事显然没有意识到 “微服务” 的本质是一种分布式系统,满足同样的业务需求需要付出巨大的成本。

简单

IT 行业有一个有趣的现象。

每一次技术潮流的到来,并不是这项技术有多大的创新。而是产生了某一个框架让原本高深晦涩的技术变得简单,从而让我们这种低端的程序员也能掌握。

Spring 对于 Java EE开发的简化,让复杂的企业级开发变得足够简单。

深度学习框架对于数学模型的简化,让人人都能参与 AI 开发,即使只是调参侠。

区块链框架对于分布式合约的简化,避免应用开发者需要下场处理分布式问题。

ElasticSearch 对于搜索引擎的简化,让搜索引擎应用得以普及。

这些技术能普及,最基本的原因就是简单。

相反的例子就是传统 Java EE 中的各种设计模式,以及 DDD 中的各种概念,工厂、facade、Event Sourcing,这些模式和概念必然难以在实际项目中推动。

当我在现实中听到某个程序员说在项目中使用了多少种设计模式并引以自豪时,最好的方法就是远离他。

笨拙

笨拙是最难的一个美德。

有太多的程序员,包括我,脑袋里装满了各种高大上的想法,见到一个高级技巧或者技术便如获至宝。

JavaScript 的语言中充满了各种精巧的设计和技术,闭包、原型链等高阶知识是面试必问的。

《代码整洁之道》和《重构》中各种高级的编码技巧。

Scala 中各种不同的编程范式,C++ 中的模板、运算符重载等高科技。

我以为我永远到不了编程大师的水平。直到后来阅读了一些源码, Backbone.js 和 Redis 让我印象深刻。朴实无华的设计,有很多逻辑判断写的非常普通,没有过多的设计模式。

当我知道 Intellij IDE 是用 Swing 开发而成的时候,惊叹不已,用最简单的食材做出最美味的食物,这才是真正的编程大师吧。

more >>

提高工时估计准确性,减少开发成本

估时不准是很正常的事情

“如果一个程序员告诉你他已经完成了 90% 的工作量,那么他还需要同样的时间完成剩下的 10%”。

软件项目延期和跳票是众所周知的事情,其中不乏知名项目。

刚毕业的时候,我在一家做系统集成的公司工作,我们定制了一套售票软件,为景区接入互联网售票方案。供应该软件的软件公司非常自信的说,这东西非常简单,最多 2 个月就能搞出来。这家公司小有名气,我老板对 2 个月交付深信不疑,于是张罗了接入的客户、市场的物料等。

不过还是没能逃过 90% 定律,2 个月交付的东西只能算作一个 Demo。于是花了另外一个月测试,修复问题和完善业务逻辑,又花了另外几个月时间响应对接客户的条件,才逐渐稳定下来。

行百里者半九十,软件开发也大体如此。开发者估不准工时常有,估准了才奇怪呢。

后来自己也做了软件工程师,参与 it 项目开发,项目延期也是非常常见的事情。

工时估算的前提是质量

IT 团队能准时交付是一项非常有价值的能力,哪怕交付时间长一点。计划两周交付,最后能准时完成,比承诺 1 周时间,但是花了三周才交付重要得多。

越是大项目,越是重要。大项目的各个组件可能会发生相互依赖。如果不能准时交付,就会付出团队等待的情况,那可是真金白银。

做过项目管理的都知道甘特图,甘特图的每个泳道表达了项目各项资源的进展和计划。然而,软件项目不确定性非常多,各种突发事件。如果能提高准时按质量交付,各个单位的等待成本会小很多。

关键的是,衡量是否准时交付的关键是质量,其次才是交付。先给一个 demo,然后再慢慢改 bug。这种 “准时” 的交付,还不如有一个明确的延期时间,本质上还是 “猛糙快”。

谈项目工时估算,前提是在满足质量要求的前提下,否则估时没有意义。

估算准确依赖工程能力

那么能不能提高软件工程工时的估算的准确性呢?其实是可以的,刚到 ThoughtWorks 的时候,参与了一个交付项目。在一个项目开始前就计划了项目结束的时间,以及下一个项目的计划和安排。结果让我非常吃惊,那个项目的结束时间,和预期只有 2 周左右。并且这两周是逐步减少开发人员,最后只有 1-2 个人负责最后一周的交接期。

这就是专业软件团队和小作坊的差别,在专业项目经理带领下能把 3 个月的项目估算,精确到 20 - 30 个人天。能把项目工时估算到这种程度,体现了 PM 的内功。

在一个敏捷团队,需要把工时估准,不在于 “估” , 而在于团队执行项目的稳定性。一般来说,准确估算工时需要考虑需求分析程度、任务拆分的合理性、技术方案的可靠性、团队成员的能力、外部依赖和环境,如果这个项目不是新项目,还需要考虑遗留系统改造的成本和数据迁移的成本。

需求和任务分析

只有需求分析非常彻底才能保证估算的输入条件。非专业的业务分析师,只能看到需求冰山水面上的部分。软件的特性、功能的复杂性等。

专业的业务分析师,不仅需要看待功能需求,并对功能需求的逻辑性考虑完备。比如用户需要一个 APP,他实际上还需要一个后台,对应这个后台会有不同的用户、角色等。

根据这些业务输出,拆分出任务,敏捷开发中我们叫做用户故事,一个用户故事代表一个合理拆分的业务逻辑。能被评估工作量,然后根据这个工作量来评估工时。

除了这些功能需求之外,还有非功能需求。客户不仅仅需要一个 APP,还可能需要的是一个安全的、高性能的、国际化的 APP,而这些往往被客户当做默认选项。

一些性能优化的指标需要分析,并考虑性能优化的任务工时;安全需求可能有 HTTPS 配置,防病毒扫描等,都需要考虑;国际化也是额外的工作量。

挖掘用户真实需求的目的是定义怎么才算完成(Define of done),如果没人说得清楚满足什么条件这个项目才算完,那么估算工时根本无从谈起。

彻底挖掘客户的真实需求是评估项目工时的首要条件。

技术方案和团队能力

技术方案和团队能力和项目时间估算很大关系。很多项目的时间估算都是技术经理或者 Teach lead 来完成,往往是他们按照自己的经验和能力进行计算。光是这样,很难算的准的。

团队有多少人?对这套技术方案的熟悉程度如何?方案是否会发生较大的调整。人越多,人员水平差距都为工时估计带来了不确定性。经验多的人来做方案,如果是他做过的相似方案,自然会估的稍准一点。但大多数情况下没有这么理想的场景。

要做好工时估算,需要结合技术方案和团队成员能力,而不是自己能干多少活儿,多快干完来算。

一方面,技术负责人需要安排相应的技术预研,走在实际编码的开发人员前面,探探路,验证方案的可行性、实施难度、风险。就像作战的侦查人员一样,我们把预研叫做 spike。 spike 需要输出一些结论、demo,支持项目的时间估算。

另外一方面,考察团队真实运作效率很好地方式是根据迭代做工时统计。按照两周为例,10 个人的团队是 100 个工时。如果按照之前的估算,2 周内需要完成的 100 个工时的任务,实际上只完成了 50 个工时。也就是进度只有 50%。

我这个算法比较粗糙,敏捷项目管理中还有更准确的速率计算方式。通过速率,就能对下一阶段的工时估算做出调整,并在工作量、人员上做出调整。

通过方案预研和速率计算是提高项目工时估算准确率的良好方法。

遗留系统和外部依赖

我常常花了一下午时间完成了某个特性升级的编码,但是花了一个月的时间才完成了线上平滑升级、数据迁移。

真正有经验的工程师都知道,方案设计的难点往往不在设计一个新东西,而在于演进一个老系统。遗留系统演进是不可避免的,这种历史包袱是造成工时估算不准的一个重要因素。

遗留系统演进带来的估算困难来源下面几个方面:

  • 前置条件不满足或者很困难。客户可能觉得只是添加一个小功能,但是涉及数据库变更、API 升级。软件项目往往牵一发而动全身。
  • 遗留系统代码难以理解,没有人说得清楚原委。这种系统往往伴随着重构,否则难以进行。
  • 数据迁移的成本。例如,需求只是简单要求对用户的某些数据加密,实际工作包括了对存量数据的迁移。
  • 临时代码的清理。遗留系统往往为三个状态,原始态-过渡态-最终态,很多人估算工时要么忘记了过渡态,就是忘记了最终态的时间成本。

不负责的猜想,有一些客户就是遗留系统演进不下去了,然后招标做新功能,实际意图是想乙方顺便消化重构的成本。总之基于遗留系统的二次开发都是一件困难的事情,能不接就不接吧。

另外,社会分工意味着一个人干不完所有的事情,IT 项目往往一个项目也不是独立的。大多数情况下需要和外部条件进行集成,这部分时间超出我们的掌控。

集成这件事的成本需要试主动集成还是被动集成来说:

  1. 需要等待别人提供服务,我们去集成。在估计工时的时候,一定要把对方的交付时间考虑进去,提前沟通,并建立契约。
  2. 提供给别人服务,被别人集成。这种情况估计工时,往往只是计算到 API 上线,实际上还需要考虑一定的支持、文档工作量。很少有一次到位的情况,大多要磨合一段时间。

集成充满了不确定性,估算工时时需要预留足够的集成空间,才能让工时估算更准确。

总结

项目工时估算是一个系统性工作,基本上很难有一个万能的方法。因此大多数情况下都是玄学,但是毕竟是 “估” ,也不能要求 100% 精确。

软件工程的估时更具有弹性,相对供应链管理的交付时间估算成本更低。做好估时,对减少项目运行成本和风险有巨大意义,工时估算的准确性也往往体现了一个 IT 团队工程能力。

more >>

学习"知识",而不是各种框架的说明书

Vue 出 3 了,Spring 又更新了版本。网上一大票程序员叫学不动了,求不要再发布新版本了。

虽然工作多年,我时常也觉得学不动了。有时候再想软件行业永远也学不完吧。

一次偶然,我在 coursera 浏览了一个课程,它的前置课程是 discrete mathematics,也就是离散数学。我在想作者为什么要把离散数学放到一个应用的计算机课程中来呢?

我的专业是计算机网络,很遗憾没有学过离散数学,只学过微积分。于是我去看了下离散数学的介绍,有空去学一下。

看完介绍之后,后背发凉。

原来离散数学是很多计算机基础课程中的基础。数据库理论中的关系演算、哈弗曼树、笛卡尔积;计算机网络中的路由算法、通信原理;编程中用到的各种位运算技巧。这些知识都是离散数学的应用,难怪我在学习计算机网络和数据库理论的时候少了点什么支撑。

如果说计算机网络、数据库原理是知识的话,离散数学应该是知识中的知识。如果没有更加基本的知识中的知识,那么学到的这些计算机知识就像一盘散沙无法找到源头。

我才幡然醒悟,知识中的知识才是我们应该首先学习的东西,掌握了基础才能对应用知识顺手拈来。

因此我把计算机科学中的知识分为了两类:

  • 计算机的一些原理性的知识,例如数据库、分布式系统、操作系统、编译原理
  • 还有一类是流行的软件、库、中间件的使用方法

在程序员的生涯中学了太多的知识,与其说是知识不如说是在阅读某个开源软件的说明书。甚至是这些说明书的解释,更胜一步是翻译过来的中文材料。

于是就产生了程序员的知识获取鄙视链:

搞研究阅读论文的瞧不起读协议做实现的,读协议的看不起那些只能读某个实现软件的官方文档的,读英文文档的看不起读中文文档的,读中文文档的看不起看翻译版的,看翻译官方文档版本的看不起读周边书籍的。

处在鄙视链的最低端就是看视频教程手把手跟着学的。很不幸,我就是那个喜欢看视频教程处在鄙视链最低端的程序员。

更为具体的例子就是开源框架。基于 MVC 设计模式的各种框架不说上百种,也有几十种了。Java 中的 Spring MVC、Struts,PHP 中就更多了 Yii、CakePHP 等。Spring MVC 的文档是一个 MVC 框架的说明书,相关书籍时这个说明书的解释。

随着现代软件工程项目的复杂性越来越高,一个项目中引入的第三方软件包数十个,切换一个项目又需要重新学习另外的第三方软件。除此之外,还需要用到一些工具,IDE、Git、deveops 工具。如果涉猎架构和运维相关,还需要对基础设施很了解,docker、aws、ansible、keepalive 等。

这真的是 “生也有涯而知也无涯”,慢慢的我开始尝试取舍。

  • git 只是一个代码管理工具,用 Intellij 中的快捷键又快又好的完成了工作。
  • MVC 框架架学透一个能用的就好。
  • 前端的各种框架,快速读一下文档,了解一下直接在项目中使用。

上面这些东西都只能叫做“开源软件说明书”,下面的叫做 “知识”。

  • 计算机导论,计算机运行的基本原理。二进制、字符串编码、位运算等
  • 操作系统,计算机软件实现的基础。线程、进程、中断方式等
  • 数据结构和算法,优化程序性能的基础。数组、链表、HASH和各种树
  • 数据库原理,优化各种关系数据库的基础。索引原理、关系演算、范式理论等
  • 计算机网络,互联网开发的基础。HTTP协议、DNS 的原理
  • 密码学,计算机安全的基础。对称加密、非对称加密和各种签名算法
  • 编译原理,理解编程语言的基础,造轮子的基础。语法分析、词法分析、闭包等

阅读“开源软件说明书”的关键在于英语,现阶段大部分流行的开源框架和库都是英文文档,阅读英语是一个至关重要的能力。而学习计算机科学基础知识在于耐心,如果说学习 git 需要 4 个小时的课时的话。数据结构和算法起码需要 100 个小时,花费在计算机科学基础上的时间还需要包括实践,实验。

程序员持续学习的关键在于,层出不穷的框架、库和工具让人眼花缭乱,忘记了真正的知识是什么。也许当有一天,抵御了计算机行业中各种“时髦技术”的诱惑后,才能有所精进吧。

more >>

DDD 就是把面向对象做好

在 MVC 架构下写代码一直有一个问题,业务逻辑应该写 C 呢,还是写 M。

写 C 就变成了事务脚本的代码,虽然比起不用 MVC 的都多入口架构好很多,但是业务逻辑还是会重复和混乱。写 M 很多逻辑又写不下去,业务逻辑往往需要跨模型处理,大量业务逻辑难以落到 M 中。于是大家就引入了一个 S (Service),变成了 MVCS 架构,这个架构有很好的群众基础,逻辑清晰容易使用,被广泛采用。

在 DDD 重新热起来之后,事情有一点微妙的变化。很多 DDD 的支持者,认为将业务逻辑写入 S 不符合 DDD 的充血模型的逻辑,应该将按照 DDD 的书将业务逻辑写到领域模型。

于是我在一个项目中见到一段意思的写法,将业务逻辑大部分写在模型中,其中有一个订单对象,有价格等相关属性,并提供了计算、结账等方法,大概如下:

class Order{
    private List<Product> products;
    ... 

    public void checkout(){
    ...
    }
    public void calculate(){
    ...
    }
}

有人觉得别扭,订单居然能自己结账了;有人看过 DDD 相关的书后,觉得很香,这个类有良好的“封装”。这就是由来已久的充血、贫血之争。

充血模型、贫血模型

面向对象在处理对象的存储时,有两种风格一直争论不断。将业务逻辑放到领域对象中,对象不仅需要承载数据也需要承载行为,这种编程逻辑被称作充血模型

例如,计算订单的价格

// 下单方法
public void order(){
    Order = new Order();
    order.setId("UUID");
    order.setPrice(100);
    ... 

    order.calculate();
}

将业务逻辑放到领域对象之外,领域对象只承载数据,以及一些 getter、setter 方法,业务逻辑被另外的类(service)来承载,这种编程模型被称作贫血模型。

// 下单方法
public void order(){
    Order = new Order();
    order.setId("UUID");
    order.setPrice(100);
    ... 
    order.setItemCount(countItems());
}

private int countItems(){
    ...
}

充血模型和贫血模型还有另外一层含义,那就是谁来负责持久化他们。

一个简单的例子,充血模型中 order 对象可以调用 order.save() 来保存它自己。在一些动态的语言 ORM 框架,借助器动态语言特性的优势,往往会这样设计。典型的是 ActiveRecord 模式,数据库的一行记录、XML 的一个 元素被读取到内存中后,可以对应一个活跃对象。代表的框架有 Ruby On Rails、Yii、CakePHP 等。

// OrderService
$order = new Order();
$order->setId("UUID")
$order->setPrice(100);
...
$order->save();

贫血模型的持久化依赖另外一个对象。在 Hibernate 中,可以使用 EntityManager 的 persist 方法来持久化,或者使用 JPA 的 Repository,使用 Mybatis 的话就需要使用 Mapper 来完成持久化。

// OrderService
Order = new Order();@PersistenceContext
protected EntityManager entityManager = null;

public void saveOrder(){
    Order = new Order();
    order.setId("UUID");
    order.setPrice(100);
    ... 
    entityManager.persist(order);
}

总结一下,动态语言更容易实现充血模型,静态语言更容易实现贫血模型,但是并不是说静态语言就不能使用充血模型和 ActiveRecord 只是稍显麻烦。

各自的局限性

充血模型的好处是可以封装一些业务逻辑,避免面向模型的开发退化为事务脚本化的代码。事务脚本化的代码在前面的文章中已经讨论过,这是一种面向功能的开发方法,而非模型的开发方法,会造成代码中业务逻辑重复,一致性差的问题。

因此在 DDD 的相关书籍以及 《企业应用架构》中被反复提及,编程大师们大多鼓励使用充血模型。面向对象开发的早期就是充血模型,当我们接触面向对象的第一堂课时,老师都是拿现实世界中事物作为例子。

一个汽车由四个轮子构成,并且能跑,我们定义它的类就是:

Class Car {
    private List<Wheel> wheels;

    public void run(){

    }
}

然后实现这个类就能描述我们的业务逻辑,充血模型可以将业务逻辑封装到领域模型中。

但是充血模型容易陷入一个困境,就是封装的层次难以维系。上面订单的例子,在实际开发过程中很难将很多业务逻辑落到模型层,例如订单计算可能需要商品、用户积分等其他模型。除了单个模型,批量业务逻辑也很难实现。

于是从 EJB2 开始倡导使用贫血模型,将业务逻辑封装到 Service 这类专门承载业务逻辑的对象,Order 这类的模型只需要承载数据结构。贫血模型,让面向对象变得非常轻量, Spring 大规模推广开之后尤为明显。

面向对象跳出 ”车有四个轮子,车能跑“ 的局限认识,换句话来说,”订单有多个订单项和总价,但是订单不能自己结账,应该由收营员结账“。贫血模型的本质是将不合理的行为从模型中抽离出去,订单模型负责承载数据,收银员对象负责承载行为。

// Cashier.java 
class Cashier{
    public final static Cashier INSTANCE = new Cashier(); 
    public Bill checkout(Order order){
       .. 结账逻辑
    }
}

// Order.java 
class Order{
    private List<OrderItem> items;
} 

Cashier 可以实现为单例,Order 作为 POJO 存在,这就是 Spring 这套开发模型的基本逻辑。贫血模型很容易将代码写成事务脚本,因此业界尤其是 Mtin Fowler 等人鼓励将更多的业务逻辑实现为充血模型。

主体、客体和面向对象

”车有四个轮子,车能跑“ ”订单有多个订单项和总价,但是订单不能自己结账,应该由收营员结账“。这两种面向对象的思维模型都没有错,关键的问题是在何种场景下合理使用。

我们使用餐厅作为例子,开启上帝视角,对事物有仔细的观察。一个典型的餐厅有什么呢?

  • 桌子
  • 菜品
  • 订单
  • 服务员
  • 账单
  • 菜单
  • 厨师
  • 服务员

假定需要实现下列需求:

  1. 对桌位进行预定
  2. 点菜下单
  3. 结账
  4. 开发票

我们用哲学认识世界的方法,把按照操作的主体、客体进行分类,主体和客体是认识世界很好的方式,也是真正理解面向对象的有效途径。

主体-客体问题是一个长期存在、关于人类经验分析的哲学论题,产生于这样一个前提:世界由客体(实体)组成,主体(观察者)知觉或假定客体作为实体存在。-- 维基百科

假定这个餐厅非常大,在断电的情况下,无法使用收银机和软件操作,需要不同的人各司其职,并用纸和笔完成正常运作:

场景 主体 客体
预定 负责预定的服务员 桌子
点菜下单 负责点菜的服务员 菜单、订单
结账 负责结账的收银员 订单、账单
开发票 负责开发票的服务员 账单、发票

我们用语言描述一下这个店的工作过程:

顾客打来电话需要预定桌位,负责预定的服务员在预定表格上预定了桌位。顾客到来后,负责点菜的服务员拿出点菜便笺,点了菜撕下复印的菜单给了后厨。顾客吃完饭后让收银员结账,收银员结算后,归档了小票。负责开发票的收银员,开具了发票。

现在我们需要信息化这个系统,如果按照一般的充血模型,桌子有预定方法、订单有结账方法,这样并不符合逻辑,其实也不符合面向对象的思想。正确的面向对象,应该对主体进行 ”拟人化“ ,对客体进行 ”拟物化“。

现实世界中的服务员、收银员变成了预订服务和订单服务。于是就写出了大家所熟悉的这种编码模式:

UseCase 主体 客体
book ReservationService Desk
order OrderService Product、Order
checkout OrderService Order、Bill
applyVoice OrderService Order、Voice

主体认识客体的过程,也是主体改造客体的过程。

领域模型是客体,领域服务是主体,应用程序的本质是认识世界(读),和改造世界(写)的过程。主体和客体是可以互相转换的,收银员能操作订单,另一方面如果需要,收银员会被商户管理员作为人员操作。

现实中的收银员反应在计算机系统中时:

  • 作为主体,就是可以管理订单的领域服务
  • 作为客体,就是被商户管理的人员和角色

面向对象就是计算机软件对现实世界的阐述,当我们能直观的描述业务场景,就能很好的编写代码。

通过这种认识论我们发现,充血模型和贫血模型并不矛盾。如果你在编写一个游戏或者前端的可视化工具,充血模型无疑非常好用;但是如果你是编写一个信息化系统,并将充血模型滥用到客体对象上,则十分痛苦。

充血模型和贫血模型的问题应该从如何选择演变成如何使用的问题,充谁的血,在哪里充血。

合适的 ”充血“

回到这个朴素的逻辑命题: 程序 = 数据结构+算法。

这个说法其实一直都不过时,领域服务也是领域的一部分,不应该强行让领域模型充血,充血的过程由领域服务完成。换句话说,领域模型,拟物化,体现了数据结构;领域服务,拟人化,体现了算法。

对象拟人化是一个非常好的实践,我在一个项目中做了一个非常小的功能,需要定时拉取服务器配置并切换应用中的数据库连接。

我设计了 4 个对象:

  • ConfigPoster 配置传输者
  • ConfigRefresher 配置刷新者
  • ConfigContainer 配置容器,用于承接本地配置
  • ConfigPackage 配置传输包

ConfigPoster、ConfigRefresher 是主体进行拟人化命名,ConfigContainer、ConfigPackage 是客体,进行拟物化命名。通过这种设计,避免了滥用的充血模型。

当然你可能说,在 CURD 项目中无法体现这类思想。那是因为一般的管理系统,业务很简单,无需过于进行面向对象设计,于是容易写出 Service 扮演所有的主体。

  • 主体就是一些管理者,ProductManager、UseManager
  • 客体就是一些简单的对象,Product、User

但是,很难说所有项目都是简单的 CURD 操作,这种情况往往是业务逻辑没有分析清楚,CRUD 项目带来的业务价值非常少,即便如此,也可以有意识的将对象拟物化、拟人化。

例如:操作订单的对象命名为 Cashier、XXXManager。开源项目尤其擅长此道,Spring Security 中TokenExtractor 用来从 HTTP 包中抽取 token; ETL 工具 kettle 用厨房中的事物 Spoon、Fork 代表各种模型,构建了非常容易理解的对象系统。

在实际使用 DDD 的过程中,领域服务可以和聚合成对出现。在避免了事务脚本的模式的同时,具有良好的封装性,能更好的对现实世界的阐述。

而通过主体和客体对模型进行分解,有很多好处:

  • 容易理解
  • 符合面向对象的直觉
  • 聚合根容易存
  • 批量问题被处理

通过对面向对象的进一步理解,这些问题也就迎刃而解了,当然学习 DDD 的过程也是重新认识这个世界的过程。

more >>

好领导,本来应是挖渠人

大多数谈论经管类的文章,作者都是企业高管,或者社会名人、公知。视角往往都是自上而下,先从战略规划,再到具体实践,高屋建瓴,视角宏远。作为一个长期一线的程序员,大多数情况下都是被管理者,我在想是不是也能从另外一个视角发出自己的声音,对我们日常团队协作有些许帮助。

手机配件送货员的故事

我比较幸运,无论是读书时期的兼职零工,还是毕业后的正式工作,和老板(直属领导)关系都还行,没有发生一些不愉快的事情。不过对身边发生的一些见闻感触很多。

高中毕业的时候在老家的小城市找了一份暑期兼职,工作的内容就是在手机零配件批发部给手机维修师傅送货。那个时候还是功能机时代,手机维修的需求很多,手机维修需要一些屏幕、听筒、排线等配件。城市不大,骑个自行车市区10 -20 分钟内大部分地方可以送到,所以老板喜欢招一些年轻小伙儿送货,人力便宜还身手敏捷。当然也有中年人做这个,临时混口饭吃。

我们店的老板姓陈,每天坐在店里接电话出货。大家都叫他老陈,为人很和气,不管是客户还是员工遇到的问题都尽力帮忙解决,跟他干还是比较开心。手机和配件这个行业在当时的小城市属于暴利行业,一块商务电池进价 20 元,给维修师傅的价格是 60 元,最终顾客需要支付 100 - 200 元不等。

隔壁陆陆续续开过几家新的批发铺子,都不太长久。有一次中午吃饭认识了一个隔壁的送货小哥,我叫他黄哥,给我说干啥都不容易,他们那个老板很烦,经常数落他们。客户(维修师傅)有时候摸不清需要更换什么配件,往往弄错,于是他们就来回跑。浪费时间不说,空跑也不挣钱,老板就责怪他们为啥不问清楚再送,如果维修师傅经常这样,就加钱或者不送了。

我感到很惊讶,这算啥事儿啊。我们之前也遇到这些问题,就给老陈吐槽这事儿。老陈说你们来回跑怪累的,我这就去买一些包,下次你就把常用的都带上,有个包放收据啥的也方便。工作中这样的例子很多,有啥事儿就给老陈说,一起想想如何改进也就解决了。

优秀的管理者,应是挖渠人。利用自己的位置和资源帮助解决问题,让员工工作流畅,就像春耕时期引水灌溉一样,及时疏通水渠中的石头和杂草,而不是怪水流的太慢。

ThoughtWorks 的一个同事

刚到 ThoughtWorks 的时候被分配到国内某项目,负责前端开发,构建一个报表页面。我们组就两个人,一个前端,也就是我,还有一个后端。

项目比较简单,我很快构建好了 React 的前端页面,因此在 Interview ++ 中的评价还行。后端那个兄弟使用 Spring boot 构建 API ,并需要连接 LDAP 获取公司的员工信息。他的工作并不顺利,花了大概两周才完成了 API 构建而且质量不佳。

后来我参加他的 Interview ++,得知他有10年以上的工作经验,之前一直都是写 C++ ,因为没有合适的项目来这个项目上写 Java。他的 Interview ++ 评价很不好,“技术能力一般,和工作经验不匹配” “不主动交流” “接受反馈的能力不好” 等。

因为项目上只有两个人,我们私下关系比较好,他虽然不是很健谈的人,但是具有非常好的沟通能力,理性和独立思考。我能理解他被给负面反馈的原因,无非是他工作在一个不适合他的位置,当技术能力无法满足项目需要时。在团队眼里,优点就会被弱化,缺点被放大。

很多人可能会说,写得好 C++ 的人写 Java 也会很快上手。但是客观事实是,计算机语言虽然很容易跨越,但是领域很难。长期重试嵌入式工作的人在没有人知道的情况下,在两周内上手 Spring boot 这种 “低级“ 技术很困难,因为需要很多服务器编程的背景知识。

在某个平行宇宙,他会不会上了一个他擅长的项目,被当做大神膜拜。“不主动交流” “接受反馈的能力不好” 这些反馈还算不算缺点呢?

优秀的管理者,应是引水人,把员工放到合适的地方,用合适的期望评价工作产出。需要遵从科学发展观,再好的水也不能逆势倒流

一个客户的故事

”我说过好多次了,提交代码前需要本地跑通,现在流水线又挂了“

”我昨天就强调,没用的代码要删除,不要留在项目里面“

”你这个代码能不能写的规范一点,这么乱“

晨会开了半个小时后,我无奈的放下耳机。在国内的一个项目上,我被派到一个团队和客户一起工作,这几乎是每天早上的状态。每天主持晨会的人是客户一个工作了很久的人,他似乎很恼怒,责备负责管理供应商的人为什么招聘了这么多能力差的人。

我入场后,分配的第一个个工作是改造一个旧项目,这个项目因为某些设计遗留问题,遇到了性能瓶颈,需要优化。一个负责相关业务的人来给我讲了一下背景,然后给我开通了一个代码仓库的权限,我就这么开始工作了。

当我开始下载好代码后,头皮发麻,这个项目没有任何文档介绍如何启动。甚至我不知道这个项目在整个系统中处于哪个位置,我开始四处求助。我找遍了整个办公室的人,得到了很多收获。

  • 想要把项目跑起来,本地需要安装 Redis、数据库等依赖,不过需要从同事那里先拷贝一些特别的配置,并注释几行代码。

  • 想要了解整个项目的背景,找了同事 A、B、C 终于拼凑出了这个项目的概况。

  • 项目没有任何规范,全凭自己发挥,至少有三种加解密的工具类,多种序列化方法。
  • 想要部署到 Dev 环境,发现每个人建有自己的流水线,部署方式各不相同。

最终这个项目做的非常头疼,技术经理没有打通整个工作流,整理相关的文档、规范、部署方法,让开发把主要力气用在开发上。每个人都需要摸索一套自己的做事方法,做东西很难说高效和”规范“ 。

优秀的管理者,应是治水人,像都江堰的鱼嘴、堤堰,疏、堵相适宜。设计工作流、协作体系,制定规范。

more >>

DDD 指导团队划分问题

前面提到过,促使公司将应用垂直拆分的一个原因是团队工作在一个项目上,团队过大无法展开。虽然将应用拆分的原因有很多,但是因为团队过大是所有原因中最被低估的一个。互联网应用往往是因为人多才拆成几个服务,而不是因为是几个服务所以才人多的。三五个人几条枪,被大公司忽悠,一个小项目拆成了十几个服务,这种技术很”吓人“的项目往往结局也很吓人。

刚入行带我的老板是从一家大公司出来创业的,随着团队扩大,他老挂在嘴边的一句话是,”一个50人以上的团队是没办法管理的“。当时不以为然,后来工作久了,参与过数百人一起开发的电商系统,经历过一个 Java 团队就有30 人的项目。

深以为然,甚至更为悲观,认为超过 15 人的团队都是难以被管理的。如果是敏捷团队,人数过多会很麻烦。敏捷有很多实践,有站会、codereview、计划会议、回顾会议等需要全体参与的会议。10 人以上的团队都难以让站会在 15 分钟内结束,codereview、回顾会议在 30 分钟内结束。超过这个时间,还要不要写代码修 bug 啦?

对于一个大型项目来说,服务化正是为了解决这个问题。通俗一点,团队划分的问题就是谁和谁坐在一起的问题,比较合适的人坐到一起工作效率更高不是吗,对于互联网公司而言,组织架构需要和技术架构适配才能取得最好的效果。回答这个问题,我们需要先看下各种公司的组织架构形式。

IT 行业企业组织架构类型

一些管理书籍将组织架构分为:金字塔式、扁平组织架构、矩阵制组织形式。对于互联网行业的公司来,我用更为通俗的说法,和现实中的经历,重新划分了一下:

  • 大部门式的组织架构。相同职责的人被收集到一起,形成了前端、后端、测试等大部门,典型的科层制的组织结构。
  • 矩阵制组织形式。大部门式的组织架构上根据项目再动态划分,行政管理和项目双线制。
  • 自由市场式的组织架构。这个公司就是一个大市场,各个单位按照一定形式混编,甚至管理团队也作为一个单位为公司提供服务。
  • 分形组织架构。基层由较全功能的小团队构成,多个小团队构成一个中层大部门,最后各个大部门构成整个公司。整个公司可以看做大型企业,每个大部门可以看做一个中型企业,每个基层团队可以视为一个创业公司。

image-20200517161135173

大部门式的组织架构

有一些公司是按照工种来划分的,客户端、测试、产品经理被分到不同的部门。我曾在一个这种类型的公司任职,客串过前端部门和后端开发部门。因为业务决定了后端开发的工作量远多于客户端和移动端开发,因此部门之间极不均衡。

image-20200517104005548

我们有 40 人的后端开发部门,10 人的产品经理部门,30 人的测试部门。这些人的工作部门领导分配,任务的流动方式是市场领导 -> 产品经理领导 -> 设计和交互部门领导 -> 各个开发部门(前端、后端、客户端、移动端) -> 测试部门领导 -> 运维部门领导 -> 市场部门领导。

这种工作模式本质还是一个大的单体团队 + 瀑布的工作流,其组织是一个金字塔形式,行动缓慢。这种组织架构带来了另外一个问题,团队扩张非常困难。这种困难和我们在技术上的容量瓶颈类似,技术上应用的水平扩展无法突破数据库的瓶颈,这种组织架构的瓶颈无法突破部门经理的负载。

矩阵式架构

大部门式的组织架构是从亚当斯密的社会化分工理论就开始了,制造业尤为明显。曾参观过富士康、自行车制造商捷安特的工厂,基本都是以大部门式的组织方式。银行、传统行业转行的 IT 公司很多也是大部门式。这种组织形式劳动密集型企业没有问题,但是对智力密集型企业缺点越来越明显。知识性工作,工作者之间需要大量的交流。相关的人,”坐到一起“ 永远是效率最快的交流方式。

随着软件工程的发展,项目管理在软件公司也像在制造业一样蓬勃发展。于是很多公司开始使用项目制,但是项目制的缺点和大部门式完全相反,项目组成员能有效沟通,但无法传达行政指令。行政管理总是不可或缺的,比如涉及绩效、涨薪等问题,项目制很难解决,几乎没有纯项目制的公司。

于是出现了项目、行政部门双轨的组织形式,这种形式能比较符合软件工程的需要。

img

传统的软件行业更像是来料加工的制造业,客户需要一个软件就启动一个项目,开发完成后释放掉这个项目。但是现代的互联网行业更像是一个服务业,软件开发需要源源不断的开发、修复问题和提供更新。于是矩阵式架构就留给了软件技术服务和解决方案公司了。

自由市场式的组织架构

互联网软件开发需要源源不断的开发、修复问题和提供更新,按照时间或者事件启动的软件项目越来越鸡肋。软件开发往平台化、服务化发展。互联网公司项目经理的话语权慢慢移交给了产品经理,软件开发变成了一个持续的行为。

相应的,互联网公司的组织架构开始调整为小型敏捷团队。响应需求和提供服务变成了其组织的目标,彻底区别于”来料加工“型的组织方式。这种组织形式需要很多灵活的小团队和一个中心来调度协调,于是变得越来越扁平。

很多公司推崇的扁平式组织架构,与其说扁平不如说是自由市场式。谁能为这个企业的内部产业链上提供服务就有机会拉起一个团队,如果能在公司外找到又好又便宜的供应商就外包出去,自由市场就是这么残酷。有人制作过一张苹果公司的组织架构图,非常有趣,围绕着 CEO 的是各个团队领导以及背后的团队。

image-20200517161736294

扁平的另外一个含义就是各个单位之间相对独立,每个单位直接向核心层汇报。每个团队的独立性使之可以让核心层快速调用,当然每个团队也更容易被替代。不过需要注意的一点是,组织架构扁平的出现可不是公司为了给雇员提供一个轻松而没有压迫的环境,而是由他们业务决定的。

因为这样比较省钱,充分体现了市场经济的哲学原理。

披着”市场经济下“外衣的扁平架构就是完美的组织架构了吗?如果是这样,经济危机就不会出现了,扁平组织架构的公司就能万世长存。扁平意味着权利下放到基层,权利结构分散和多样。团队之间虽然可以平等对话,但是会出现互相不正常竞争、KPI 至上的问题。团队之间在每个财年争抢预算,各自为政。

这种组织架构要求一个精神领袖,或者强大的平台,否则难以约束基层的权利。当缺乏精神领袖时,这种系统并不稳定,举个不恰当的例子是周天子衰败的春秋时期,以及失去向心力的民国时期。至于民国时期的状态,可以了解下《中国近代简史》,下面是一张军阀分布图。

img

分形组织架构

当人类有组织后,大部门式的组织架构就有了,这是一种朴素的组织架构方式;那么自由市场式的组织架构是IT 行业特有的吗?拿我们近代史中的故事。二战时期的步兵往往就是大部门式的组织架构,这符合那个时期的军队特征。不过日、美军已经有全功能小队作战的思想,小范围用于山地特种作战。这个和全功能的敏捷团队有一点类似,有一点自由市场式的味道。

幸运的是我军走了另外一条路,也就是我们要聊的分形组织架构。当时我军在装备、人员上都和日军差距巨大,但好在产生了”三三制“这种班组突击战术,后来推广全军的组织架构上。三人构成一个战斗小组呈三角进攻队形,每名士兵分工明确,进攻-掩护-支援。三个战斗小组组成一个班,班长、副班长、二组长,各带一个战斗小组行动。一个排由三个或多个班组成,一个连由三个排组成,全军依次类推。这种组织形式的好处是,分工明确但又有向上信息传递的通道,不那么依赖中心,能在战术穿插过程中快速重新组织起来,形成战斗力。

img

分形是数学上的概念,它的局部总是和全局相似,自然界无处不在:雪花、树干、晶体等等。这种组织形态类似数学上的分形,我们暂且把这种形态叫做分形组织架构。

img

当然”三“也不是绝对的,可以由三人或者多人构成,现在由于兵种更加多样,现在军队也不是完全”三三制“了。不过”三三制“这种思想还广泛存在,影响了很多机构、企业的组织形式。

image-20200517194703512

曾经很长一段时间里,对国内一些大型的互联网公司的组织架构感到疑惑。他们既不像大部门式的组织架构,也不像自由市场式的扁平架构。当一个公司大的可怕时,这些公司更像一个市场而不是一家公司,现在的理论仿佛不能完全描述他们。

三四个后端、一两个前端、一个 UI 设计师、两个测试外加一个产品经理,这就是一个互联网公司最基层的一个敏捷团队,负责一个大产品中的一个服务,也是很多初创团队的配置。

三四个敏捷团队大约几十人左右,就可以负责一个完整的产品,这也是一个起步成功的小公司的配置。

三四个产品构成一个事业部,能负责一个公司一条产品线,这也是一个中大型公司的配置。最后,多个事业部构成了最终的大公司,当然这些团队还有可能是市场、法务、生产等其他角色。

这种形态已经是目前大的互联网公司主流形态,金山、百度、阿里等等。这样来看的话,分形组织架构的局部就可以看做自由市场式的组织架构。

使用 DDD 指导团队划分

对于互联网企业而言,比较看好的是分形组织架构和自由市场式的组织架,因为我们基于 DDD 理论设计的分布式系统某种程度上也是分形结构。既然团队划分是为了服务技术架构,那我们又可以借助 DDD 的一些成果为团队划分帮一点忙了。

我们采用和《分布式授权设计》类似的套路,先把 DDD 的服务划分图做一些调整,然后再映射上团队划分图。再来解决一个一个细节问题。我拿最复杂的那张图做蓝本,并补充了分布式授权部分补充的授权服务。其实这张图还不够复杂,在真实的互联网公司往往还有支付、大数据、物流、售后服务等等。

image-20200516232757864

我们从 DDDD 得到应用的分层,然后划分出服务。根据康威定律,一个公司的组织架构和技术架构需要适配,当我们采用分布式系统划分了我们的应用后,传统的开发、测试大部门就不再适用了。

因此我们使用敏捷的工作方式和团队划分方式。这也是我们正在讨论的问题,根据我们服务的划分作为底图,初步得到我们的团队划分方式:

image-20200516234815771

作为敏捷团队,后端开发、客户端开发、移动端开发、前端开发、测试被打散到各个团队中。我将团队性质不同使用颜色区别了一下,以便于后面调整和阐述每种类型的团队成员组成部分。抱歉,因为这个图我还想保留 DDD 分层的信息,显得有点凌乱,我们会一步一步突出团队划分。

  • 绿色, Deveops 团队,负责基础设施的开发和运维,提供开发、部署平台。负责维护测试环境、部署、日志、监控、代码质量检查、堡垒机、安全审计、数据库管理等。
  • 红色,领域服务团队,负责领域服务开发、测试。可以由纯后端开发组成、QA为主。领域服务的 QA 负责 API 测试、集成测试、性能测试。
  • 青色,应用团队,负责应用层和端侧实现。负责整合后端 API 实现端到端应用,团队由前端、客户端、后端开发或者全栈工程师组成。QA 负责应用端到端测试,确保最终交付质量。
  • 藏青色,认证和授权团队,负责提供统一的认证和授权服务,也就是 SSO 服务。认证和授权团队一般都比较特殊和独立,因此单独一个团队。

这样一整理,我们发现每种颜色对人员的需求是不一样的,甚至需要自己独立招人。试试就是现在主流的互联网公司都是这样操作的,不仅需要独立招人,还有内部的人才市场。

那么,对于上面这个场景下的人员构成,我们再细化一下每个团队需要的具体角色。

image-20200517220726961

这种系统还有一个问题没有解决,每个团队相同的角色之间没有交流。每个团队有自己的 UI ,那么整个公司就没法统一;各个团队的工程师如果没有交流,整个公司的技术架构就是一团糟;BA 各自设计,业务逻辑只考虑自己的应用和服务,必然会造成逻辑上的矛盾。为了解决这个问题,有些公司创造了一个”委员会”的概念,由分形的上一级协调拉通。

这些委员会不是一个团队,而是由每个团队挑选出能“说的了话”的人共通参与,进行需求、设计、架构、测试评审的。

  • 架构委员会。由后端开发、devops、各端(前端、IOS、安卓、PC)等和技术选型、方案、架构相关的人员参与。职责为参与方案评审、技术选型、安全评估、规范制定、技术演进等。有一些公司会提供单独的架构师职位,其实更多的公司作为架构师的前提是一个优秀的一线工程师。
  • UI/UX委员会。由各个团队的 UI/X 组成,负责制定 UI 规范,设计系统。
  • 测试委员会。由各个团队测试组成,负责制定测试策略、测试用例编写规范。
  • 业务需求委员会。由各个团队业务组成,负责拉通业务需求和产品设计,保持各个应用的业务逻辑的一致性。

当然如果是在一家大的电商公司还会有自己的支付、物流、大数据、营销、财务平台,除了 IT 部门外还有市场、销售、法务,根据我们的分形理论,他们可能存在于另外一个平行世界里。

总结

聊完这些,对于一个大团队是否可以被管理又没有那么悲观了。当人类开始有能力成群结队用石头击败野兽的时候,管理就出现了。推动人类社会进步的是火吗,其实更应该是建立在语言之上的社会组织能力。800年周王朝建立在封建社会对奴隶社会的优越性上,资本主义的萌芽不仅仅是因为蒸汽机,更可能是社会化大生产的新型组织形式。

一个大的公司真的应该重新审视组织和技术两个方面,组织架构的创新也许比科学技术的创新更重要,团队划分的问题实际上是一个如何解放生产力的问题。

more >>

解决应用垂直拆分的分布式授权问题

应用垂直拆分后,面临的另外一个技术问题就是分布式授权问题。实际上 OAuth2 非常成熟,互联网的分布式授权体系基本都是基于 OAuth2的,使用分布式会话机制实现的 session 共享不能算严格分布式授权。讲解 OAuth2 的文章非常多,这里简单回顾一下,细节请参考相关资料 —— 之前的博客 细说API - 认证、授权和凭证

OAuth2 有四个角色:资源所有者、资源服务器、授权服务器、客户端。

  • 资源所有者(Resource Owner)。通俗来说就是需要授权的用户。
  • 资源服务器(Resource Server)。这个比较好理解就是提供资源或者业务能力的服务器。
  • 授权服务器(Authorization Server)。提供授权、发放凭证、检查凭证的服务器。
  • 客户端(Client)。发起授权请求的服务器。

OAuth 2 主要解决了两个问题:

  • 在不传递用户口令情况下让用户获得权限通行证。这个是通过服务间传递一次性授权码的方式代替密码传递实现的,基本原理比较简单:用户访问应用服务,应用服务去资源服务器拿资源,被要求授权。用户被跳转到授权服务,并使用密码请求授权服务器,授权服务器生成一次性授权码,跳转回应用服务。应用服务通过一次性授权码去授权服务器换取资源服务认可的凭证。
  • 让分布式系统中各个资源识别访问者是否有权限访问。资源服务器提前注册到授权服务器,通过凭证去授权服务器检查是否有效,并获取角色信息。

基本的逻辑如下:

oauth.png

实际上,OAuth2 有主要 4 种模式,解决不同场景下的问题

  • 授权码模式(authorization code)。通过一次性授权码,在无密码传递情况下获取授权。

  • 简化模式(implicit)。没有应用服务器,客户端充当应用服务的角色,授权服务器直接把权限给客户端。

  • 密码模式(resource owner password credentials)。应用服务器和授权服务器彼此信任,通俗来说密码模式就是走公司自己的授权服务,授权码模式走第三方授权。用户直接在应用服务输入密码,应用服务传递密码给授权服务器。

  • 客户端模式(client credentials)。和用户授权无关,解决服务之间访问的问题。也就是常说的 AK/SK 授权,识别来调用你服务的来源是否合法。

匹配 OAuth2 权限角色

在 OAuth2 实际应用中,有一个问题没有解决。应用垂直拆分后,那些服务分别充当 OAuth2 中的角色呢?我们回来之前的复杂问题,收银系统。当时为了简单,省略了用户服务和认证(IAM)服务,为了画下这张图,省略了一些元素,重点突出授权方面的内容。

image-20200516150054993

这个图还是看不出我们应用中那些是资源服务器、那些是应用服务器、认证服务器。我把图修改为半透明,并映射相关信息。

image-20200516151440735

通过 DDD 和 OAuth2 的结合,可以清晰地将 DDD 中的各种角色映射到 DDD 分层模型上。总结如下:

  • DDD 各种应用服务,需要实现为 OAuth 客户端。

  • DDD 各种领域服务一般设计为资源服务器。

  • 授权服务器需要既要提供一个 DDD 概念上的应用层和领域层,可以部署到一起发布。

  • 用户服务既要充当授权服务的资源提供者,实现用户登录。又要充当资源服务器,需要被权限管理。是一个比较特殊的互为依赖的关系,为了破解这个死循环,所以往往系统管理员都内置在数据库中。

  • 系统内部一般使用密码模式简化授权流程。

  • 如果需要提供一个开放的授权服务,授权服务器需要支持授权码模式。

  • 服务之间的信任问题使用客户端模式,通过 AK/SK 访问,在很多公司内部叫做 ”集成账号“ 调用。

  • 主流的实现上一般设计了认证服务和用户服务。用户服务负责用户、权限、部门等数据管理,认证服务负责 token 的分发,权限校验,绑定第三方登录等职责。

拦截器应该放到哪里?

除了搞明白 DDD 分层和 OAuth2 角色映射关系之外。DDD 思想还可以回答另一个问题:访问控制应该在哪里完成?用户获取到凭证后,授权服务器确实可以校验凭证,那么谁来负责访问控制(ACL)呢?接入层、领域层、还是应用层?

为了回答这个问题,我们需要将权限分为两种情况:

  • 方法级别的功能权限。解决的问题是,我能做什么。
  • 数据权限,角色和对象之间的权限。解决的问题是,我能对什么数据做什么。

我们发现,功能权限不正是应用层的职责吗?应用层定义某个角色能完成一个独立的业务职责,也就是 use case。像添加商品、删除商品,都是一些 use case。那么这些权限控制放到应用层非常合适,应用层可以简单地在方法前面加上注解就可以实现。

@PreAuthorize("hasRole('ROLE_ADMIN')")
public void addProduct(){
}

数据权限则没有那么简单,有两种声音:

  1. 所有的权限都应该集中控制,数据权限也不例外,不应该放到领域层,应该尽量前移。在网关中完成认证和授权,请求进入系统内部后无需再检查权限。
  2. 数据权限和业务相关,应该留给领域层决定,例如删除文章,需要有业务逻辑实现删除的是谁的文章。或者满足一定规则,比如删除共享给我编辑的文章。

在趟过很多坑,以及和大量资深工程师交流后,我是第二种声音的坚定支持者。原因有几个:

  1. 不是所有的数据权限都能容易的被统一拦截器拦截实现,这个想法很美好,但是很不现实。
  2. 用户对象的权限,本质上是一种业务规则,应该属于领域服务中来完成,否则大一统的数据权限成本高昂。非常简单的例子:有一个协作工作系统,一篇文文章可以由一个团队的成员编辑、删除。如果放到应用层,根本无法通过一个注解完成,因为有隐藏的业务规则存在。这种规则在 DDD 中可以使用 specification 模式来描述。

权限设计的经验

  1. 通过 HTTP 请求的方法来设计拦截器,因为很多权限不是以 HTTP 请求为粒度设计的。另外权限应该同应用层和领域层有关,接入层只起数据编解码、转换的作用,拦截器不要放到接入层。
  2. 权限设计过于复杂,例如用户对一个集合部分数据有权限,部分无权限。业务往往要求 ”无权限即不可见“。但是这样做成本非常高,如果设计成上面方案 1 之后几乎没有能力扭转局面。这种场景会带来分页、过滤、统计等各种问题,建议从业务上避免。
  3. 通过 HTTP header 传递权限信息。服务之间的权限信息不要侵入业务方法,与之类似的还有语言、版本、trace id 之类的信息。
  4. 用户密码错误,不要提示密码错误,应该提示”用户名或密码错误“。防止用户名被嗅探,被账户注册机等灰产盯上。

分布式授权的性能问题

分布式授权往往会出现性能问题,每一次请求资源服务器都会拿到 access_token 后去认证服务器校验是否有效。在 Spring Security 中默认使用 RemoteTokenService,字面意思就是通过远程访问进行校验,通过 HTTP client 访问认证服务器。OAuth2 RFC 规范文档叫做 Introspection,定义了相关规范。

每一次远程调用意味着性能的浪费,为了消除掉这次远程调用,可以使用两种方法:

  1. 直接访问 token 存储源,Spring Security 设计有 RedisTokenStore。不过这种方式违背了分布式系统的初衷,可以在内部系统酌情使用。
  2. 使用自包含凭证,也就是 JWT(Json Web Token),Spring Security 设计有 JwtTokenStore。通过使用 Token 自身携带的凭证进行鉴权,可以较好的提高效率。

使用 JWT token 有两个注意事项:

  1. JWT 因为自包含的特性无法做到撤回,解决的办法是通过 redis 设置一个黑名单,redis 过期时间和 token 比 token 的过期时间稍长即可。另外也需要给 access_token 设置一个合适的过期时间,一般在 5 - 10 分钟。
  2. JWT 本质上是签名而非加密,Json Web Token 是 Json Web Signature 的应用。JWT 由消息体 + 签名构成,实际上就是结构化的 HMAC 签名方法。因此不能将一些敏感信息放到 JWT 消息体中,消息体只存放一些必要信息,并用于验证签名是否有效。如果需要更多用户信息,需要从用户服务获取。

image-20200516221603716

总之,应用垂直拆分后解决授权问题主流的方法是 OAuth2 + OpenID(OpenID 是一种基于授权的认证机制,可以参考相关资料)。OAuth2 只提供了一个授权模型,DDD 可以指导那些服务可以设计为客户端、资源服务器、授权服务器,根据 DDD 对系统分层可以得到一个映射关系,从而梳理出合理的权限系统。最后,分布式授权也会带来性能问题,我们可以通过 JWT 来解决这个问题,JWT 只是一种凭证格式,不是授权机制,应注意避免滥用。

more >>

解决应用垂直拆分的联表问题

应用垂直拆分后面临的另外一个问题是实现表关联查询,例如某项业务在界面上有一个订单列表,订单列表中的一项是商品名称。按照一般的规律,商品和订单会被划分到两个服务中。

如何实现这个需求呢?

如果是单体、小项目,首选的方案就是订单表和商品表进行关联查询。不仅在分布式系统下,关联查询不可能实现,即使两张表在同一个库里,对于商品、订单这类大表联表的速度也非常慢。

使用实时查询

服务化后,为了解决这个问题,走进的一个误区是通过 ID 来查询一个列表。为了构造出带有商品信息的订单列表,很多开发者会在分页后通过订单上的商品 ID 构造一个请求发送到商品服务,以此获取商品数据。为了避免循环调用产生 N+1 的问题,商品服务的开发者”贴心的“提供了一个通过商品 ID 数组批量获取商品数据的接口。

当这种接口大量出现的时候,拆分出来的服务又耦合到一起了,系统性能受损,且容易造成”雪崩“。会有大量的请求打到基础服务上,用户服务、商品服务、基础配置服务等首当其冲,需要注意这是分布式应用下的一种反模式。

在不得已且场景合理的情况下可以采用这种依赖方式获取数据,例如:

订单列表上有实时的物流信息,物流信息由物流服务提供,并且变化频繁。业务要求用户能看到自己的订单列表,列表中有一列为物流信息,包括:物流状态、物流单号、物流公司。

这种场景下,不得不采用将订单分页后通过 ID 到物流服务中获取,然后返回给前端。

即使是使用这种方式,也需要遵守一些规则。获取两个服务的数据并编排到一起,显然这个工作应该放到应用层,并尽可能使用异步的方式获取数据并聚合到一起。正是这个原因,Nodejs 以其天然的异步特性,被大量使用在应用层和接入层。如果使用 Java 则可以考虑 RxJava 等异步库来实现。

image-20200516093818246

同时,这种调用也需要进行熔断和回退。当物流服务 不可用时,应该即使中断连接,并依然将可用的数据返回给前端。界面上的表现是订单数据显示完整,仅仅是物流数据暂无法获取,对用户来说应用基本可用。

image-20200516093947300

不过,大多数场景下可以通过数据副本冗余和搜索服务解决这个问题。

使用数据副本冗余和反范式

产生这个问题,实际上是受到数据库关系理论影响太深了,数据库关系理论可以有效解决数据一致性和冗余,但在分布式系统下不再适用。

我们来回顾下关系理论中最重要的数据库范式理论。范式理论在某种程度上代表着数据库设计冗余度,主流的范式理论有第一到第五范式,第三范式后面有一个巴斯-科德范式,可以看做 3.5 范式。随着满足范式增高,意味着数据库冗余降低,也就是表越 ”碎“,所以一般参考第三范式。

image-20200516133902761

  1. 第一范式(1NF)。列职责单一,即表中每一列都是不可分割的,这是数据库设计的基本条件。学生表(学生姓名,学生基本信息),用户基本信息中还有姓名。不满足第一范式,连关系数据库都不能算。
  2. 第二范式(2NF)。所有字段都需要依赖主键,表中不应该存放和主键无关的字段。选课表(学号,姓名,性别,课程名,成绩),学号与课程名称是主键。姓名依赖学号,但是和课程没有关系。应该拆分成学生表、选课表。不满足第二范式,本质是多个模式叠加一起了,会存在冗余以及部分记录中大量空字段。
  3. 第三范式(3NF)。所有的字段都应该直接依赖主键,而不是传递依赖主键。基于第二范式的选课表(选课ID,学号,课程号,课程名,成绩),选课ID是主键,学号、课程号、成绩决定了选课记录。课程名和选课 ID 实际上没有关系,课程名存在冗余。如果要满足第三范,需要将课程名拆分出去,通过课程号外键关联。

第三范式导致了学生和课程关联到一起,如果需要将学生和课程拆分成不同的服务,选课由课程服务提供,冗余其中一个服务的部分字段。在分布式应用系统下,可以通过反范式的思想来解决服务间的数据关联问题,单体系统我们一般将范式设计到第三或者巴斯-科德范式,分布式系统则应该考虑退化到第二范式,使用数据冗余和服务间同步。

image-20200516135408376

关于反范式应该注意两点:

  1. 反范式不是没有范式,而是将高范式退化大低范式,通过冗余换联表成本。
  2. 有些场景使用了冗余和快照是业务使然,例如订单中商品快照本来就不是商品,他们之间无需同步。即使在单体系统下也应该分开。这种冗余也不是反范式。

下面几个常见类似业务场景:

  1. 某电商系统将订单和商品拆分为不同的服务,但是业务要求订单列表中需要显示商品名称。
  2. 某大型网站提供了用户认证 SSO 服务,且拆分了单独的用户服,在某管理后台应用中,需要显示已经登录的用户列表和用户基本信息。
  3. 某电商系统将订单和支付拆分为不同的服务,但是业务要求订单列表中需要显示支付状态。

通过 DDD 思想来分析,业务场景 1,订单中的商品和商品服务中的商品明显不是同一个概念。关于二义性的问题,我们在之前的文章中也讨论过,在不同的上下文下会存在二义性,通过发现二义性能建立合理的模型。这里的商品应该作为订单服务中订单项上的商品快照存在,设计为数据冗余。充分分析实际的业务问题,自然就解决了冗余关联的问题。类似的场景还有收货地址、票据等,这些对象都可以使用 DDD 中的值对象来设计,利用不可变性,让代码更直观。

image-20200516100017610

对于业务场景 2 属于常见需求,但是领域建模比较难的部分。很多系统都会提供认证服务,在认证这个上下文中,模型的本质是认证后的用户而非用户本身。因此往往是 token + 用户快照,不过并不是持久化到数据库中(往往是 redis ),所以在线用户列表并不需要去关联用户表,数据的一致性在用户重新登录后完成。

image-20200516100927239

对于业务场景 3 不能简单套用业务场景 1、2 。订单中需要冗余支付状态信息用于在订单服务中使用,但是对于这类需要服务间更新的冗余数据来说,可以借助在《分布式事务设计》部分谈到的最终一致性完成。不需要实时的访问订单服务,而应该采用同步和推送的方式,将支付服务中的状态同步到订单服务。

image-20200516141244461

在充分对业务分析的场景下,这种方式优势明显:

  1. 查询性能高,不需要跨服务查询,缓存策略简单
  2. 不需要熔断,没有雪崩效应
  3. 便于业务统计分析,数据统计的成本低

当然,并不是所有的业务都能满足条件去实现这种设计,需要具备一些条件:

  1. 业务天然具有分离性,典型的就是商品快照、收货地址、发票、单据
  2. 业务上不完全分离,但能接受一定程度的延迟,对实时性要求不高,可以通过最终一致性同步过来。例如商品服务和供应链服务。

还有一些陷阱需要注意:

  1. 盲目冗余,被冗余数据和当前服务完全没有关系。例如用户是否有折扣由积分阈值决定,积分可以动态增加和减少。有可能业务需要用户列表中显示有无折扣,但是折扣信息和用户完全不相关,这种情况也不要冗余。
  2. 延迟要求较高,在前面的例子中物流信息变化十分频繁,并且业务上对延迟接受度低,也不要设计为冗余。

使用搜索引擎解决复杂查询

解决两个服务的问题,有时候可以引入第三个服务来解决。我们来看下面的场景:

一个社区应用,用户发帖时间达到一定可以领取荣誉勋章,该系统将用户信息和成长体系分为两个服务。现在有一个需求是管理员需要根据荣誉勋章或者用户名进行搜索,并显示到一个列表上。同时类似业务也有可能出现在用户侧。

这个场景有两个特点。第一,不满足冗余设计的条件,荣誉勋章输入成长体系的一部分,这个服务和用户服务不太搭边。第二,需要跨服务搜索,且搜索会很复杂。

这种场景下实时查询和冗余设计的都不是很方便解决这个问题,好在可以通过搜索服务来解决这个问题。

image-20200516103402240

开源成熟的搜索引擎 Elastic Search 在互联网公司广泛应用,用于解决服务垂直拆分后的查询问题非常实用。通过创建多个 Elastic Search 的索引,可以满足各种不同的查询需求。

使用搜索引擎会造成项目的运维成本上升,带来好处同时也需要考虑成本。另外真实项目中,服务间数据同步失败,最终一致性丢失这种问题始终存在。搜索引擎应该只拿来做搜索,不应该获取搜索引擎中的数据直接应用于交易业务。用户从搜索引擎获取的列表,然后根据该条目操作退款等业务,即使条目中含有相关数据,也应根据关联字段实时访问服务获取最新正事数据,保证强一致性。

总之解决表关联问题还是需要转换关系型数据理论和分布式系统下的两种思维方式,灵活应用。跨库联表等技术显得非常不自然,同时在性能、业务规则上都面临挑战。

more >>