在关于软件的复杂度上, David J. Wheeler 说:
“We can solve any problem by introducing an extra level of indirection.”
在使用了一段时间的 React Hook 之后,对于分层有一些感触。可能在维护和管理规模较大的软件上,添加更多的抽象和分层是必不可少的。但是分层不一定会带来更多的复杂度,巧妙的设计可以让软件依然容易维护。
我发现设计好、接受度高的软件,代码倾向于让用户按照业务逻辑来组织,而不是按照框架的实现来组织。
比如 React Hooks,在没有它之前,在一个组件中,你要将所有的所有组件的 ComponentDidMount
放在一起,将 ComponentDidUpdate
放在一起。如果一个页面有 5 个组件构成,那么每一个组件都要分别写到两组里面去,如果涉及更多的状态管理,涉及同一个组件的状态管理将分散在更多的地方。
但是 Hooks,让你可以把通一个组件的状态、控制逻辑、渲染逻辑都放在通一个地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } |
这就使得代码的阅读性和可维护性变得很好。
另外一个例子是 Django 框架组织代码的形式。Django 使用 app 来组织用户的代码,在每一个 app 里面都有 view model 等,控制这个 app 的内容。这样的好处有:这个 app 只管理这一部分的逻辑,与其他 app 的耦合性很低,“高内聚,低耦合”。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
user ├── __init__.py ├── admin.py ├── apps.py ├── authentication.py ├── middlewares.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20210311_1035.py ├── models.py ├── serializers.py ├── tests.py └── views.py |
一开始接触这样的框架的时候比较不适应, 比如怎么划分 app,是一个经验问题。新手很容易将所有的内容都写到同一个 app 中,或者直接按照团队的分工来划分 app。但如果正确掌握了这种组织代码的形式的话,代码就的可维护性就会提高很多。
一个反例是蚂蚁的 SOFA 框架。以前的同事跟我说,“来蚂蚁就要学习 SOFA 的分层,学会了这个就掌握了精华了。” 使用这个框架写了一年多的代码,我还是无法理解其中的智慧。撇开启动速度长达三分钟、配置混乱并且难以理解这些问题不说。就说你要把代码写在哪一层这个问题,就会难倒很多新手。下面是一个新项目默认的分层结构,实际上随着项目的开发,层数会增加很多:
这样的设计默认了用户必须理解框架对每一层的设定。将项目变得难以管理,并且增加了很多工作量。比如对某一个 model 添加一个评论功能, 在 Django 中几乎是一小时就可以完成并上线的工作量, 在 SOFA 中可能需要几天的时间,在不同的层上添加逻辑。实际上大多数时候这些“层”什么都没有干,只不过是直接去调用下一层。
好的设计应该是 “make the easy things easy, and the hard things possible.” 显然,这种设计是让所有的事情都变得一样复杂。即使写一个 Hello World 出来,你用这个框架也需要创建出来一个庞然大物。
实话说,我在蚂蚁的这段工作经历,从开发体验上说,是非常痛苦的。包括框架启动慢、复杂并且混乱的配置,对 Java 语言的强绑定,缺少文档,代码难以测试(因为即使是本地开发也连接了很多其他服务)等等。
那么为什么会造成这种情况呢?我认为和组织形式有关。康威定律说“设计系统的架构受制于产生这些设计的组织的沟通结构。” 我认为可以再扩展一下,不光受制于沟通结构,和整个组织的政策都有很大关系。做一个事情的方案有很多种,可以使用一层抽象,也可以使用三层抽象,甚至可能有某种优雅的方法不添加额外的实体概念去实现。这不是一件简单地事情,需要极具经验的工程师才能做得很完美。然而假如 KPI 的压力太大,以及 KPI 只看结果贯彻地太好,那么怎么做就不会变的不重要,毕竟都可以达到一样的效果(但从某一个量化指标上来说),虽然可能会带来更大的理解成本,以及潜在的维护成本、沟通成本,甚至带来的稳定性隐患等。但是这不重要,KPI 怎么完成无所谓,只要完成了都一样。
另一个表现是晋升,一些公司像是封建社会一样有着森严的等级,某一等级的工程师只能做那一等级的事情,大家都想着向上晋升。但是很多晋升过去的人已经不写代码了,很多高等级的 SRE 工程师甚至都已经很久不使用终端了。这就导致在晋升的时候,这些高等级的工程师组成的评委团不会太过于注重技术方案。本来评审一个候选人的时候应该问 “为什么选择使用 A 而不使用方案 B?” “你这样做会有某某问题是如何解决的” “XX是怎么处理的”,由于无法理解技术所以只能问出这种问题:
你发这个的底层逻辑是什么?顶层设计在哪里?最终交付价值是什么?过程的抓手在哪里?如何保证结果的闭环?能否赋能产品生态?你比别人发的亮点在哪?优势在哪?我没有看到你的沉淀和思考,你有形成自己的方法论吗?你得让别人清楚,凭什么发这个的人是你,换别人来发不一样吗?
或许觉得这是网友的调侃,但是在当你确确实实要在晋升的时候去想破头思考这些问题该怎么回答的时候,就不那么好笑了。
在这种环境下,像是压测、限流、熔断、容灾等等方案,只要去做肯定是可以完成的,但是你可以因为这件事带来非常大的改造成本,造成严重的开发效率问题,搞出来很多让开发人员难以理解的概念,和蹩脚的设计,也可以做的很漂亮。虽然对于将来评价你的评委来说,这并没有很大的不同。甚至你因为设计的拙劣带来了很大的改造成本,却又给你带来了更多的工作量,加班卖力的完成,于是又成了一个可以被人称道的点,可以凸显你的推动能力、领导能力,这是评委们非常喜欢的能力。虽然本质上只是给大家带来了一堆麻烦而已。
另外,这种晋升机制又会让大家去强行给自己加活。通常的套路表现为:找出开源软件中的一两个毛病,然后以此为借口声称这无法满足我们公司的需要,所以需要“自研”一套,然后“自研”的软件解决了自己当初找到的那几个毛病,成功获得晋升。而实际上,自研的东西可能又带来了成百个其他的问题,但除了给使用者造成了痛苦之外,倒没有什么问题,因为还有其他人虎视眈眈地想再重新研发一次,替换掉你这个项目呢。
如此对技术的不够重视,加上繁杂的会议积压开发时间,工期紧,导致大部分的工程师不会有时间去思考设计、分层的问题了。虽然有时候停下来思考可能带来更多的收益,但是弥漫在焦虑中很难停得下来。我在这种环境中也写出了不少垃圾。在这种情况下,人们就越倾向于使用自己非常熟悉的技术,不愿意学习新的东西,因为这会减少自己工作中的麻烦。懒惰地使用分层和抽象解决问题,导致软件越来越复杂。声称 Java 才是适合“企业级”应用的最佳解决方案,实际上只是懒得思考和设计。毕竟,现在的事情已经够多了,我们应该把更多的时间放在“顶层设计”,思考“业务价值”上,技术方面,只要实现了就好,怎么实现的,没有人关心。我觉得这也就是为什么蚂蚁的很多人将很多东西做成了自己熟悉的系统的样子,比如很多中间件经过内部的修改变得只支持 Java,如果你在蚂蚁使用除了 Java 之外的语言几乎是难上加难(2020年);比如很多写过交易系统人去写一个逻辑非常简单的东西都会分成7层来写,甚至类的名字会使用交易系统的概念来明令,XXOrder,XXTransaction;比如听说 Python 实现东西很快,但是会把 Python 去写成 Java 的样子。
说了这么多,需要提醒一下读者的是,这并不能代表蚂蚁所有的技术,甚至有人会觉得 SOFA 非常成功,给无数小微企业带去了收益。总之,只是我自己的想法而已,如果我很喜欢蚂蚁的研发环境,我也不会离开蚂蚁。也可能是我天生愚钝,无法理解里面的大智慧吧。
复杂是很简单的,简单是很困难的。好的软件需要很多年的持续耕耘才行,一边做一遍思考,从自己现在做的事情开始,一点一滴,随着时间慢慢积累才行。
最后给读者推荐我比较喜欢的一个视频吧,John 讲的 A Philosophy of Software Design | John Ousterhout | Talks at Google 。以及他的书:A Philosophy of Software Design .
2021年06月15日更新:
一点想法。有关 web 框架,好的框架都是从简单的开始,随着项目的发展,逐渐变得越来越复杂,比如说 Flask,项目开始的时候可能就是一个文件,用户可以根据需要,引入依赖,拆分模块。虽然有些框架采用了不一样的哲学,比如 Django, React, 但是一开始脚手架生成的框架总是简单的。不好的框架,一开始就会给你生成大量的代码,即使你要完成一个很简单的功能,也要给你分个七八层,引入几十个依赖。记得以前有一次公司让领导亲手用我们自己的框架写一个 Hello World 类似的东西,领导们写了一整天,搞的满头大汗。
最近看了若干喷阿里的文章……腾讯或者其他大厂会好很多吗?
我也不是喷吧,就是说说自己的感受。感觉不会好很多吧,KPI 就是一个不合理的东西,我觉得合理的方式就应该招有自驱力的程序员,然后告诉他们问题,他们就会解决。靠KPI来驱动总会走歪。
就跟评价学生用GPA,评价经济用GDP,评价研究者用引用数一样……招有自驱力的程序员可能不是很容易scale吧。
你这篇文章真情实感、有理有据,写得很好的。
赞同!
KPI 不行,就来 OKR,子子孙孙无穷匮也,核心都是面向评分开发。
很遗憾的是,最终每个环节看起来都很完美,可是结果却往往不令人满意。
没有具体了解过 SOFA 框架,但是就图中这个架构图来看还是挺清晰的。
总体上看还是 MVC 的架构,核心领域层可能借用了 DDD 的思想,微服务化或者核心功能下沉到中台,业务层通过组装多个 domain 进行逻辑编排,表现层充当 adapter 的功能。
只不过箭头画的可能不对,上层和下层之间不应该是调用的关系,而是下层去实现上层的接口,这样可以做到层和层之间的隔离。
—
如果业务简单确实不太需要特别复杂的架构,反而会增加开发的成本(像文中所说的写一个 Hello World 都要每一层实现一遍,可能做的事情只是数据的透传和封装)。但是业务不断复杂后,之前的这种简单的架构模式就可能造成混乱,尤其是很多人协作开发一个项目,没有一个统一的架构最后就会演变成每个人自己写自己的。
诚然,过早的优化是万恶的根源(Knuth 语),但是在大公司里,成熟的项目是很难去推动优化的,一来是重构本身的成本和收益不好量化,另一个就是新的迭代在老的架构上总是能做的,所以最终会导致破窗户变得越来越破。
btw,最近在做 TT 的业务架构改进,对这一块思考还挺多的,有机会可以聊聊。
感谢留言。
感觉它的设计和你想的不一样。这里箭头实际上就是调用关系,每一个部分都是实现了自己的方法让上面去调用。所以才导致类似的函数名字会在不同的层数出现好几次。也会导致新手(即使在使用了3个月之后)很困惑自己的代码到底应该写在什么地方。
实际上即使有了这种框架,每一个项目也都是自己的风格。比如有人喜欢将所有的功能封装到 facade 层,然后使用 web 调用 facade。也有人喜欢将 API 的逻辑放到 biz 层,然后 web 和 facade 都去调用 biz。放在哪一个 biz,又成了问题。微服务意义不就是不同的业务逻辑可以有不同的设计吗?
感觉这种 “统一” 并没有去解决任何的问题。应该可以允许多种设计好的架构存在,而不是无论复杂与否都在同一个架构下写代码,表面一致,内部还是无法一致的。
另外如何避免写出垃圾的代码,有 2 点建议:
– 强制 code review(作为 CR 组委会的一员,我现在每天会有大量的时间花在 code review 上,没有组委会成员 approve 的代码不会被合并)
– 只招优秀的员工,优秀的人能容易合作。
完全同意。
非常真实的感受,现在BAT某个大厂工作得我每天都面对一堆非常难用的工具折腾,举个例子是想对部署的各个服务器进行进程的reload,需要阅读一份长达20页的操作文档,并且在里面零碎找到想要的命令进行拼凑,另外控制台命令不能两个人同时登陆使用,当别人不使用时候还不能自动关闭,而是需要手动去kill后再拉起使用,逼到自己把这些命令存在一个文档里面,需要的时候一行行拷贝出来使用。
嗯,可以试试剪切板管理工具,我用 clipy,刚出的 PasteNow 看起来也不错。
https://imtx.me/blog/pastenow-1-released/
> 新手很容易将所有的内容都写到同一个 app 中,或者直接按照团队的分工来划分 app。但如果正确掌握了这种组织代码的形式的话,代码就的可维护性就会提高很多。
按照 Conway’s law 应该根据代码架构来调整团队,否则会有很大的阻力,很可能最终还是实现成了符合组织结构的模样。
非常推荐 Team Topologies 这本书,把组织结构设计和团队的类别讲得很明白。
感谢推荐!