技管思維 logo

技管思維

Subscribe
Archives
February 22, 2022

不用LeetCode,只要Show me the code

(由於Revue已經有兩次沒送出去的記錄了,因此今天開始會測試一下 buttondown.email 這個服務,如果用起來也順利的話,我就會開始慢慢改用buttondown。)

關於找新人這件事…

從開始接任管理職之後,隨即而來的就是招募新人這件事。這其實是很「個人化」的事,每個管理者有自己的管理風格,當然就會有自己的招募風格。有的人比較一板一眼,職能不足直接刷掉,不必相送;有的人喜歡直接和面試者聊天,聊得來就用。這並沒有什麼對錯可言,職位需求不同,管理者需求不同,招募的風格就會不同。

今天的主題就是說說,我是怎麼徵軟體工程師的。

不是「用不下去」就是「留不下來」

許多漫畫都出現過這類的橋段(其實三井壽的例子不是100%準確,但意思到了各位知道就行😅),主角雖不是能力最好的,但是他有熱情,所以我們應該給他機會,他之後一定會有爆發性的成長…

只是理想很豐滿,現實很骨感。我也曾在「職能之後都可以再練,有熱情最重要」的slogan下,找過一些「用不下去」或「留不下來」的人。「用不下去」比較好理解,就是距離真的太遠了,他真的要能上線工作,根本還需要個6個月到1年的那種。「留不下來」是有的是進來沒多久就跟你說,他覺得自己的興趣不在這裡,或是做久了沒興趣,要跟自己的朋友去創業了等等。不過不管是哪一種,很巧合的是他們當時的職能都沒有到達我心中的標準,只是堪用,但就是覺得他們”有熱情”,職能都可以再練,所以就用了。

回到現實,至少先求「用得下去」

經過幾次這樣的經驗後,就和同事認真討論,調整了職位說明,面試題目,最重要的是調整了心中的標準 — 職能程度不能再放水了。「留不下來」我們很難掌握,但至少不能「用不下去」。從面試,進公司,新人訓練,交辦任務,到最後交接任務,揮手道別。做過管理者的就知道,花費的時間及心力成本非常可觀,真的是傷不起。

在調整試題及錄用標準後,在筆試後就刷掉的人就更多了。大多數是去資策會上過一期課程就來的,或是學歷很漂亮(最高有一個是台大資管的)的。看著大量的工作壓著現有的組員喘不過氣來的樣子,雖然著急,但心中也知道,把標準降下來放行的話,最後只會增加無謂的成本。

所幸之後真的就陸陸續續找到了一些很不錯的組員進來。隨著新人訓練順利完成,開始真的交辦任務後,才發現他們技術能力比我預期的還要好:有的人對自己的工作非常有衝勁,不看到一個結果捨不得下班;有的人不慍不火,卻默默的幫助其他組員或其他部門的人,解決一個又一個的疑難雜症;有的人技能真的是非常強,一個人可以當兩個人用(不是苦力的那種,而是他的know-how可以當兩種人用);之前還找到一個中途轉換跑道,只為了追求遊戲製作夢,還跑到國外學校就讀的,他的職能也很好,對我們而言最大的額外幫助是可充當英文口譯。

最重要的是,他們都很清楚自己的角色定位,也認同這個組職。

職能之後都可以再練,有熱情最重要?

我後來重新思考「職能之後都可以再練,有熱情最重要」這句話。以我自己為例,我因為有投入遊戲產業的熱情,所以我會自己去學程式,看人家遊戲的規格,思考怎麼設計會好玩,為什麼好玩之類的職能,我不但會去學,我還一定會動手做,實際做做才知道嘛!不然哪叫做「我想做遊戲」呢?所以一反過來想就完全想通了:

在領域知識取得成本為零的前提下,熱情就是職能,就是對自己的要求。

讓我們再想得更深入一點。

意向已經清楚的面試者,他們在職能方面必然有所準備。那他們會端出什麼來說服你要錄用他呢?當然是他們最有熱情,最拿手的那些好菜。美術會秀出他畫的東西,告訴你這是什麼畫風,他喜歡誰的作品,他最擅長的是UI還是角色動畫等;軟體會直接打開他寫的遊戲,可能跟某個你知道的遊戲相像,但他一定會目露火光的告訴你,他做了什麼別人沒有的規格,用了什麼技術,效能多好等等;編導可能就會興高采烈的拿幾個劇本或設定告訴你,他的TA(Target Audience)是哪種玩家,同類的遊戲大致上是哪些規格,他想了變體,市場上受歡迎的規格有哪裡不同,他的故事營造了哪些起伏,絕對可讓玩家有不同感受等。

或許你不一定認同或懂得欣賞他們的作品,不過你一定看得出來同一件事:他們真的有”產品”,勇於表達出自己的意見,而且能說出和別人的作品異同之處,這才叫有熱情的。遊戲作品是這樣的,不是聲光特效規格越高就越好玩,通常都是越有自己靈魂的產品,會越受到玩家認同。

有實力當然要用,有實力的怪咖更要用

在確實說明清楚我們這邊的職務內容,是什麼,不是什麼之後,基本上應該就不會再有意向不對的狀況了。若職能很不錯,最後一關來到了自己的老板及人資部門,他們會再從不同的角度,來探探面試者的「忠誠度」。是真的打從心力認同我們公司,要進來一起打拼呢?還是只是要沾公司的品牌,或是先求有再求好,騎驢找馬型的呢?能力很好,但會不會難以相處,難以配合,難以要求呢?

老板有他的閱歷經驗,人資部門也有他們的工具及專業,若他們確定這個人問題不小,那我想我們就不用堅持了,就送客出門,大家洗洗睡吧。但有些狀況就比較難定案了,像是他的表現很OK,可是對職涯規劃又不明確告知的人呢?又若是面試者的實力真的很誘人,但表達立場的態度又有那麼點小硬的時候(就怕真的進來會跟大家處不好…),我們又怎麼決定要不要用他呢?

其實我們自己又何嘗不是這樣?我們有實力,不就是因為有自己的立場,自己的堅持嗎?

對自己肯要求,才會有實力及個性出來,而這種人才是真正會關心產品的人。能在專業領域確實有兩把刷子,必然就是要有多面向看事情,實務適應力好,能隨時自省的能力,才能養成在實務需求上真正的專業,而這種能力就是大家能好好合作的關鍵能力。只要透過好的試題,挖出真正專業的面試者,你其實不可能找到難合作的,因為他的專業已說明了一切。

好,專業人才跟合作愉快,我想是直接相關的。所以下一步,我們就得回頭思考我們招募者應該要做好的功課:設計一份好試題。

好試題可以挖得快,更可以挖得深,挖到寶

招募過程中常見的一道關卡是「筆試」,可能有人會覺得,都2022年了,為什麼還要有「筆試」這麼古老又不討喜的過程呢?

「用筆寫」就是用心寫

「用筆寫」是很慢,但很便宜1,又可以挖到最多面試者細節的方法。因為會花時間寫,就會要求出題者去好好思考,自己想問的重點是什麼;當然也就會要求面試者好好想想,自己該寫什麼才是重點。不會的就得趕緊跳過,不能花時間寫一堆無關緊要的「補充資料」。

我們要想的,就是能用筆寫的「好試題」

因為要用筆寫,所以傳統那種問答題(例如「試說明什麼是Thread?」)就是完全不適合的。我們要想的就是能在最簡回答型式的前提下,挖出面試者最多最深程度的題目,也就是「高CP值」的題目。通常這樣子的題目,都會有以下的特徵:

  1. 找出問題
  2. 比較差異
  3. 預期結果

找出問題

這個手法用來瞭解面試者的程度有多深很好用,而重點就是儘量多埋一些問題給面試者來找,舉個例子:

// main.cpp:  
#include <cstdio>  
#include <cstdlib>  
#include <string>  
#include <iostream>  
char* get_full_name();  
void show_names();  
void new_names();  
std::string get_gateway_info();  
std::string get_dns_info();  
int main(int argc,char** argv)  
{  
    show_names();  
    new_names();  
    printf("New full name:%s\n",get_full_name());  
    printf("%s\n",get_gateway_info());  
    std::cout << get_dns_info() << std::endl;  
    exit(EXIT_SUCCESS);  
}

// profile.cpp:  
#include <cstdio>  
#include <cstring>  
#include <string>  
static char first_name[12];  
static char last_name[12];  
static std::string default_gateway = "192.168.1.254";  
static std::string default_dns = "192.168.1.1";  
void show_names()  
{  
    printf("%s %s\n",first_name,last_name);  
}  
void new_names()  
{  
    printf("Input first name: ");  
    scanf("%s",first_name);  
    printf("Input last name: ");  
    scanf("%s",last_name);  
}  
char* get_full_name()  
{  
    char rtn[24];  
    strncpy(rtn,first_name,sizeof(first_name));  
    strncpy(rtn+sizeof(first_name),last_name,sizeof(last_name));  
    return rtn;  
}  
std::string get_gateway_info()  
{  
    std::string rtn;  
    rtn = "IPv4 Gateway:" + default_gateway;  
    return rtn;  
}  
std::string get_dns_info()  
{  
    std::string rtn;  
    rtn = "IPv4 DNS:" + default_dns;  
    return rtn;  
}

把這兩個模組建置出main.exe,執行後卻看到這樣的結果:

Input first name: foo
Input last name: bar
New full name:(null)
�)
IPv4 DNS:192.168.1.1

我們要問面試者的題目是:

  • 為何「New full name」會顯示出(null)?
  • 「�)」應該是要秀出「IPv4 Gateway:192.168.1.254」的,為何會變成亂碼?

這兩個題目是看得到的問題,但高手們已經看到還有其他的坑2了,對嗎?是的,所以我們還可以繼續追問:

  • 有時程式一運行起來就會崩潰,有時又不會,問題是出在哪兒呢?
  • 用戶若輸入過長的名字(超過12個字),會發生什麼問題?

這樣就可以一路一直往下挖,看看面試者的程度有多深。

雖然上述的例子是找軟體工程師的例子,但大原則是一樣的:

就像「踩地雷」一樣,只要你埋的雷夠經典,很快就可以把職能明顯不足的都炸飛。

比較差異

這個手法很適合拿來考那些似是而非,但實務上卻常踩的那些坑。這樣子的問題很容易考出面試者是否有足夠的實戰經驗,也可以知道職能基礎是不是夠紮實,像是:

// 下列程式印出來的數值會是一樣的嗎?為什麼?  
char phone_number[11]="0912345678";  
printf("sizeof()=%d,",sizeof(phone_number));  
printf("strlen()=%d\n",strlen(phone_number);

或是:

// 最終印出來的結果為何?為什麼?  
int foo(int value)  
{  
    int rtn;  
    value += 1;  
    rtn = value;  
    return rtn;  
}  
int bar(int* value)  
{  
    int rtn;  
    *value += 1;  
    rtn = *value;  
    return rtn;  
}  
int main(int argc,char** argv)  
{  
    int a=1;  
    int b = foo(a);  
    printf("a=%d,b=%d\n",a,b);  
    int c = bar(&a);  
    printf("a=%d,c=%d\n",a,c);  
    exit(EXIT_SUCCESS);  
}

C/C++是這樣的,其他領域必然也會有這種差一點差很多的例子。A和B要能夠比較差異,那就要A跟B都要有清楚的瞭解,才能比較得出來。一個問題就可以問到3件事(A,B跟差異),這CP值能不漂亮嗎?

預期結果

這個手法主要的重點是「改變環境 → 預期結果」。搭配前兩個手法一起使用,就會有1+1>2的效果。

這裡舉一個較大的例子。這樣一題C/C++的考題,搭配了「比較差異」的手法,就可以把物件導向觀念的精髓考出來,一題就夠了。

// main.cpp:  
#include <cstdio>  
#include <cstdlib>  
#include <iostream>  
#include "dog.h"  

int main(int argc,char** argv)  
{  
    dog kuro;  
    animal* shiro = new puppy();  

    std::cout << kuro.get_class() ;  
    std::cout << " , ";  
    std::cout << shiro->get_class() << std::endl;  
    kuro.sleep(); shiro->sleep();  
    kuro.walk(); shiro->walk();  
    exit(EXIT_SUCCESS);  
}  

// animal.h:  
#pragma once  
#include <string>  
class animal  
{  
    public:  
    std::string get_class();  
    virtual void sleep();  
    virtual void walk()=0;  
}; 

// animal.cpp:  
#include <cstdio>  
#include "animal.h"  

std::string animal::get_class()  
{  
    char signature[64]={0};  
    sprintf(signature,"%s",__FUNCTION__);  
    return std::string(signature);  
}  
void animal::sleep()  
{  
    printf("%s:%d>\n",__FILE__,__LINE__);  
}  

// dog.h:  
#pragma once    
#include "animal.h"  

class dog : public animal  
{  
    public:  
    std::string get_class();  
    void walk();  
};  

class puppy : public dog  
{  
    public:  
    virtual void sleep();  
    void walk();  
};  

// dog.cpp:  
#include <cstdio>  
#include "dog.h"  

std::string dog::get_class()  
{  
    char signature[64]={0};  
    sprintf(signature,"%s:%d>",__FILE__,__LINE__);  
    return std::string(signature);  
}  
void dog::walk()  
{  
    printf("dog::walk() >> \n");  
}  
void puppy::sleep()  
{  
    printf("%s:%d>\n",__FILE__,__LINE__);  
}  
void puppy::walk()  
{  
    printf("%s\n",__PRETTY_FUNCTION__);  
}

這樣一題我們可以問出許多「預期結果」:

  1. 請寫出預期的執行結果。
  2. 若將shiro的宣告改為「dog* shiro = new puppy();」最終的結果會一樣嗎?為什麼?
  3. 若將「dog::get_class()」的宣告及實作都移除,最終的結果差異為何?
  4. 若將「dog::walk()」的宣告改為「virtual dog::walk();」,最終的結果會一樣嗎?為什麼?
  5. 將「puppy::sleep()」的宣告及實作都移除,最終的結果差異為何?
  6. 將「puppy::walk()」的宣告及實作都移除,最終的結果差異為何?

「預期結果」類的問題,常會混入「找出差異」的要素。但不變的是,面試者的觀念必須非常清楚,才能從這堆看來都很像的源碼中找出答案。

真正的熱情不需要LeetCode,而是「Show me the code」

我聽過很多人很喜歡去刷LeetCode,好像那是一種專業能力的認證一樣(突然覺得我不是個軟體工程師…😅)。其實我們實務上的實作需求都很簡單,並沒有LeatCode那麼刁鑽。反倒是在軟體架構上的部分,需要花多一點的時間去思考及設計。上述的題目雖是C/C++為主,但不同領域的軟體開發應該都可以用同樣的原則。

我很喜歡Linus Torvalds的那句名言:「Talk is cheap, show me the code.」他也說了,他寫的碼其實也沒多漂亮,但能好好的解決問題。我想招募這件事也是一樣的邏輯,你想要找對這份職務真正有熱情的人?那就請他們Show出東西來吧,他們的專業是用多少熱情培養出來的,你一看就會知道。


  1. 「很便宜」是指不用準備電腦的相關環境:輸入法,網咖般的環境限制(不能叫控制台,改登錄碼或命令列等),網域權限,試題暫存檔或解答…所以給他一份紙本考卷跟一支筆,實在是便宜多了。 ↩

  2. 「其他的坑」是first_name及last_name沒初始化,get_full_name()把區域變數的位址傳回。 ↩

Don't miss what's next. Subscribe to 技管思維:
Powered by Buttondown, the easiest way to start and grow your newsletter.