在程式設計的世界中,存在著許多基於多年經驗所形成的「原則」和「思維方式」。這些可以說是先人們在失敗與成功的過程中所發掘出的 開發智慧。
本文將針對剛開始學習程式設計或剛進入實務的人,精選出10個知名的原則介紹,這些原則了解後一定會獲得幫助。
了解原則的好處主要有三個:
那麼,讓我們逐一來看看吧。
有參考著名書籍《可讀代碼》的部分內容。
如想了解更詳細的內容,建議閱讀。
不要重複相同的程式碼
DRY是最著名的程式設計原則之一。它的核心思想是避免在程式碼中多處寫下相同的知識或邏輯。
如果相同的處理散佈在程式碼的各個地方,每次規格變更時就必須 逐處無漏修正。修正的漏網之魚會成為錯誤的溫床。
# ❌ 違反DRY的例子:多處計算含稅金額
order_total = price * 1.10
invoice_total = item_price * 1.10
receipt_total = product_price * 1.10
# ✅ 遵循DRY的例子:將其封裝為函數
def calc_tax_included(price, tax_rate=0.10):
return price * (1 + tax_rate)
order_total = calc_tax_included(price)
invoice_total = calc_tax_included(item_price)
receipt_total = calc_tax_included(product_price)
將邏輯封裝成函數後,若稅率變更,只需在一處進行修正。
「程式碼的外觀相似」並不一定意味著違反DRY原則。假如看似相同的處理實際上依不同的商業規則執行,強行共通化反而可能導致複雜化。應該以「相同知識是否重複」為判斷基準。
保持簡單
KISS原則強調設計和程式碼應儘可能保持簡單。複雜的程式碼難以閱讀、容易出錯,且難以維護。
# ❌ 不必要地複雜
def is_adult(age):
if age >= 18:
return True
else:
return False
# ✅ 簡單
def is_adult(age):
return age >= 18
上述例子雖然極端,但實際開發中常有「想要寫得更簡潔、更聰明」的慾望,這會導致過度複雜的情況出現。過度使用設計模式、過度抽象化、巢狀過深的三元運算子等,「只有撰寫者能理解的程式碼」應避免使用。
KISS的本質是 「寫出華麗程式碼的能力」不如「寫出人人都能閱讀的程式碼的能力」更有價值。
不要實作目前不需要的東西
YAGNI原則警告不要因為「未來可能會用到」而預先實作功能。這是自極限程式設計(XP)中衍生出來的思想。
# ❌ 違反YAGNI的例子:提前構建未來誰也不會用的功能
class User:
def __init__(self, name, email):
self.name = name
self.email = email
self.phone = None # 可能將來會用
self.fax = None # 可能將來會用
self.secondary_email = None # 可能將來會用
def send_sms(self): # 現階段尚未有需求
pass
def send_fax(self): # 現階段尚未有需求
pass
# ✅ 遵循YAGNI的例子:只構建當前需求所需的功能
class User:
def __init__(self, name, email):
self.name = name
self.email = email
「未來可能需要」的程式碼會產生如下成本:
在需要的時候再進行實作就足夠了。
一個模組僅有一個責任
SRP(Single Responsibility Principle)是SOLID原則中的第一個原則。旨在使一個類別或函數只因為一個理由而發生變更的思維方式。
# ❌ 違反SRP的例子:一個類別擔任多個責任
class Report:
def calculate_totals(self, data):
# 聚合邏輯
pass
def format_as_html(self, data):
# HTML格式化邏輯
pass
def send_email(self, html):
# 電子郵件發送邏輯
pass
這個類別有「聚合」、「格式化」、「發送」三個責任。如果只想修改聚合邏輯,卻不得不接觸到發送電子郵件的代碼。
# ✅ 遵循SRP的例子:責任分離
class ReportCalculator:
def calculate_totals(self, data):
pass
class ReportFormatter:
def format_as_html(self, data):
pass
class ReportMailer:
def send_email(self, html):
pass
分離責任後,更改的影響範圍減小,測試也易於撰寫。
當有人問「這個函數(或類別)是做什麼的?」時,若回答中含有「〇〇 和 △△」等關聯詞,則可能存在多個責任。
按角色分隔程式碼
SoC(Separation of Concerns)原則強調將程式按不同的關心事(即目的或角色)進行分離。SRP主要是針對類別或函數層面的原則,而SoC則是以更廣泛的視角應用於整個系統的結構。
在網頁前端的常見例子中,關心事情可以這樣分開:
| 關心事 | 負責 |
|---|---|
| 結構(顯示什麼) | HTML |
| 外觀(如何顯示) | CSS |
| 行為(如何運作) | JavaScript |
在後端框架中被廣泛採用的 MVC(Model-View-Controller) 模式也是SoC的一個典型實踐例子。
Model — 數據與業務邏輯
View — 畫面顯示
Controller — 接受與處理使用者輸入
在dbt項目中,將staging(數據整形)和 marts(業務邏輯)分為不同目錄也可以視為SoC的實踐。
當關心事適當分離時,某部分的變更不容易影響到其他部分。
先寫出可以運行的東西,優化在之後
這句格言源自計算機科學家唐納德·克努斯的名言。在撰寫程式碼階段時,如果提前因為「這裡可能會慢」而推測進行性能調整,往往會出現如下問題:
# ❌ 過早的優化:還未進行性能分析就自行實作快取
class UserService:
def __init__(self):
self._cache = {}
self._cache_ttl = {}
def get_user(self, user_id):
if user_id in self._cache:
if time.time() - self._cache_ttl[user_id] < 300:
return self._cache[user_id]
user = self._fetch_from_db(user_id)
self._cache[user_id] = user
self._cache_ttl[user_id] = time.time()
return user
# ✅ 先寫出可以運行的東西
class UserService:
def get_user(self, user_id):
return self._fetch_from_db(user_id)
# 在性能有問題時再進行測量後優化
正確的做法是按照以下順序進行:
每當觸碰程式碼時,應將其清理得比找到時更乾淨
環衛法則源自於「在露營地時,留下的營地比來時更乾淨」的教誨。此原則應用於程式設計時,意指在修正現有程式碼時,除了原有的變更,還應同時做一些小的改善。
具體來說,以下改進都屬於此範疇:
環衛法則適用於「隨手可做的微小改善」。私自進行與本任務無關的大規模重構會產生檢查負擔或引入意外錯誤。大型改善應另立任務來進行。
即使每次都是小的改善,但如果整個團隊都養成這個習慣,程式碼基礎每天都會逐漸改善。反之,如果沒有任何人進行改善,程式碼會逐漸劣化(這被稱為 技術負債)。
不要信任輸入或外部數據
防禦性程式設計是一種針對意外輸入或突發情況,使程式能安全運行的準備思想。
用戶的輸入、外部API的響應、文件內容、其他函數的返回值等,「來自程式碼外部的東西」都應以「不知將會進入什麼」的心態來編寫。
# ❌ 沒有防禦的例子:直接信任輸入
def divide(a, b):
return a / b # 如果 b 是 0 呢?如果不是數字呢?
# ✅ 防禦性寫法
def divide(a, b):
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("引數必須是數字")
if b == 0:
raise ValueError("不能被0除")
return a / b
防禦性程式設計的特別重要點包括:
放棄「只要正常情況下運行即可」的想法,追求「無論發生什麼情況都不會崩潰」的程式碼就是防禦性程式設計的目標。
好的命名是最好的文件說明
變數名、函數名、類別名、文件名。命名是程式設計中最為頻繁的設計決策之一。得到適當名稱的程式碼即使沒有註解也容易理解,而不適當命名的程式碼則即使有再多的註解也難以理解。
# ❌ 不知道在做什麼
def proc(d):
r = []
for x in d:
if x['a'] > 18:
r.append(x)
return r
# ✅ 僅憑名稱即能理解意圖
def filter_adults(users):
adults = []
for user in users:
if user['age'] > 18:
adults.append(user)
return adults
以下是提出好的命名的一些基本指導原則:
避免過度縮寫。 使用 user 而非 usr,使用 button 而非 btn。閱讀者的理解優先於敲擊次數。不過,對於 i(迴圈計數器)或 e(例外)此類廣泛接受的縮寫則無妨。
函數名稱應以動詞開頭。 如 get_user()、calculate_total()、is_valid() 等,以動詞表達該函數的功能。
布林值則應以疑問句形式命名。 如 is_active、has_permission、can_edit 等,這樣就變得更易讀。
保持一致性。 同一概念要使用相同術語。若在某一處使用 user,在另一處卻使用 customer,又在別的地方用 account,這將導致混淆。
當無法想到合適的名稱時,程式碼本身設計模糊的可能性也隨之增加。無法命名的情況可能是責任或目的尚未明確的信號。
首先尋找現有的庫和工具
重新發明輪子指的就是在已經廣泛使用的解決方案存在的情況下,自己從零開始製作相同的東西。
# ❌ 重新發明輪子:自行解析日期
def parse_date(date_str):
parts = date_str.split('-')
year = int(parts[0])
month = int(parts[1])
day = int(parts[2])
# 潤年處理是...?時區是...?
...
# ✅ 使用現有的庫
from datetime import datetime
def parse_date(date_str):
return datetime.strptime(date_str, '%Y-%m-%d')
與自製相比,現有庫優勢明顯:
不過,並不是所有情況下都適合依賴庫。以下情況更適合自製:
時刻記住「首先尋找 → 若未找到則開始製作」的順序。
到此介紹了10個原則,但最重要的是:原則並不是必須嚴格遵循的規則。
原則僅僅是「在許多情況下能導致良好結果的指導方針」。根據情況,有時不遵從原則反而是合理的判斷。
例如,面對以下情境時,遵循DRY原則可能變得不合理:
原則之間也可能存在矛盾。
| 場景 | 矛盾原則 |
|---|---|
| 想要共通化程式碼,但會使結構變複雜 | DRY 與 KISS |
| 希望為未來擴展做準備,但目前不需要 | SRP 與 YAGNI |
| 向上提升性能,但程式碼可讀性下降 | 優化與 KISS |
在此情境下,需要思考「對於目前的團隊和專案,什麼是最重要的」來做出判斷。正確答案將隨著情境而變化。
了解原則的真正價值在於能夠用語言表達「為何做出這樣的判斷」。例如:「我知道DRY原則,但在這種情況下故意容許重複,原因是〇〇」如此能夠解釋,則可被認為是在靈活運用原則。
本文所介紹的10個原則整理如下:
| # | 原則 | 簡單來說 |
|---|---|---|
| 1 | DRY | 不要重複相同程式碼 |
| 2 | KISS | 保持簡單 |
| 3 | YAGNI | 不要實作目前不需要的東西 |
| 4 | 單一責任原則(SRP) | 僅持有一個責任 |
| 5 | 關心分離(SoC) | 按角色分隔程式碼 |
| 6 | 過早的優化是萬惡之源 | 先寫出可運作,優化在之後 |
| 7 | 環衛法則 | 來時比去時更乾淨 |
| 8 | 防禦性程式設計 | 不要信任外部 |
| 9 | 命名重要 | 好的命名是最佳文件說明 |
| 10 | 不要重新發明輪子 | 首先尋找現有的東西 |
不需要一次實踐所有內容。首先在日常編碼中意識到「這段程式碼,是否違反DRY原則?」或「這個函數名稱,是否可以更具體明確?」開始即可。
原則不只是一種知識,而是需要透過反覆實踐最終成為自己的東西。希望本文能成為邁向第一步的契機。
感謝您的閱讀,希望本文對您有所幫助,若覺得有用,請務必給予LGTM支持。
原文出處:https://qiita.com/ktdatascience/items/8a159a81a9c5531c985c