不用LeetCode,只要Show me the code
(由於Revue已經有兩次沒送出去的記錄了,因此今天開始會測試一下 buttondown.email 這個服務,如果用起來也順利的話,我就會開始慢慢改用buttondown。)
關於找新人這件事…
從開始接任管理職之後,隨即而來的就是招募新人這件事。這其實是很「個人化」的事,每個管理者有自己的管理風格,當然就會有自己的招募風格。有的人比較一板一眼,職能不足直接刷掉,不必相送;有的人喜歡直接和面試者聊天,聊得來就用。這並沒有什麼對錯可言,職位需求不同,管理者需求不同,招募的風格就會不同。
今天的主題就是說說,我是怎麼徵軟體工程師的。
不是「用不下去」就是「留不下來」
許多漫畫都出現過這類的橋段(其實三井壽的例子不是100%準確,但意思到了各位知道就行😅),主角雖不是能力最好的,但是他有熱情,所以我們應該給他機會,他之後一定會有爆發性的成長…
只是理想很豐滿,現實很骨感。我也曾在「職能之後都可以再練,有熱情最重要」的slogan下,找過一些「用不下去」或「留不下來」的人。「用不下去」比較好理解,就是距離真的太遠了,他真的要能上線工作,根本還需要個6個月到1年的那種。「留不下來」是有的是進來沒多久就跟你說,他覺得自己的興趣不在這裡,或是做久了沒興趣,要跟自己的朋友去創業了等等。不過不管是哪一種,很巧合的是他們當時的職能都沒有到達我心中的標準,只是堪用,但就是覺得他們”有熱情”,職能都可以再練,所以就用了。
回到現實,至少先求「用得下去」
經過幾次這樣的經驗後,就和同事認真討論,調整了職位說明,面試題目,最重要的是調整了心中的標準 — 職能程度不能再放水了。「留不下來」我們很難掌握,但至少不能「用不下去」。從面試,進公司,新人訓練,交辦任務,到最後交接任務,揮手道別。做過管理者的就知道,花費的時間及心力成本非常可觀,真的是傷不起。
在調整試題及錄用標準後,在筆試後就刷掉的人就更多了。大多數是去資策會上過一期課程就來的,或是學歷很漂亮(最高有一個是台大資管的)的。看著大量的工作壓著現有的組員喘不過氣來的樣子,雖然著急,但心中也知道,把標準降下來放行的話,最後只會增加無謂的成本。
所幸之後真的就陸陸續續找到了一些很不錯的組員進來。隨著新人訓練順利完成,開始真的交辦任務後,才發現他們技術能力比我預期的還要好:有的人對自己的工作非常有衝勁,不看到一個結果捨不得下班;有的人不慍不火,卻默默的幫助其他組員或其他部門的人,解決一個又一個的疑難雜症;有的人技能真的是非常強,一個人可以當兩個人用(不是苦力的那種,而是他的know-how可以當兩種人用);之前還找到一個中途轉換跑道,只為了追求遊戲製作夢,還跑到國外學校就讀的,他的職能也很好,對我們而言最大的額外幫助是可充當英文口譯。
最重要的是,他們都很清楚自己的角色定位,也認同這個組職。
職能之後都可以再練,有熱情最重要?
我後來重新思考「職能之後都可以再練,有熱情最重要」這句話。以我自己為例,我因為有投入遊戲產業的熱情,所以我會自己去學程式,看人家遊戲的規格,思考怎麼設計會好玩,為什麼好玩之類的職能,我不但會去學,我還一定會動手做,實際做做才知道嘛!不然哪叫做「我想做遊戲」呢?所以一反過來想就完全想通了:
在領域知識取得成本為零的前提下,熱情就是職能,就是對自己的要求。
讓我們再想得更深入一點。
意向已經清楚的面試者,他們在職能方面必然有所準備。那他們會端出什麼來說服你要錄用他呢?當然是他們最有熱情,最拿手的那些好菜。美術會秀出他畫的東西,告訴你這是什麼畫風,他喜歡誰的作品,他最擅長的是UI還是角色動畫等;軟體會直接打開他寫的遊戲,可能跟某個你知道的遊戲相像,但他一定會目露火光的告訴你,他做了什麼別人沒有的規格,用了什麼技術,效能多好等等;編導可能就會興高采烈的拿幾個劇本或設定告訴你,他的TA(Target Audience)是哪種玩家,同類的遊戲大致上是哪些規格,他想了變體,市場上受歡迎的規格有哪裡不同,他的故事營造了哪些起伏,絕對可讓玩家有不同感受等。
或許你不一定認同或懂得欣賞他們的作品,不過你一定看得出來同一件事:他們真的有”產品”,勇於表達出自己的意見,而且能說出和別人的作品異同之處,這才叫有熱情的。遊戲作品是這樣的,不是聲光特效規格越高就越好玩,通常都是越有自己靈魂的產品,會越受到玩家認同。
有實力當然要用,有實力的怪咖更要用
在確實說明清楚我們這邊的職務內容,是什麼,不是什麼之後,基本上應該就不會再有意向不對的狀況了。若職能很不錯,最後一關來到了自己的老板及人資部門,他們會再從不同的角度,來探探面試者的「忠誠度」。是真的打從心力認同我們公司,要進來一起打拼呢?還是只是要沾公司的品牌,或是先求有再求好,騎驢找馬型的呢?能力很好,但會不會難以相處,難以配合,難以要求呢?
老板有他的閱歷經驗,人資部門也有他們的工具及專業,若他們確定這個人問題不小,那我想我們就不用堅持了,就送客出門,大家洗洗睡吧。但有些狀況就比較難定案了,像是他的表現很OK,可是對職涯規劃又不明確告知的人呢?又若是面試者的實力真的很誘人,但表達立場的態度又有那麼點小硬的時候(就怕真的進來會跟大家處不好…),我們又怎麼決定要不要用他呢?
其實我們自己又何嘗不是這樣?我們有實力,不就是因為有自己的立場,自己的堅持嗎?
對自己肯要求,才會有實力及個性出來,而這種人才是真正會關心產品的人。能在專業領域確實有兩把刷子,必然就是要有多面向看事情,實務適應力好,能隨時自省的能力,才能養成在實務需求上真正的專業,而這種能力就是大家能好好合作的關鍵能力。只要透過好的試題,挖出真正專業的面試者,你其實不可能找到難合作的,因為他的專業已說明了一切。
好,專業人才跟合作愉快,我想是直接相關的。所以下一步,我們就得回頭思考我們招募者應該要做好的功課:設計一份好試題。
好試題可以挖得快,更可以挖得深,挖到寶
招募過程中常見的一道關卡是「筆試」,可能有人會覺得,都2022年了,為什麼還要有「筆試」這麼古老又不討喜的過程呢?
「用筆寫」就是用心寫
「用筆寫」是很慢,但很便宜1,又可以挖到最多面試者細節的方法。因為會花時間寫,就會要求出題者去好好思考,自己想問的重點是什麼;當然也就會要求面試者好好想想,自己該寫什麼才是重點。不會的就得趕緊跳過,不能花時間寫一堆無關緊要的「補充資料」。
我們要想的,就是能用筆寫的「好試題」
因為要用筆寫,所以傳統那種問答題(例如「試說明什麼是Thread?」)就是完全不適合的。我們要想的就是能在最簡回答型式的前提下,挖出面試者最多最深程度的題目,也就是「高CP值」的題目。通常這樣子的題目,都會有以下的特徵:
- 找出問題
- 比較差異
- 預期結果
找出問題
這個手法用來瞭解面試者的程度有多深很好用,而重點就是儘量多埋一些問題給面試者來找,舉個例子:
// 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__);
}
這樣一題我們可以問出許多「預期結果」:
- 請寫出預期的執行結果。
- 若將shiro的宣告改為「dog* shiro = new puppy();」最終的結果會一樣嗎?為什麼?
- 若將「dog::get_class()」的宣告及實作都移除,最終的結果差異為何?
- 若將「dog::walk()」的宣告改為「virtual dog::walk();」,最終的結果會一樣嗎?為什麼?
- 將「puppy::sleep()」的宣告及實作都移除,最終的結果差異為何?
- 將「puppy::walk()」的宣告及實作都移除,最終的結果差異為何?
「預期結果」類的問題,常會混入「找出差異」的要素。但不變的是,面試者的觀念必須非常清楚,才能從這堆看來都很像的源碼中找出答案。
真正的熱情不需要LeetCode,而是「Show me the code」
我聽過很多人很喜歡去刷LeetCode,好像那是一種專業能力的認證一樣(突然覺得我不是個軟體工程師…😅)。其實我們實務上的實作需求都很簡單,並沒有LeatCode那麼刁鑽。反倒是在軟體架構上的部分,需要花多一點的時間去思考及設計。上述的題目雖是C/C++為主,但不同領域的軟體開發應該都可以用同樣的原則。
我很喜歡Linus Torvalds的那句名言:「Talk is cheap, show me the code.」他也說了,他寫的碼其實也沒多漂亮,但能好好的解決問題。我想招募這件事也是一樣的邏輯,你想要找對這份職務真正有熱情的人?那就請他們Show出東西來吧,他們的專業是用多少熱情培養出來的,你一看就會知道。