對于程序員來說,怎樣才算是在寫有“技術(shù)含量”的代碼?
你好呀,我是歪歪。
我最近其實(shí)在思考一個(gè)問題:
對于程序員來說,怎樣才算是在寫有“技術(shù)含量”的代碼?
為什么會(huì)想起思考這個(gè)看起來就很厲(裝)害(逼)的問題呢?
因?yàn)檫@就是知乎上的一個(gè)問題:
https://www.zhihu.com/question/37093538
第一次看到這個(gè)問題的時(shí)候,我很快的就劃過去了,完全就沒有關(guān)注這個(gè)問題。但是就是看了那么一眼,這個(gè)問題就偶爾不經(jīng)意間在腦海中浮現(xiàn)出來。
然后隔了一段時(shí)間,中午刷知乎的時(shí)候這個(gè)問題又冒出來了。
好巧不巧,也是那天中午,我看到了這樣的一個(gè)面試題:
看到這個(gè)面試題的第一眼,我就想起了 Dubbo 服務(wù)中的一個(gè)預(yù)熱功能。
在結(jié)合知乎這個(gè)問題,我當(dāng)時(shí)就覺得:Dubbo 服務(wù)的預(yù)熱源碼在我看來就是一個(gè)“有技術(shù)含量”的代碼呀。
這一塊功能編碼確實(shí)一點(diǎn)也不復(fù)雜,主要是能體現(xiàn)出編碼的人對于 JVM 和 RPC 方面的“內(nèi)功”,能夠意識到,由于 JVM 的編譯特點(diǎn),再加上 Dubbo 在架構(gòu)中充當(dāng)著 RPC 框架的角色,所以為了服務(wù)最大程度上的穩(wěn)定,可以在編碼的層面做一定的服務(wù)預(yù)熱。
但是寫完相關(guān)回答之后,從評論區(qū)來看,基本上是清一色的吐槽,說我舉得這個(gè)例子和問題相悖。
比如我截取點(diǎn)贊最高的兩個(gè)評論:
看完這些吐槽之后,我覺得這些吐槽是有道理的,我的例子舉得確實(shí)不好,非常的片面。
開始我還不服氣,但是現(xiàn)在我也是真心的覺得“別人說的對”。
為了更好的引出這個(gè)話題,我先搬運(yùn)并擴(kuò)充一下我當(dāng)時(shí)的回答吧。
順便也算是回答一下剛剛說的那個(gè)面試題。
服務(wù)預(yù)熱
下面這個(gè)方法,只有兩行,但是這就是 Dubbo 服務(wù)預(yù)熱功能的核心代碼:
org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance#calculateWarmupWeight
看一下這個(gè)方法在框架里面調(diào)用的地方:
當(dāng)我們不指定參數(shù)的情況下,入?yún)?warmup 和 weight 是有默認(rèn)值的:
也就是在用默認(rèn)參數(shù)的情況下,上面的方法可以簡化為這樣:
static int calculateWarmupWeight(int uptime) {
//int ww = (int) ( uptime / ((float) 10 * 60 * 1000 / 100));
int ww = (int) ( uptime / 6000 );
return ww < 1 ? 1 : (Math.min(ww, 100));
}
它的入?yún)?uptime 代表服務(wù)啟動(dòng)時(shí)間,單位是毫秒。返回參數(shù)代表當(dāng)前服務(wù)的權(quán)重。
基于這個(gè)方法,我先給你搞個(gè)圖。
下面這個(gè)圖,x 軸是啟動(dòng)時(shí)間,單位是秒,y 軸是對應(yīng)的權(quán)重:
從圖上可以看出,從服務(wù)啟動(dòng)開始,每隔 6 秒權(quán)重就會(huì)加一,直到 600 秒,即 10 分鐘之后,權(quán)重變?yōu)?100。
比如當(dāng) uptime 為 60 秒時(shí),該方法的返回值為 10。
當(dāng) uptime 為 66 秒時(shí),該方法的返回值為 11。
當(dāng) uptime 為 120 秒時(shí),該方法的返回值為 20。
以此類推...
600 秒,也就是十分鐘以及超過十分鐘之后,權(quán)重均為 100,代表預(yù)熱完成。
那么這個(gè)權(quán)重是干啥用的呢?
這個(gè)就得結(jié)合著負(fù)載均衡來說了。
Dubbo 提供了如下的五種負(fù)載均衡策略:
- Random LoadBalance :「加權(quán)隨機(jī)」策略
- RoundRobin LoadBalance:「加權(quán)輪詢」策略
- LeastActive LoadBalance:「最少活躍調(diào)用數(shù)」策略
- ShortestResponse LoadBalance:「最短響應(yīng)時(shí)間」策略
- ConsistentHash LoadBalance:「一致性 Hash」 策略
除了一致性哈希策略外,其他的四個(gè)策略都得用到權(quán)重這個(gè)參數(shù):
權(quán)重,就是用來決定這次請求發(fā)送給哪個(gè)服務(wù)的一個(gè)關(guān)鍵因素。
我給你畫個(gè)示意圖:
A、B、C 三臺服務(wù),A,B 的權(quán)重都是 100,C 服務(wù)剛剛啟動(dòng)。
作為一個(gè)剛剛啟動(dòng)的服務(wù),是不適合接受突發(fā)流量的,以為運(yùn)行在服務(wù)器上的代碼還沒有經(jīng)過充分的編譯,主鏈接上的代碼可能還沒有進(jìn)入編譯器的 C2 階段。
所以按理來說 C 服務(wù)需要一個(gè)服務(wù)預(yù)熱的過程,也就是剛剛啟動(dòng)的前 10 分鐘,應(yīng)該有逐步接受越來越多的請求這樣的一個(gè)過程。
比如最簡單的加權(quán)隨機(jī)輪詢的負(fù)載均衡策略中,關(guān)鍵代碼是這樣的:
org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance#doSelect
看不明白沒關(guān)系,我再給你畫個(gè)圖。
在 C 服務(wù)啟動(dòng)的第 1 分鐘,它的權(quán)重是 10:
所以代碼中的 totalWeight=210,因此下面這行代碼就是隨機(jī)生成 210 之內(nèi)的一個(gè)數(shù)字:
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
在示意圖中有三個(gè)服務(wù)器,所以 for 循環(huán)中的 lenght=3。
weights[] 這個(gè)數(shù)組是個(gè)啥玩意呢?
看一眼代碼:
每次循環(huán)的時(shí)候把每個(gè)服務(wù)器的權(quán)重匯總起來,放到 weights[] 里面。
在上面的例子中也就是這樣的:
- weights[0]= 100(A服務(wù)器的權(quán)重)
- weights[1]= 100(A服務(wù)器的權(quán)重)+100(B服務(wù)器的權(quán)重)=200
- weights[2]= 100(A服務(wù)器的權(quán)重)+100(B服務(wù)器的權(quán)重)+10(C服務(wù)器的權(quán)重)=210
當(dāng)隨機(jī)數(shù) offset 在 0-100 之間,A 服務(wù)器處理本次請求。在 100-200 之間 B 服務(wù)器處理本次請求。在 200-210 之間 C 服務(wù)器處理本次請求:
也就是說:C 服務(wù)器有一定的概率被選上,來處理這一次請求,但是概率不大。
怎么概率才能大呢?
權(quán)重要大。
權(quán)重怎么才大呢?
啟動(dòng)時(shí)間長了,權(quán)重也隨之增大了。
比如服務(wù)啟動(dòng) 8 分鐘之后,就變成這樣了,C 服務(wù)器被選中的概率就大了很多:
最后到 10 分鐘之后,三臺服務(wù)器的權(quán)重一致,承擔(dān)的流量也就幾乎一致了。
C 服務(wù)器承擔(dān)的請求隨著服務(wù)啟動(dòng)時(shí)間越來越多,直到 10 分鐘后到達(dá)一個(gè)峰值,這就算是經(jīng)歷了一個(gè)預(yù)熱的過程。
前面介紹的就是一個(gè)預(yù)熱的手段,而類似于這樣的預(yù)熱思想你在其他的一些網(wǎng)關(guān)類的開源項(xiàng)目中也能找到類似的源碼。
但是預(yù)熱不只是有這樣的一個(gè)實(shí)現(xiàn)方式。
比如阿里基于 OpenJDK 搞了一個(gè) Alibaba Dragonwell,其實(shí)也就是一個(gè) JDK。
https://github.com/alibaba/dragonwell8
其中之一的特性就是預(yù)熱:
除了預(yù)熱這個(gè)點(diǎn)之外,我還在知乎的回答中提到了最少活躍數(shù)負(fù)載均衡策略的實(shí)現(xiàn) LeastActiveLoadBalance.java:
從初始化提交之后,一共就沒修改過幾次。
你也可以對比一下,初始版本和當(dāng)前最新的版本,核心算法、核心邏輯基本沒有發(fā)生變化:
除了這個(gè)策略之外,其他的幾個(gè)策略也是差不多類似的“穩(wěn)定”。
從評論說起
我在知乎回答這個(gè)問題的時(shí)候,沒有上面這一小節(jié)寫的那么多,但是核心內(nèi)容大概就是上面這些。
在回答說提到預(yù)熱,我是想表達(dá)看似不起眼的兩行代碼,背后還是蘊(yùn)含了非常多的深層次的原因,我覺得這是有“技術(shù)含量”的。
而提到負(fù)載均衡策略的實(shí)現(xiàn),多年來都沒有怎么變化,我是想表達(dá)這些重要的、底層的、基礎(chǔ)的代碼,寫好之后,長年沒動(dòng),說明最開始寫出來的代碼是非常穩(wěn)定的。能寫出這樣穩(wěn)定的代碼,我覺得這也是有“技術(shù)含量”的。
接著帶你看看評論區(qū):
評論幾乎是清一色的不認(rèn)可這個(gè)回答。但是我前面說了,在回答這個(gè)問題的時(shí)候,確實(shí)覺得我的回答是比較貼近主題的。
但是看了評論之后我想明白了,為什么這是一個(gè)不好的答案,正如評論區(qū)說的:
例子舉得不行,只不過是因?yàn)橐鉀Q的問題一直沒有發(fā)生改變,所以解決方案也就相對穩(wěn)定。
首先這樣的代碼本來就和絕大部分程序員實(shí)際工作中寫的代碼差距過大,框架的源碼值得學(xué)習(xí),但是在實(shí)際開發(fā)中的借鑒意義不大。
而且評論區(qū)也提到了,絕大多數(shù)程序員根本就沒有機(jī)會(huì)去寫這樣的比較考驗(yàn)“技術(shù)能力”的代碼。
這也確實(shí)是事實(shí),少部分中間件的開發(fā)和絕大部分業(yè)務(wù)邏輯的開發(fā),是兩個(gè)思維模式完全不一樣的程序員群體。
然后我看了一下這個(gè)話題下的高贊回答:
其實(shí)高贊回答就這一句話:
一個(gè)優(yōu)秀的程序員,在接到一個(gè)要編寫“毀滅地球”的任務(wù)的時(shí)候,他不會(huì)簡單的寫一個(gè)destroyEarth()的方法;而是會(huì)寫一個(gè)destroyPlanet()的方法,將earth作為一個(gè)參數(shù)傳進(jìn)去。
這才是比較貼近我們實(shí)際工作的一個(gè)例子。
就著這個(gè)例子,我換個(gè)常規(guī)一點(diǎn)的需求來說,比如讓你接入一個(gè)微信支付的需求:
你可能會(huì)這樣去定義一個(gè)類:
public class WechatPayService {
public void wechatPayment(){
//微信支付相關(guān)邏輯
}
}
當(dāng)要使用的時(shí)候,就把 WechatPayService 注入到需要使用的地方,沒有任何毛病。
但是隨著而來的一個(gè)需求是讓你接入支付寶支付。
你當(dāng)然是自然而然的搞了一個(gè)類似的類:
public class AliPayService {
public void aliPayment(){
//支付寶支付相關(guān)邏輯
}
}
但是你寫著寫著發(fā)現(xiàn):誒,怎么回事,感覺支付寶的這套邏輯和微信的有很多相似之處啊,開發(fā)的關(guān)鍵步驟感覺都一模一樣?
于是你定義了一個(gè)接口,使用策略模式來專門干“支付”相關(guān)需求:
public interface IPayService {
/**
* 支付抽象接口
*/
public void pay();
}
在我看來,這是一個(gè)非常常規(guī)的開發(fā)方案,我甚至在拿到“微信支付”這個(gè)需求的時(shí)候,我就輕車熟路的知道應(yīng)該使用策略模式來做這個(gè)需求,為了方便以后的開發(fā)。
但是,我這個(gè)“輕車熟路”也是有一個(gè)熟悉的過程的,我也不是一開始,一入行,一工作就知道應(yīng)該這樣去寫的。
我是在工作之后,看了大量的實(shí)際項(xiàng)目里面的代碼,看到項(xiàng)目在用,覺得這樣很實(shí)用,項(xiàng)目結(jié)構(gòu)也很清晰,才在其它的類似的需求中,刻意的模仿學(xué)習(xí)、理解、運(yùn)用、打磨,慢慢的融入到了自己的編碼習(xí)慣中去,由于太過熟悉,我漸漸的認(rèn)為這是沒有技術(shù)含量的東西。
直到后來,有一次我?guī)е粋€(gè)實(shí)習(xí)生做一個(gè)項(xiàng)目,項(xiàng)目中有一個(gè)排行榜的功能,排行榜需要支持各個(gè)維度,前端請求的時(shí)候會(huì)告訴我當(dāng)前是需要展示哪個(gè)排行榜。
在需求分析、系統(tǒng)設(shè)計(jì)以及代碼落地階段我都自然而然的想到了前面說到的策略模式。
后來實(shí)習(xí)的同學(xué)看到了這一段邏輯,給我說:這個(gè)需求的實(shí)現(xiàn)方式真好。如果讓我來寫,我絕對想不出這樣的落地方案。
但是我覺得這就是個(gè)常規(guī)解決方案而已。
我舉這個(gè)例子是想表達(dá)的意思就是對于“技術(shù)含量”這個(gè)東西,每個(gè)人,每個(gè)階段的理解是截然不同的。
與我而言,站在我現(xiàn)在正在寫這篇文章的時(shí)間節(jié)點(diǎn)上,我覺得有技術(shù)含量的代碼,就是別人看到后愿意使用,愿意模仿,愿意告訴后面來的人:這個(gè)東西真不錯(cuò),你也可以用一用。
它可以小到一個(gè)項(xiàng)目里面的只有寥寥幾行的方法類,也可以大到一套行業(yè)內(nèi)問題的完整的技術(shù)解決方案。
除了這個(gè)例子外,我還想舉我剛剛參加工作不久,遇到過的另外一個(gè)例子。
需求說來也很簡單,就是針對一個(gè)表的增刪改查操作,也就是我們常常吐槽的沒有技術(shù)含量的 crud。
但是,我當(dāng)時(shí)看到別人提交的代碼時(shí)我都震驚了。
比如一個(gè)新增操作,所有的邏輯都在一個(gè) controller 里面,沒有所謂的 service 層、dao 層,一把梭直接把 mapper 注入到了 controller 里面,在一個(gè)方法里面從數(shù)據(jù)校驗(yàn)到數(shù)據(jù)庫交互全部包圓了。
功能能用嗎?
能用。
但是這樣代碼是有“技術(shù)含量”的代碼嗎?
我覺得可以說是毫無技術(shù)含量了,用現(xiàn)在的流行語來說,我甚至覺得這是程序員在“擺爛”。
我要基于對于這一段代碼繼續(xù)開發(fā)新功能,我能做什么呢?
我無能為力,原來的代碼實(shí)在不想去動(dòng)。
我只能保證在這堆“屎山”上,我新寫出來的代碼是干凈的、清晰的,不繼續(xù)往里面扔垃圾。
后來我讀了一本書,叫做《代碼整潔之道》,里面有一個(gè)規(guī)則叫做“童子軍軍規(guī)”。
軍規(guī)中有一句話是這樣的:讓營地比你來時(shí)更干凈。
類比到代碼上其實(shí)就是一件很小的事情,比如只是改好一個(gè)變量名、拆分一個(gè)有點(diǎn)過長的函數(shù)、消除一點(diǎn)點(diǎn)重復(fù)代碼,清理一個(gè)嵌套 if 語句...
這是讓項(xiàng)目代碼隨著時(shí)間流逝而越變越好的最簡單的做法,持續(xù)改進(jìn)也是專業(yè)性的內(nèi)在組成部分。
我覺得我對于這一點(diǎn)“規(guī)則”落實(shí)的還是挺好的,看到一些不是我寫的,但是我覺得可以有更好的寫法時(shí),而且改動(dòng)起來非常簡單,不影響核心功能的時(shí)候,我會(huì)主動(dòng)去改一下。
我能保證的是,這段代碼在經(jīng)過我之后,我沒有讓它更加混亂。
把一段混亂的代碼,拆分的清晰起來,再后來的人愿意按照你的結(jié)構(gòu)繼續(xù)往下寫,或者繼續(xù)改進(jìn)。
你說這是在寫“有技術(shù)含量”的代碼嗎?
我覺得不是。
但是,我覺得這應(yīng)該是在追求寫“有技術(shù)含量”的代碼之前,必須要具備的一個(gè)能力。而且是比寫出“有技術(shù)含量”的代碼更加重要的一個(gè)基礎(chǔ)能力。
延伸
以上就是我個(gè)人的一點(diǎn)觀點(diǎn),但是我還想延伸出一些別的東西。
比如在寫文章之前,我也在思否網(wǎng)站上提出了這個(gè)問題:
https://segmentfault.com/q/1010000042111980
大家見仁見智,從各個(gè)角度給出了不同的回答。
這也再次印證了前面我說的觀點(diǎn):
對于“技術(shù)含量”這個(gè)東西,每個(gè)人,每個(gè)階段的理解是截然不同的。
我把大家給我的回復(fù)貼過來,希望能對你也有幫助:
再比如我最近在知乎上看到了這樣的一個(gè)視頻:
https://www.zhihu.com/zvideo/1542577108190068737?page=ogv
里面的主人公黃玄,說了這樣的一段話:
這已經(jīng)是另外一個(gè)維度的程序員,對于“什么是有技術(shù)含量的代碼”的另外一個(gè)維度的解答了。
我遠(yuǎn)遠(yuǎn)達(dá)不到這個(gè)高度,但是我喜歡這個(gè)回答:
不斷的傳承下去,成為下一代軟件,或者說下一代人類文明的基石。我覺得能夠去參與這樣的東西,對我來說,可能是程序員的一種浪漫。
所以你呢,對于這個(gè)問題,你會(huì)給出什么樣的答案呢?
好了,那本文的技術(shù)部分就到這里啦。