查看原文
其他

做程序员,有没有什么捷径可走?

灵剑 Python猫 2023-04-15
△点击上方“Python猫”关注 ,回复“1”领取电子书

大家好,我是猫哥。分享今天在知乎上看到的一个回答:

作者:灵剑
来源:https://www.zhihu.com/question/68611994/answer/300229284

没有什么捷径可以走,任何一个概念和技术都是在大量实践的基础上、从内心深处理解了才会有所领悟。而且,他人的观点未必就是正确的,即使正确,也未必是你理解的那个意思。

比如说很多人动不动把设计模式六大原则、还有多少种设计模式挂在嘴上,我是从来都不对这些东西感冒的,我一直都觉得六大原则完全就是一种唯像的规律,如果你照着去做,你可能会发现反而遇到种种困难,甚至不同原则之间看上去像是互相矛盾的一样。

而我对设计原则的理解,直到最近才总结成一条原则,我称之为最大最小原则。

我们说任意一个接口,或者说任意一段程序,它都包含了输入和输出:自己调用参数是输入,返回值和传递给下层模块的调用参数是输出。一段程序要能正常工作,它肯定不是随便什么输入都能接受的,而是要求输入的参数满足一定的约束,比如说某个参数必须是整数类型,必须大于 0,另一个参数必须是回调函数,回调函数调用的结果一定要符合某个特性,等等。输出也肯定不能是乱七八糟什么都输出,而是必须要符合适配方的需要,这些输出结果所具有的特性称作保证,比如说保证输出为 20 个字符以内的字符串,等等。

当我们把程序级联起来的时候,一段程序的输出会成为下一段程序的输入,比如说调用A模块,A模块内部调用 B 模块,则 A 模块产生的数据会成为 B 模块的调用参数。

在这种条件下,前一级程序的输出和下一级程序的输入就产生了这样的关系:前一级程序的输出保证,必须完全覆盖下一级程序的输入约束。

那么我们要让这个设计稳定,尽量不随着需求的变化而产生大的变动,要做的事情就很简单了:

在投入资源一定的条件下,设计能满足需求的模块划分和接口,使得每个接口的输出保证最大化,而输入约束最小化。

注意到输出保证最大化、输入约束最小化、还有投入资源一定,这三个要求往往是互相矛盾的,接受更多的可能的输入意味着可能会增加实现的复杂度,降低输出的一致性,所以这三个要求是有优先级的:投入资源最优先保证,第二是输出保证最大化,第三是输入约束最小化。

具体来说大致是循环以下的步骤:

  1. 当前设计中,是否有修改前一级的输出保证和后一级的输入约束,使得总的实现能大幅度简化的情况?如果有,修改这个约束和保证,将它在前一级和后一级之间进行转移。如果这个接口在修改后输出等于输入,移除这个接口(例如,如果有段程序只是为了把前端传过来的JSON里的字段名修改一下、调整一下格式,为什么不直接让前端使用修改过之后的格式呢?)
  2. 当前设计中,是否有功能重复的部分,可以合并成同一个接口,从而显著降低实现复杂度?如果有,将它们合并
  3. 是否有接口输出的一致性不好,比如有多种可能的情况,或者不同的输入参数对应了不同类型的输出结果,而将不同的情况分解成不同的接口,并不显著增加实现复杂度?如果有,将它拆分成多个相同输入的不同的接口
  4. 是否有接口有多余的输入约束,例如有多余的输入参数,或者有和同类型接口中不一致的输入,或者不必要地假定了输入参数符合某种关系(例如两个参数不能同时为True),移除这个约束并不会显著增加实现复杂度?如果有,移除这些多余的输入约束,让它接受更广泛范围的输入。

某些情况下,程序自己也是一种输入输出,比如参数可以接受回调函数,以及面向对象等,这种情况下程序自己是输出,而程序满足的接口是输入,这个输出的输出保证和输入约束是函数的输入约束和输出保证两部分,在这个函数本身满足输出保证最大化、输入约束最小化的情况下,使用这个函数作为输出,也就满足了输出保证最大化、输入约束最小化。

当以上步骤正确完成的时候,我们一定会得到以下的结果:

  1. 所有功能都只被实现了一次,所有的实现都是必要的
  2. 每个接口都只能产生同一类型的、有最强保证的结果,因而它一定只有一个职责(对应单一职责原则)
  3. 每个接口都只对输入参数做最小的约束,每个子类的实现都会自己的输出做了最大的保证,因此子类一定可以替代父类(所谓里氏替换原则)
  4. 因为接口的最小约束,所以接口一定只使用了参数中最必要的信息,也就相当于依赖接口,也不用关心什么难懂的依赖倒置原则了
  5. 当然,接口隔离原则也是最小约束的副产物
  6. 当然,迪米特法则(最小依赖)就更是了
  7. 当一个接口提供了最大的保证,而接受最小的约束,这意味着这个实现已经无法再容纳其他可能的输入了,那么在输入输出关系不变的情况下,它的实现就已经完全确定了,也就是对修改关闭了;其他业务逻辑则可以利用后一级输入约束少于前一级输出的特点,和前一级并列插入到后一级之前,从而实现扩展,那么开闭原则也是不言自明了

也就是说我们只要遵从前面的一条原则,后面的这些所谓原则都是自然满足的了,而且它不会让你弄错原则的适用范围,比如说修改了业务逻辑(意味着输入相同的情况下,输出做了修改)了情况下还强行适用开闭原则,或者想不清楚现在的设计究竟是不是单一原则。

设计中一个最常见的错误就是把后一级的接口的输入约束设计成和前一级接口的输出保证一模一样,从而将约束一路传递到后面所有的模块中,导致以后业务调整时完全没有办法扩展,动不动就“重构”,这些都是设计出问题的表现。

如果你的设计服从最大最小原则,那么所谓的需求变更无非就是以下这些情况:

  1. 完全调整了某一类输入时产生的输出:直接修改处理这类输入的程序
  2. 让某一类输入的某些特例产生不同的输出:为这个新类型的输入独立编写一个并行的路径,直到输出落回到原来接口的约束范围内
  3. 增加全新的输入:增加全新的路径,在可能时复用旧接口

在这其中没有任何“重构”的余地,如果做了“重构”,那说明某些被修改的接口要么减少了输入约束(意味着以前没有做到约束最小化),要么拆分了接口(说明以前没有做到保证最大化),那都是上一次的设计失误。与加引号的伪“重构”不同,真正的重构只有在应用了全新技术(比如换了语言,换了基础框架)的情况下才是有可能的。

这个原则顺便也可以说明我对类型系统的观点。

在我看来,类型其实只是约束(保证)的一部分,而并不是全部,而它的缺点可以一言以蔽之:可能导致提供了太多的约束。比如说我们的某个接口使用了一个 int 型的参数,但是实际上它可能需要的只是一个可以执行+的对象,int, float, complex 甚至是 str 其实都可以,但是设计参数的时候未必能意识到这件事;再比如说参数写成了某个类,那就只有它的子类才能传入,但是实际上你需要的可能只是某个 interface,而可能因为后期逻辑的简化,即使是这个interface 也只用到了其中的一部分,那就必须要重新定义 interface 了。

而 Duck Type 的语言就能保证只有用到的部分才是约束,从而保证最小约束。所以动态类型语言编程总有一种更“纯粹”的感觉。

Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~


还不过瘾?试试它们




8.5K Star!  Python 代码内存分析的利器

说说 Python 的元编程

Python 有可能删除 GIL 吗?

CI/CD 工具选型:Jenkins 还是 GitLab CI/CD?

Python 的切片为什么不会索引越界?

由浅入深:Python 中如何实现自动导入缺失的库?


如果你觉得本文有帮助
请慷慨分享点赞,感谢啦

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存