前幾天講的 Template Method、Factory,聽起來都是「顯然正確」的設計。但如果今天這個工具從頭到尾只打算處理一個來源站,這些抽象層是不是就變成過度設計?
答案是:要看的不是「現在有幾個來源」,是「這個抽象要解決的重複,實際上重複了幾次」。
只有一個來源站時,Template Method 多出來的抽象類別、@abstractmethod、介面定義,全部都是純粹的額外複雜度——沒有第二個實作可以驗證「這個抽象切得對不對」,很容易切出一個「看起來通用,實際上只服務單一情境」的介面,日後真的要加第二個來源站時,才發現介面切錯了地方,還得重構一次。
三次法則是一個常被引用的經驗法則:出現兩次重複可以先容忍,出現第三次再考慮抽象化——不是因為數字 3 有什麼魔力,而是兩個樣本不足以判斷「哪裡會變、哪裡不會變」,第三個樣本出現時,才比較看得出真正的變動邊界在哪。
❌ 反例:只有一個來源站,就先切好一層抽象
class Crawler(ABC):
@abstractmethod
def get_items(self, soup, url): ...
class OnlySite(Crawler):
def get_items(self, soup, url):
return soup.select(".only-site-list a")
這個抽象類別目前只有一個子類別,Crawler 這一層完全沒有驗證過「哪些邏輯真的共用、哪些其實是 OnlySite 專屬」。如果第二個來源站出現時結構差異很大(例如不是靠 CSS selector,而是要解析內嵌 JSON),現在切的這層抽象可能整個用不上。
✅ 正例:先讓具體實作跑起來,等第二、第三個來源出現,重複顯現出來再抽出共用骨架
# 只有一個來源站時,先寫成一支具體、不帶抽象層的類別
class OnlySiteCrawler:
async def collect(self, url):
html = await self._get_html(url)
soup = BeautifulSoup(html, 'html.parser')
return [item['href'] for item in soup.select(".only-site-list a")]
回想你維護的專案裡有沒有「只服務一個情境,卻已經先切好抽象層」的程式碼?如果拿掉那層抽象,程式碼會變簡單還是變複雜?
明天是第一部的回顧:把這五天累積的爬蟲層設計原則收斂成它的責任邊界,以及它不該負責的事。