王辉的博客

是什么让我对未知世界始终充满热情?

2018年6月更新 已离职,再见,Murex!
2017年4月更新 Murex程序员的学习氛围

11年毕的业,后来凭着对计算机的热情和执着,进了Murex,当了一名软件工程师,到如今,工作一年有余了。一直以来,都有学弟学妹或朋友向我询问关于Murex面试的种种事宜,我很高兴能帮助他们,一方面是能为他们顺利的找到自己喜欢的工作尽一点微薄之力,一方面也是为了多交些志同道合的朋友。搁这说说当时面试的心得,留点文字作为回忆的同时,也希望能给大家一点小小的启发。

暂且不说具体面试是怎么个经过,先说说当时的心态吧。

找工作之前,在一投行干了一半年的实习,对多线程,设计模式,稍有了解,经验不多,但热情很大,爱追根问底,倒腾代码。当时找工作的方向很清楚,就是要到金融业里做一个不懂金融的程序员。原因有二,一是在金融业里,搞编程,有挑战性,什么海量数据了,低延迟了,可以点起我战斗的热情,二是,赚钱多。另外,当时我的职业规划也很清楚,坚定不移的走技术路线,一是我好这一口,好到可以日有所思,夜有所梦,梦的都是代码,二是,走这条路,也能有肉吃,并且,也不少瘦肉。

之所以说完心态再说面试,是觉得,我当时的这个心态,对我最后取得Murex的肯定是有决定性关系的。

再来说面试。第一轮笔试,面过的普遍反映有点难,其实也是,和大部分公司要求做选择题不一样,Murex的题都是开放性的,给你些代码,让你说哪里好,哪里坏,坏的地方如何提升改进。不过说来说去,考的都是平日的积累。你说难吗?其实也不是特难,题出的也不偏,考的都是些基本的概念,比如面向对象的思想,多线程的使用。可正是如此,可以看出考生对基本概念理解的深浅。

所以第一轮面试,之前的准备是有必要的,但主要是要出于温故知新的目的。过与不过,要看的更理性一点。过了,说明工作适合自己。没过,可能是平日的积累没有达到要求,但也可能,工作本身不适合自己,毕竟每个人和每个人的情况不一样。把它看做一种测试,不仅是对知识的测试,也是一种匹配度的测试。

说到这,具体题是什么题,就没那么重要了,发挥出真实的水平就行,对别人负责,也对自己负责。

在接下来的和人力的面试,我觉的是些软能力的测试。比如,语言表达能力,另外最重要的就是自己对自己的了解,自己对自己的规划,自己的动机。人家就问为什么。问为什么要做程序员,为什么是金融业,为什么留在法国,为什么选的是Murex?所以,要多思考,思考自己的生活,思考自己的职业规划,思考自己的兴趣,思考自己为什么要下这一步棋,而不是另外一步,思考下一步棋的下一步该怎么走。所有的这些问题,我是学生的时候就在想,并且现在也在想,以后还要想,因为一切都是变化的,人在变,物在变。什么是动机,动机是内在的东西,推着自己往前走的那个东西,是主动的。而不是,人云亦云,随波逐流被动的。

第三面,我当时的印象,就是聊天,聊得高兴,聊得两情相悦,就被收了。但是,也有最后问问题的,写代码的,看情况。

今天写了这么些个情不自禁流出来的文字,其实就想说,在任何情况下,做自己,足矣。只有这样才能以不变应万变。另外把功夫用在平时,做到无怨无悔,就够了。Murex选你,其实你也在选它。

公变和反变,又称协变和逆变,要深入理解它们,我建议从它们所解决的问题出发,然后再从技术层面看它们是怎么实现的。

用一句话概括,协变和逆变为了更好的写通用算法。优秀的程序员的都是懒的,能一下做完的事情绝对不重复。

咱们举个例子,说有一个豆子交易员,他牵线搭桥,把豆子从生产者交易到消费者手里,然后从中抽一成的佣金。这个算法是通用的,一方面它既适用于普通的豆子生产者也能处理生产特别豆子的,另一方面它既支持豆子消费者又能交易给所有农产品通吃的更广大消费者。

function exchange(producer producer, consumer consumer) {
val bean = producer.getBean();
getMoney(bean.getBeanPrice() * 0.1);
consumer.consume(bean);
}
我们接下来具体看看在什么情况下能用这个算法。现在我们有三个类,分别是父类,农产品(product),中间的豆子 (bean),和子类绿豆 (greenBean)。

先看消费端,对于这个交易员来说,如果来的消费者是农产品通吃,那么给交易员一个consumer,交易可通过。但如果消费者只吃绿豆,那么交易员就做不了这单生意了,因为生产者提供的可能只是普通的豆子。

再看生产端,对于交易员来说,如果来的生产者是产绿豆的,那么给交易员一个producer,交易可通过。但如果生产者给的是一个任意的农产品,交易员就不做这个生意,万一是一个大西瓜,只抽一成的佣金,运来运去赔本不说,也不一定卖的出去,因为一些挑剔的消费者是只吃豆子的。

消费端,看逆变,农产品是豆子的父类,而农产品的消费者确是豆子消费者的子类,consumer可以被当做consumer传给通用的交易算法。生产端,看协变,绿豆是豆子的子类,绿豆的生产者也是豆子生产者的子类,producer可以被当做producer传给通用算法。

这一逆一协中的奥秘就是,要想写出来通用的算法,当你面对的是生产方的时候,条件要严,而当面对的是消费者的时候,条件要松。交易员把好严生产松消费的关,生意就好做,算法写一个就完事。

最后,不同的编程语言有不同的实现方式,不管是java中的super/extends,还是c#中的in, out,还是scala中的+/-,说的都是一个事。如果你还不满足,想挖的更深,这里有一个从范畴论的角度开讲的,http://tomasp.net/blog/variance-explained.aspx/。

软件测试从本质上讲,和其它领域的测试一样,都是为了验证该能用的功能都能用。拿造自行车来讲,出厂之前得试一下吧,保证脚蹬刹车等都好用。软件也一样,发布之前通过测试来完成产品验收。可不一样的是,自行车出厂之后,是什么就是什么了,很少有人会回来要求给自行车添加新的功能。可对于软件来说,这却是家常便饭。不变和变之间,虽然只有一字之差,却意味着思维方式上本质上的区别。

对于自行车的测试来说,它们也是一成不变的,就是为了在车子出厂之前,利用他们给车子打下个合格证。而软件呢,是变化的,不断成长的,主要体现在新功能的引入。可限于软件这种产品的复杂性,新功能的引入是有风险的,它们可能破坏既有的功能。所以在面对这些风险的时候,软件中的原有测试所扮演的角色,就是保护这些既有的功能在新事物的入侵下安然无恙。

所以说要用发展的眼光来看待软件测试,同一个测试,随着时间的推移,虽然说在本质上讲是不变的,但它们在不同的生命阶段扮演的角色的侧重点是不一样的。

如果一个接口后面,只有一个实现它的类,那么这个接口,还真正有必要吗?

我觉得百分之九十的人提出这个问题时的情形是应该这样的:他没有用接口的习惯,在能少则少的指导思想下,只要是只有一个类,他绝对不用接口。突然有一天,他看到他的一个同事,在写具体的类之前,先把接口写好了并且写了相应的文档解释这个接口应该提供的服务,可之后只实现了一个类。他就问了,这个接口有必要吗?这个问题暂且先放下不说,看完另外一种情形,在加讨论。

试想我们有一段写好的代码,用了接口,接口后面,有两个具体实现此接口的类,突然有一天,我们发现,其中有一个类,以后不会再被用了,于是乎就把这个类给删了,这时只剩下了一个类,那么敢问,是不是把接口也给删了呢?

以上两个问题,其实都是在对同一件事发问,但是出发点不一样,但我觉得这两个问题结合在一起回答,会使答案更加清晰。

先分析第一种情况,有意思的是,他那个同事为什么在实现类之前,先把接口给定义了? 先实话告诉你,我就是那个喜欢用接口的人。所以我就都交代了先。我之所以先写了个接口,倒不是因为,一下就想到了,接口后面会有多少具体实现的类,而是为了想先搞定依赖于此服务的另一模块。打个比方吧,我正在写的这个模块需要一个缓存服务,但缓存不是它的核心业务,所以说它根本不关心缓存具体是怎么实现的。所以我就写了个抽象的接口先,并清楚的描述此接口应该提供的服务,至于如何实现,一字没提。更加给力的是,在写那个具体的缓存类之前,依赖于它的这个模块的单元测试我也写好了。欢乐吧!最后,这个缓存怎么搞,我可以选择自己搞,也可以给外包出去,反正,协议都已经写好了。

好了,先小结一下,在这里接口的用处很明显,它定义了一个服务是什么,而不是说,服务是如何实现的。如此看来,写接口有利于专注在核心业务上,有利于软件的模块化,有利于增加代码的文档解释率,有利于提高模块的可测试性,并且还可以通过分布式工作加快开发进程。当然,一切的前提,是我们可以抽象出来我们所需要的服务。否则,都是扯淡。

现在再看第二种情形,我们需要把接口给删了吗?恐怕,想删都没那么容易了,为什么?因为,这个接口在测试里被用了,这个接口上有完善的文档。当然,估计也没多少人会提出要不要删它的这个问题。

得嘞,在看看开头的问题,是不是显得很幼稚?

项目里是给覆盖率定义了警戒线的,可我们却一直在覆盖率下面。

测试覆盖率为什么上不去?因为我们没有在那上面下功夫。为什么没有下功夫呢?因为我们没有预算。我们会有预算吗?不会,因为做测试应该是每天工作的一部分,是需要一直在后台执行的。

对与新写的代码,我很认同这一点,没有测试的代码,不能称之为代码。可是对于已经写过的老代码呢。我觉得我们需要预算,预算的目的是为了还过去欠下的技术负债。

虽然都是靠自己,但不同形式的尾部递归,在堆栈上的表现还是大有不同的。有的更容易造成堆栈溢出,而有的却相当友好。至于怎么优化,慢慢来看。

从最基本的概念说起,每个线程都有一个堆栈,这个堆栈的高低取决于它所调用的函数的情况。举例来说,此线程先调用了函数甲,函数甲一个人心有余而力不足,无法独自完成所有的任务,它又请求了函数乙的帮助。出来借的,总归是要还的。每次往外的调用,都会使堆栈增长,就像欠下的债一样。调用的太多了,就会负债累累,非崩盘不行。

基本套路看完后,就可以看看尾部递归的招数啦。如果一个函数在运行到最后一步的时候才力不足,那么它请人帮忙的行为就被叫做尾部调用。如果尾部调用的对象是它自己,它就成为了尾部递归。尾部递归,不像其他的尾部调用,它造成堆栈溢出的可能性更大,因为自己调用自己,简单快捷方便,不用见外。找别人的时候,首先得需要这个人存在。此外,即便有人了,也得好意思张开那个嘴才成。所以一般不是递归的时候,堆栈的增长都是很有限的。

最后就要说说优化了。其实可以这样想,假如老大找老二帮忙,老二又得找老三。如果老二对老三这样说,“你这件事做完,直接给老大就行了,不用来找我”,那么,这一系列的调用就优化了。反之,如果老二说,“你做完之后,先给我,我再做点补充,然后再给老大。”,这样一来,就不优化了。递归是同样的道理,不过把老大,老二和老三都看成同一个人就行了。

显而易见,优化的手段就是减少中间步骤,把任务传达的干干净净。

上大学的那会,老班长点名点腻了,想当小兵,不仅可以不点名,而且可以偶尔翘翘课。我不知其中的苦恼,愣是接了他的差事,干了一段班长,管了一会班委会。如今细细想来,这段经历还是相当珍贵的,单从现在从事的编程工作来讲。

一个好班长和一个平庸班长的最大区别,就是,好的班长,自己不累,可以把班委会的各个成员,如生活委员,学习委员,文艺委员的积极性都给调动起来,让他们各自发挥各自的特长,好刀都用到好刃儿上,进而又快又好的完成指导员交予的任务。而平庸的班长,则“独揽大权”,什么活都一个人干,自己累个半死,他人闲个半死。

把这个场景和编程做一下对比,会发现,它们之间有异曲同工之妙。把指导员和同学比作用户,把班委会比作为同学和指导员服务的接口。班委会的各成员就是实际实现这些服务的角儿。离接口最近的人,是班长,因为总是他第一时间收到用户的请求,其他的委员离接口相对远些,因为他又受班长的指派。这样一来,一个好的班长,作为服务的总实现者,从来不去做具体的事情,而是把任务指派下去,实现分布式工作,而平庸的班长,会使用集中性处理。

对应到编程上,前者,创建了很多小的单元,每个单元做自己擅长的是,实现了模块化,责任结构一目了然。后者,一团糟,都纠结在了一起。总结一下,在编程里,离接口越近的类,具体实现就应该越少,指派的越多,而离接口越远的类,就应该指派的越少,而具体实现的越多。各自做各自擅长的事情。

0%