程序的“通用性”和“過度設計”困境
在軟件工程的實際操作中,我常常遇到這樣一種現象:本可以用簡單代碼解決的問題,卻因為設計者過分關注“通用性”、“可維護性”和“可擴展性”而變得不必要地復雜,難以理解。
他們的思維方式是這樣的:“這段代碼未來可能會在更多場景中使用,所以我現在應該考慮它的可擴展性。”結果,他們在代碼中加入了各種“框架結構”,以便在未來有新需求時,無需修改就能在新的地方使用。
“通用性”的價值與誤區
作者并不否認“通用性”的價值,我的一些程序也具有很高的通用性。然而,很多人所謂的“通用性”實際上適得其反,這種現象通常被稱為 “過度設計” 。關于過度設計,有一個有趣的故事:
傳說在上世紀60年代美國和俄羅斯的“太空競賽”期間,NASA 遇到了一個嚴重的技術問題:宇航員需要一支能在外太空真空環境中書寫的筆。最終,NASA 花費了150萬美元開發了這樣一支筆。不幸的是,這種筆在市場上并不暢銷。
俄羅斯人也面臨同樣的問題,他們則用了鉛筆。
雖然這個故事是虛構的,但它具備了伊索寓言的力量。現在讓我們看看軟件行業,可能會發現:
代碼需要“重用”的場合比您想象的要少得多。
許多人在寫程序時,連“當前異常”都處理不好,卻關注“未來的需求”。他們總是想象別人會重用這段代碼。然而,實際上,由于設計過于復雜,理解這些設計所需的心智努力已經超過了從頭開始的成本。因此,大多數人根本不會使用他們的代碼,而是重新寫一個。有些人最終會發現自己甚至看不下去之前寫的代碼,更愿意刪除它并重新開始,而不是談論重用。
我們用一個簡單的例子來說明這一點。假設我們需要寫一個簡單的程序來計算兩個整數的和:
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 輸出 8
這個函數非常直觀,可以很好地滿足當前需求。然而,一些程序員可能會考慮未來可能需要更多的計算能力,因此設計了一個復雜的框架:
class Operation:
def execute(self, a, b):
raise NotImplementedError
class AddOperation(Operation):
def execute(self, a, b):
return a + b
class OperationFactory:
@staticmethod
def get_operation(op_type):
if op_type == 'add':
return AddOperation()
# 可以添加更多操作類型。
raise ValueError("不支持的操作類型")
operation = OperationFactory.get_operation('add')
result = operation.execute(3, 5)
print(result) # 輸出 8
雖然這個設計提供了可擴展性,但在當前只需計算兩個整數和的場景中,這個設計無疑增加了代碼的復雜性和理解成本。
實際修改代碼所需的工作量比你想象的少
還有一種情況是,這些設計為“共享”而寫的代碼在很多地方并沒有被使用,因此即使你完全手動修改它們,也不會花費很多時間。現在,隨著 IDE 技術的發展和各種高級重構工具的出現,批量代碼修改不再特別麻煩。過去需要在邏輯層面設計可維護性,現在只需在 IDE 中點擊幾下就能輕松完成。因此,在考慮設計框架之前,你還應該考慮這些因素。
例如,在上面提到的復雜設計中,如果我們需要修改加法操作,我們需要修改多個類和文件。在這種情況下,我們可能會發現簡單的函數實現更容易維護和修改。
“考慮”通用性并不意味著你已經準確“掌握”了通用性
許多人考慮通用性,但他們并不準確地看到未來可能需要修改的部分,因此他們的設計往往錯失重點。當新需求出現時,發現最初認為可能變化的部分并沒有變化,而那些認為不變的部分卻發生了變化。
能夠準確預測未來需求并從代碼中抽象出真正通用的框架是非常困難的任務。它不僅需要編程技能,還需要強大的觀察現實世界事物的能力。許多人設計的框架只是復制他人的經驗,無法適應實際需求。Java 世界中的許多設計模式就是由這些半吊子的人創造的。
例如,假設我們需要添加一個新的操作,如減法。如果我們沒有準確掌握哪些部分需要通用設計,我們可能會發現現有框架不適用于新需求:
class SubtractOperation(Operation):
def execute(self, a, b):
return a - b
operation = OperationFactory.get_operation('subtract')
result = operation.execute(10, 3)
print(result) # 輸出 7
在這里,我們需要修改 OperationFactory 類以支持減法操作。這表明,雖然我們考慮了通用性,但我們沒有準確掌握未來的需求,導致框架的靈活性有限。
初始設計的復雜性
如果在初始設計中過早地考慮未來需求,可能會導致不必要的復雜性和問題。因此,這種對未來變化的考慮阻礙了進展。原本如果我們專注于解決當前問題,可以取得很好的結果。然而,由于“通用性”帶來的復雜性,設計者每次都要多費一些心思,無法創建優雅的程序。
例如,在上面提到的復雜設計中,如果我們只需要一個簡單的加法操作,復雜的框架反而使初始設計變得臃腫且難以理解:
class Operation:
def execute(self, a, b):
raise NotImplementedError
class AddOperation(Operation):
def execute(self, a, b):
return a + b
class OperationFactory:
@staticmethod
def get_operation(op_type):
if op_type == 'add':
return AddOperation()
raise ValueError("不支持的操作類型")
# 初始設計的復雜性。
operation = OperationFactory.get_operation('add')
result = operation.execute(3, 5)
print(result) # 輸出 8
相比之下,只需執行一個簡單的函數即可。
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 輸出 8
理解和維護框架代碼的開銷
如果你設計了一個框架式的代碼,每個程序員都需要理解這個框架的構建,才能在這個框架下編寫代碼,這帶來了學習成本。一旦發現這個框架有設計問題,依賴它的代碼可能需要修改,這帶來了修改成本。因此,在設計中加入“通用性”的初衷是為了節省未來的修改成本,但可能會增加當前的開發和維護成本。
例如,在復雜的框架設計中,添加新操作類型需要理解多個類及其關系:
class MultiplyOperation(Operation):
def execute(self, a, b):
return a * b
# 理解和維護的成本。
operation = OperationFactory.get_operation('multiply')
result = operation.execute(3, 5)
print(result) # 輸出 15
而在簡單的函數實現中,添加新功能相對簡單:
def multiply(a, b):
return a * b
result = multiply(3, 5)
print(result) # 輸出 15
結論
在軟件工程中,設計的“通用性”確實是一個重要的考慮因素,但我們必須謹慎。過度設計和過度工程不僅不會提高代碼的可維護性,反而會增加開發和維護成本。在實際項目中,我們應該根據當前需求采用最簡單直接的解決方案,而不是為了未來的可能性增加當前的復雜性。
總的來說,簡潔直觀的代碼往往比復雜的框架更能滿足實際需求。我們應在設計中保持平衡,避免過度工程,專注于解決當前問題,同時為未來擴展留有余地。