5

如何做好抽象?

 3 years ago
source link: https://zhuanlan.zhihu.com/p/375730170
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何做好抽象?

problem solver

写代码的关键是要做好抽象,我们今天就来谈谈几种常见的创造抽象的做法。

错误做法:凭空抽象

第一种对抽象的误解是,我就灵机一动,知道这里几个界面很类似,肯定需要搞个抽象出来。为什么?不知道,就是感觉需要。

这种做法的问题是严重依赖拍脑袋。每个人的脑袋拍完了之后想法都不一样。

错误做法:小抽象封装成大抽象,一圈先搞完,再套一圈,自底向上

比如说我们有一张数据库表。我们可以把这个封装到一个class里,别人不允许访问这个class的细节。然后多个class由封装到一个package里。package多个又可以封装到一个module 里。

如果我们怕class被其他package访问到,我们可以要求package内部的class,只能在这个package内访问。类似的package也可以被封装到module里。

这个做法行不通是因为人们很难判断自己最底层的抽象是不是就是错的,然后每一圈包裹进来的东西是不是太多了,或者太少了。

当年在做 honovation/veil 的时候就是这么想的。认为 python 抽象要做好,关键在于把 python 的包实现层级的访问控制。a.b,a.b.c,a.b.c.d 都可以确保自己的可见性仅对父一级可见。结果是一圈套一圈并不能帮助我们找到好的抽象,帮助非常有限。经常陷入到根据名字,找其成员的尴尬境地。比如我们有一个包叫 shopping,那么啥应该归纳到 shopping 这个大框框里呢。而且套的圈层次多,也使得经常会把不同的含义的东西来代表这些圈。比如有的时候,一个圈圈代表的是客户端,一个圈是一种客户端的需求内容。有的时候一个圈又代表了一个业务流程,比如退货申请审核等。有的时候一个圈又代表了一个外部集成系统。这些圈根本没法用嵌套的方式组合起来。

错误做法:先搞一个通用实现,然后配置成具体的需求

比如我们不知道客户有什么电商需求。但是我们可以先做一个通用型电商系统,比如 pet store。但是做得极其灵活,什么都可以扩展。然后用户如果要一个摩托车租赁系统,要一个辅导课培训课以及辅材销售系统,美发美甲产品以及服务管理系统,鲜花周期配送系统,都可以通过自己二次开发而获得。

在做 zhimai 的第一版的时候就是这么想的。认为我们先应该有一个通用的中台实现,不包含“复杂”的业务需求,但是预留下扩展。这样在做复杂需求的时候,在扩展里搞就可以了。问题是没有产品经理的需求,没有人知道那个“中台”是什么,那个最通用的东西是什么,又应该预留什么扩展点。

错误做法:找重复代码,抽取公共实现,下沉成可复用的

这个的想法是我们先随便写。然后去代码里找是不是有明显可以复用的东西,把这些可复用的部分抽取出来。这样的做法是可能两份不一样的实现,需求是完全一样的,只是两个人写了两遍,用了不同的写法(比如css有无数种排版布局的可能表述)。要从实现代码反推出可以合并来,确实有些困难。还包括可能需求本来是可以一样的,但是两个产品经理没有互相通气,稍微出了一点各自的小心机。这样也会导致实现代码不一样。

某些中台,公共平台的想法也是这么出来的。把业务上的共性提取一下,沉淀成可复用的业务能力。这个也是大部分初学编程,特别是知道了 function 是什么之后,本能的工作方式。

正确做法:先用硬性指标一切为二,然后靠人来压缩分类

比如把代码分成可以写 css 的文件,和不可以写 css 的文件。凡是写 css 的文件,需要通过“同学甲”来评审。这样就有一个非常硬性的标准来确保

  1. 同学甲,不会过于的忙。大部分文件可以不写 css 也能实现需求
  2. 所有用到 css 的需求都肯定会过同学甲

然后我们通过“同学甲”这个人脑来实现压缩分类。他需要判断这个需求是复用已有的 regular ui,扩展已有的 regular ui,还是放入 special ui 里。

这种做法是用硬性指标,把所有代码都一切为二了(同学甲负责的,和无需同学甲负责的)。然后用专人来负责对其中他所负责的部分进行分类。这个分类标准虽然仍然是启发式的,拍脑袋的。但是范围小,拍的脑袋也是同一个脑袋。

为了避免同学甲犯强行抽象的错误。检查 regular ui 部分里的参数是不是过多(非必要参数占比)。如果一些参数只有一个地方传,那肯定是强行抽象了,应该把代码移动到 special ui 来。

为了避免产品需求出太多幺蛾子(不规范,不规则),也为了避免同学甲偷懒。需要控制 special ui 的增长速度。把 special ui 部分的投入做为 cost 指标,让负责工期的人知道当前的投入里,有多少是投入了一次性的 special ui 里。

除了 ui,我们在其他场景下也可以这么做。比如所有涉及 I/O 的代码,都由一个团队负责。然后这个团队就要把所有的 I/O 收敛成几种 sdk 实现。这个都是建立在可以很容易很硬性的一切为二的基础上的(是不是有css,是不是有 I/O),而且可以自动化成 lint 检查,强制约束不被绕过。

虽然我们经常会说封一层,或者谁谁来收口。但是这些封一层,或者收口,往往缺少强制检查手段而流于形式。而缺少强制检查手段,又是因为缺乏硬性的分工标准,不能像有css,和无css那样,简单表述为“对某个底层系统的 api 垄断”。

正确做法:先写具体需求,再推导出具体需求中的集成部分

先构建一个可复用的壳子,然后通过扩展点来写具体需求。这个方案的问题是没有人知道啥叫那个可复用的壳子,又有哪些扩展点。

正确的做法是先认为所有的页面都是特殊的,都是一份具体的实现,是独立的。如果所有的页面都可以独立的写,皆大欢喜。如果发现具体的业务之间有交叉集成的部分,那这些集成点就是扩展点,就是所谓 motherboard,也就是可复用的壳子。

可能最终的产出和“先写通用实现,再扩展出具体业务”是一样的。但是这个得出抽象的过程是不同的。抽象不是一个客观存在,而是因为“有多个地方使用”这样的依赖关系,使得其变成了一个抽象。不要问好的抽象是什么。照着“好的抽象”长什么样去寻找,是找不到“好的抽象”的。而是先解决具体的问题,然后做得具体的需求足够多了,在这个过程中“倒逼”出了抽象。

演绎还是归纳

纯演绎有问题,没有办法从所谓第一性原理去推出具体的业务逻辑。业务逻辑就没有逻辑。物理规律可以用上帝不是左撇子(然而上帝真的是弱左撇子)之类的对称性美感,从原理假设演绎,再去实验验证演绎的结果是不是正确。但是写业务代码不行。

纯归纳也不行。整个 codebase 太大了,参与的人太多了,归纳建立在都是我一个人盘子里的菜的基础上。没有一个人可以 cover 全局,不能纯靠归纳。

好的抽象构建的过程是在 inductive bias 约束下的局部归纳。上面的两个正确做法就是实践出来的 inductive bias。没有 inductive bias 直接全局搜索最优解代价太大了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK