類別設計
減少易變性(mutability)
- 除非有好理由,不然類別應該要 immutable:好處是單純、thread-safe、可重複使用;缺點是每個不同的值都是獨立的物件,也就是為了改一個值,就要放棄現在的物件取得新的物件。
以 python 為例,int 是 immutable 的,所以使用 '1'
的值,它只要產生一次對應的物件,後續只要使用到 '1'
的值,就使用同一個物件即可。
>>> id(1)
10914496
>>> x = 1
>>> id(x)
10914496
- 如果類別是 mutable,要使類別的狀態空間(state space)小且清楚定義:寫清楚在甚麼狀態下可以呼叫哪些函式。
子類別要合理
- 如果 class A 繼承自 class B,那是否能回答 class A is a class B(is-a relationship),如果不行那就用複合(composition,has-a relationship)
- 不要為了實作方便而讓 public 類別繼承另一個 public 類別
Stack 不應該繼承自 Vector,因為 Stack 不會有 Vector 有的如
insert、erase
的函式,另外使用者也可以使用 Vector 的函式來對 Stack 操作,所以 Stack is not
a Vector。而是使用 Vector 來做儲存容器,只提供操作 Stack
有關的函式,如:push、pop,也就是 Stack has a Vector。
為繼承設計與撰寫文件,否則禁止繼承
- 繼承會違反封裝原則。繼承類別對被繼承的類別的實作細節敏感,一旦被繼承的類別有改動,就會影響繼承類別
- 讓不是設計用來繼承的類別完整(不要留有需要被實作的地方)
函式設計
不要讓使用者做任何模組可以作到的事情
- 減少樣板程式碼。樣板程式碼通常用剪貼就可以達成,又醜又煩而且容易出錯,應該整理起來
在 MLGame
中需要遊戲開發者設置遊戲啟動方式,原本需要呼叫大量函式,而且每個遊戲幾乎一樣,只是參數不同,後來就改成直接提供一個
dict 存放需要的參數,由 MLGame 讀取並幫助配置。改善前樣子:
def ml_mode(config: GameConfig):
try:
difficulty, level = _get_difficulty_and_level(config.game_params)
except GameParameterError as e:
print("Error: " + str(e) + "\n" + usage())
return
from mlgame.process import ProcessManager
process_manager = ProcessManager()
process_manager.set_game_process(_start_game_process, \
args = (config.fps, difficulty, level, \
config.record_progress, config.one_shot_mode))
process_manager.add_ml_process(config.input_modules[0], "ml")
process_manager.start()
改善後,使用者只需設置像 config 的內容,除了簡單明瞭,也減少出錯的機會:
GAME_PARAMS = {
"()": {
"prog": "arkanoid",
"game_usage": "%(prog)s <difficulty> <level>"
},
"difficulty": {
"choices": ("EASY", "NORMAL"),
"metavar": "difficulty",
"help": "Specify the game style. Choices: %(choices)s"
},
"level": {
"type": int,
"help": "Specify the level map"
}
}
from .game.arkanoid import Arkanoid
GAME_SETUP = {
"game": Arkanoid,
"ml_clients": [
{ "name": "ml" }
]
}
不要違反最小驚訝原則
- 使用者不能被 API 行為嚇到。例如:某函式順手做了多餘的事情。
- 寧可多一點額外的實作功夫,就算降低效能也值得
Fail Fast
- 當出錯時,越早報錯越好。
- 能在 compile time 報錯最好,如果是在 runtime,第一個錯誤的函式調用發生時報錯最好
為所有可以以字串形式取得的資料,提供程式化的存取方式
- 否則使用者只能解析回傳的字串,也會讓這個字串形式變成 API 的一部分,讓你不能更動回傳的資料格式
小心地 overloading
- 避免曖昧的 overloading:傳相同的物件(透過 casting)傳給不同的 overloading 函式要有相同的效果
- 即使可以用 overloading,也不代表你應該要用,最好是給予不同的名稱
函式引數與回傳值使用合適的型別
- 引數傾向使用介面(interface)而非類別:會更有彈性,效能也較好
- 引數使用最可能的型別(縮小可能性),使程式能夠在編譯時期報錯
- 如果有更好的型別,就不要用字串型別:Boolean vs "yes/no"
- 不要使用浮點數作為貨幣數值:浮點數無法精確表示 0.1
- 使用雙精度浮點數(64bit double)比較精確
使用一致的引數排序
- 尤其是當引數的型別一樣的時候:如果放的排序不同,出錯也很難找到
- 同樣類型的引數,在函式之間應該要有同樣的位置
例如 C 語言中的
char* strcpy(char* dest, const char* src) 跟
void bcopy(const void *src, void *dest, size_t n),
兩者 dest 與
src 的順序相反,如果照著
strcpy 的引數順序放到
bcopy,變成
bcopy(dest, src),結果就是
dest 沒有改變,src
變成 dest(可能是一堆垃圾值)。不過後來
bcopy 就被廢棄了,被 void *memcpy(void *dest, const void *src, size_t n)
取代。
避免過多的引數
- 理想是兩到三個引數,更多的引數就必須靠文件才能使用了
- 如果一個函式引數過多,則可以拆解函式或是建立幫助類別來包含這些引數,因為大部分的引數應該會有預設值,最後再傳入這個幫助類別的物件到函式中
避免回傳需要特別處理的值
- 像一個回傳陣列的函式,就不要回傳 null,而是空陣列
演講的整理就到這邊結束,感受到寫出好的程式碼真的是需要花心力的,如果有問題的話歡迎討論。
沒有留言:
張貼留言