「這支 Job 一開始只是抓資料寫進資料庫,後來陸續加了同步關聯、處理附件、呼叫另一支 Job……現在已經 100 多行了,這樣正常嗎?」
Job 會長大,通常不是一次設計錯誤造成的,是一次一次「反正加在這裡最快」的小決定累積出來的。今天要對照同一個系統裡兩支長度差非常多的 Job——一支 16 行、一支超過 120 行——具體看看「一支 Job 該做多少事」這個問題,在真實程式碼裡是怎麼被回答(或沒被回答)的。
public function handle(DataGatewayClient $client): void
{
$assigners = $client->getAssigners();
foreach ($assigners as $assigner) {
NewsAssigner::query()->firstOrCreate([
'code' => (string) $assigner->UNIT_CODE,
], [
'name' => (string) $assigner->UNIT_CODE_DESC,
'eng_name' => (string) $assigner->UNIT_ENAME,
'raw_data' => $assigner,
]);
}
}
這支 Job 只做一件事:呼叫外部系統的 SOAP 介面拿到一份清單,逐筆用 firstOrCreate 寫進資料表。沒有分頁、沒有複雜的欄位轉換、沒有呼叫其他 Job。讀這支 Job 完全不需要理解上下文,handle() 方法本身就是完整的說明文件。
另一支負責同步新聞資料的 Job,職責描述聽起來也很單純:「抓新聞、寫進資料庫」。但攤開 handle() 之後,實際發生的事情遠不只這樣:
assigners()->sync()、identities()->sync())handle() 裡呼叫 dispatch_sync(new FetchNewsFile(...))——同步派發另一支負責下載附件的 Job第 4 點是這支 Job 裡最值得停下來想一想的地方。這支 Job 本身實作了 ShouldQueue,代表它是設計成要被排進佇列、非同步執行的。但它內部呼叫附件下載邏輯的方式,用的是 dispatch_sync()——這個方法會讓被呼叫的 Job 立刻在當下的 process 裡同步執行完,不會真的排進佇列。
換句話說:外層這支 Job 已經享受了「非同步、背景執行」帶來的好處(不阻塞使用者請求),但它內部呼叫附件下載的方式,卻是同步、阻塞的——附件下載要花多久,這支 Job 的整體執行時間就要多久,「非同步」的好處在這一段完全沒發揮出來。
dispatch_sync() 通常出現在兩種情境:一是開發階段圖方便,先同步跑確認邏輯對不對,之後忘了改回 dispatch();二是刻意這樣寫,因為附件下載的結果會影響到後續某個判斷,必須等它跑完才能繼續。不管是哪一種,這支 Job 目前的寫法沒有留下任何說明——沒有註解解釋「為什麼這裡故意同步呼叫」,讀程式碼的人只能自己猜。
這支 Job 裡還藏著一段被整段註解掉、沒有刪除的舊邏輯(一個查找關聯用的變數計算),留在那裡但沒有人動它。這是一個很適合套用「重構前先問這段還要不要,而不是直接刪掉」這條紀律的具體案例——死碼留著不動,跟直接刪掉,都不是無腦就能下的決定,要先搞清楚它是不是還在被依賴、或者只是單純被遺忘。
❌ 一支 Job 混了「抓資料」「轉換欄位」「寫入」「同步派發下載」四種節奏
public function handle(): void
{
$data = $this->fetchPaginatedData(); // 抓分頁
$attributes = $this->transform($data); // 轉換 30+ 欄位
$news = News::create($attributes); // 寫入
$news->assigners()->sync(...);
$news->identities()->sync(...);
dispatch_sync(new FetchNewsFile($news)); // 同步呼叫另一個 Job,失去非同步的意義
}
✅ 拆成職責單一的多支 Job,各自可以獨立測試、獨立重試
public function handle(): void
{
$data = $this->fetchPaginatedData();
$attributes = $this->transform($data);
$news = News::create($attributes);
$news->assigners()->sync(...);
$news->identities()->sync(...);
FetchNewsFile::dispatch($news); // 真正非同步排隊,失敗可以獨立重試
}
真實程式碼目前是前者。列出後者不是說「這支 Job 一定要立刻重寫」,而是提供一個具體的判斷基準:當一支 Job 內部出現「呼叫另一支本該非同步的 Job,卻用同步方式呼叫」的寫法時,這通常是職責邊界模糊的訊號,值得回頭問一句「這裡為什麼不能真的排隊」。
你的專案裡有沒有一支 Job,一開始很單純,後來陸續被加了很多事?回想一下它現在的長度,如果要你憑印象講出它「到底在做什麼」,你講得出幾件事?如果超過三件,可能已經到了該問「這些事該不該拆開」的時候。
dispatch_sync() 出現在一支 ShouldQueue 的 Job 裡,是職責邊界模糊的訊號,值得停下來問為什麼明天要看兩種不同的 Observer 註冊方式——一種用 PHP 8 的 Attribute 語法、一種用傳統的 ServiceProvider 手動註冊,同一個系統裡兩種寫法並存,背後代表什麼。