打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
在做程序员的道路上,掌握了什么技术使你感觉自我提升突飞猛进?

1. 工具箱广度深度,或者说在技术选型上控制系统复杂度的能力,广度:懂多少数据库/数据处理框架/AWS几个重要的Service了解多少/著名的开源软件框架工具了解程度, (这个一年前的答案列了一些我们当时经常用的,业界也很流行的,您可以参考下。阿莱克西斯:后端所谓复杂的问题是什么? ) 广度决定了眼界; 深度:为什么数据库要这么实现设计,为什么AWS这个地方有这个缺陷(比如SQS为什么是可乱序的queue),看似类似的几个框架,在本质上有什么不同,是在哪个本质问题上做了哪些决定行的trade-off导致了它们在设计实现和提供的功能上分道扬镳? 深度决定了能否真的在合适的场景应用合适的能力与工具。

2. 程序语言理解深度和表达抽象能力,这是在实现上控制复杂度的能力,懂不懂得最小表达力原则?懂得几种编程范式?它们之间怎么根据具体情况作出取舍?是否知道怎样才能把code写成诗?怎么样才能在重重困难中,坚守高聚合,低耦合?怎样组织程序,才能使得让程序正向流动产生期待效果之外,程序能否根据效果/结果,倒推并很容易的倒推出这样的一种结果是由什么原因,那个组件造成的(这是系统怎么才能很容易进化的关键,也是“做出来就是好的”程序员最难克服的一点)?code写出来逻辑线是否清晰可见?

3. 方法论,编程大道(programming in the big),和构架能力,这是在时间跨度的整体上控制复杂度的能力,辨别什么是对的,应该做什么的能力;在时间跨度上,在信息不完整的情况下,现在怎么构架,才能使得当将来信息完整了,我们能很轻松的根据将来的正确信息,把系统调整成最好最正确的状态,什么决定应该现在做,什么决定可以和怎么样才能留给将来做,并且在这个过程中,保证能够支持业务正常运转。在整体上,怎么把需求获取/设计/coding/测试/安全/部署/运行监测/报警/性能/系统回馈分析/数据统计/报表…等等在全局把握,安排的妥妥当当相互支持而不是相互抵触,相互使绊子。这里包含的知识包括,到底是waterfall, TDD,BDD,还是type driven,怎么执行Agile,什么是devOps,continuous delivery。 到底应该是技术决定业务,还是业务决定技术?。给一个100人的团队和超复杂/抽象的需求(比如需求就是让公司业务翻一倍,怎么翻一倍这个抽象问题要怎么分解成n个大问题,这n个大问题怎么分解成m个中问题),怎么把抽象问题落到实处,怎么能把大的问题分解成哪怕是比较弱的程序员也可以解决的小问题,然后还要证明这是根据现在的信息,可以做出的最好的决定

一个DDD 实践者经常使用的重要的practise,就是跟PM和客户讨价还价, 学会拒绝和剪裁,合理的push back,也是我面试Sr. SDE时候会考察的软技能之一

因为复杂的技术实现是有代价的,要看换来的业务价值是什么,如果核心逻辑要求一个技术实现,关系到项目的成败,那就要不惜代价的去选择完备的实现方案。而当一个超复杂的技术实现只是为了一个可有可无的业务功能,那就要强势说服客户PM砍掉这个功能。因为支持这个功能造成系统的复杂度,可能会给拓展核心业务造成困难。

这里推荐一下Worse is better这篇文章,论述了为什么简单大于一切。

简单要大于正确性,因为现在的正确未必是未来的正确,砍掉意义不大的正确性,保持核心的简单性,是让核心能够轻装上阵,更好演化的关键。

简单大于一致性,这个很多例子了,大家从事务的强一致行转为prefer 最重一致性就是在这点上trade off的最好例子

简单大于功能完备性,这个上边我也解释过了。不需要非核心的功能完备性而牺牲核心或者系统的整体简单性。

那么怎么在简单,业务正确性,一致性,完备性里,做合理的trade-off(因为完全不要正确性,一致性和完备性也不行呀)就需要对业务有深入的了解认识。程序员的本质还是解决问题的人,只是恰巧用计算机来解决问题而已,好的程序员是简化问题和解决问题的高手,他能够在业务/技术/人员/文化/复杂度/等等多角度做剪裁,来衡量各种得失,而不是把自己逼死累死在复杂的技术实现上,用灵活的手段来让自己的组和公司获得成功。

跳出技术思维的束缚,从全局来思考问题,我想,这就是DDD的精神吧。

开始面向测试用例编码之后。一个测试友好的代码,其输入、输出都是清晰的,而且功能应该是单一的,这样才能方便测试。

我是N年前进网易之后,跟着组里一个从Google来的leader,他要求所有写的代码都有对应的gtest测试用例来覆盖,有了这个概念和练习之后,开始要求自己在实现代码方面要功能尽量的功能单一、有明确的输入输出,这时候开始个人觉得有了比较大的进步。

当然,很多时候为了测试而测试是不应该,但是有了测试用例,任何的改动只有没有跑过用例的全都可以自动化检测出来。

因为接触新的概念而茅塞顿开的体验,有时是因为新的知识与原有知识体系打通了。

比如在使用了多年C/C++后,一些不正确的固有观念会阻碍对技术认识的深入,但是自己却察觉不到。这时如果你深入学习和使用Python等新的语言和技术,就会对已经掌握的技术有颠覆性的认识。肯定不仅仅是学到了一种新技能而已,而是新的知识会和老的知识发生化学反应。之前认为“必须如此”的做法,在吸收新的知识后,你可能会不那么确定了。

忘了哪位大牛说过,经常用Vim和命令行的人应该去试试IDE,经常用IDE的人应该试试Vim/Emacs加命令行,深表赞同。真知灼见往往藏在你完全不知道的地方。学习技术,故步自封是大忌。

突飞猛进的提升常常来自于灵感,而灵感是辛勤思考的果实。

这个道理就不展开了,被苹果砸、梦见蛇能产生什么灵感,取决于你之前有过多少辛勤的思考和试验。灵感并不是什么玄的东西,依然是积累的结果。

同样一种新的概念、技术,对不同的人意义完全不同。

比如,Milo Yip回答说TDD非常有用。我赞同,但是我自己和周围的人并没有用的太多。

测试驱动开发确实对很多团队、很多开发者有过巨大影响,但是我之前的团队尝试过,TDD在游戏逻辑开发中存在很难跨越的障碍,导致并没有实际使用下去。所以说“突飞猛进”的感触因时而异、因人而异。

总之,在学习的过程中,你会不断地改善方法、否定之前的方法,或者否定之前否定的方法,然后从中收获到大量的乐趣。

在这个过程中,每一个时刻的你,都不能用“蠢”来形容。如果称以前某个时候的自己很蠢,那么之后可能某个时刻你可能会更“蠢”,这涉及到了学习的本质。

知识靠逐步积累,并没有那么神奇的一大步。

不过倒是想起来职业生涯中对于一个不起眼的小工具的有趣经历。工作两年后在 team 里也算是效率比较高的人,基本上算是 lead 了。有一次和一个 new comer 确认工作流程的时候他总是强调自己用 diff 的结果。我在这之前从来没用过 diff。经他说了几天,diff 成为我日常使用最多的工具。(后续:这个 new comer 不久就离职了。从我们共同的同事和下家公司的同事的反馈来看,他的 performance 并不好。但是从这件事情上,我一直把他认为我的领路人。)

后来我到 Adobe,这里有一个做出版软件十多年的人。我到现在心里也会叫一声老前辈。他头一年见到我就会说「我怎么看你老泡在 diff 上呢?」之后自然他也开始用 diff 了。

可能对于大多数人来说,我们两个人在 diff 工具上的知识盲点是可笑的。即使很强的人,也会在意料不到的地方有盲点。

前端,针对前端开发吧,但其实有些经验是有普适性的。

一、DSL (领域特定语言) 吧

对于DSL其实完全谈不上掌握,但不断的浅层次实践,确实足足保证了我至少3年左右的稳定技术类产出,在业务中也受益不少。

前两年主要的关注点其实主要还是集中在偏向于L(Language)的部分,自己也确实掌握了不少内部或外部DSL的设计技巧,也能结合自己熟悉的宿主语言做出一些黑科技的方案。但本质上这部分内容其实是已经被解决的问题,所以越来越感觉是一种“工具”,针对不同的问题域,L部分的设计准则往往具有一定相似性。

近两年越来越发现DS(Domain Special)才是真正的关键,在很多想法无法落地,陷入死胡同的时候,重新理清思绪找到系统的限界上下文,往往整个问题就豁然开朗了。这个其实更偏向于方法论范畴,在各个业务领域,各个工种其实都有不同的实践意义。业界常说的DDD不只是一个口号而已,后端的微服务领域边界,前端的组件功能边界,其实都可以从这种思维中受益。

要说推荐书的话

  • 《编程语言实现模式》

  • 《实现领域驱动设计》

  • 《flex 与 bison》

二、明确理解了html在传统前端开发中的重要性

很多年轻的前端会比较沾沾自喜使用一些css3实现一些酷炫的效果,使用一些复杂的JS框架完成业务开发(其实大部分都是拿锤子找钉子),往往忽视了html的重要性。

就和复杂模块的开发,我们可以明确的感受到,『数据结构设计的是否优良』几乎可以奠定后面的逻辑开发是否顺利,一个傻逼的数据模型定义,往往需要山路十八弯的去解决看似不复杂的需求

在界面开发中,也是一样,『一个设计精良的html结构』完全可以决定后期我们维护js和css的容易度,面试中对于新人也逐步注重这方面的考察。这些问题其实比你问css3有哪些属性这类问题要有意义的多

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

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

我们说任意一个接口,或者说任意一段程序,它都包含了输入和输出:自己调用参数是输入,返回值和传递给下层模块的调用参数是输出。一段程序要能正常工作,它肯定不是随便什么输入都能接受的,而是要求输入的参数满足一定的约束,比如说某个参数必须是整数类型,必须大于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的语言就能保证只有用到的部分才是约束,从而保证最小约束。所以动态类型语言编程总有一种更“纯粹”的感觉。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
感触最深的几条设计哲学 - 我爱佳娃 - BlogJava
[5+1]开闭原则(一)
泛型
一分钟了解“好”接口的设计与实现
《Unix编程艺术》
Unix编程艺术,Unix哲学
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服