歡迎光臨
每天分享高質量文章

API 演進的正確方式 | Linux 中國

負責任的庫作者與其用戶的十個約定。

— A. Jesse

 

想象一下你是一個造物主,為一個生物設計一個身體。出於仁慈,你希望它能隨著時間進化:首先,因為它必須對環境的變化作出反應;其次,因為你的智慧在增長,你對這個小東西想到了更好的設計,它不應該永遠保持一個樣子。

Serpents

然而,這個生物可能有賴於其目前解剖學的特征。你不能無所顧忌地添加翅膀或改變它的身材比例。它需要一個有序的過程來適應新的身體。作為一個負責任的設計者,你如何才能溫柔地引導這種生物走向更大的進步呢?

對於負責任的庫維護者也是如此。我們向依賴我們代碼的人保證我們的承諾:我們會發佈 bug 修複和有用的新特性。如果對庫的未來有利,我們有時會刪除某些特性。我們會不斷創新,但我們不會破壞使用我們庫的人的代碼。我們怎樣才能一次實現所有這些標的呢?

添加有用的特性

你的庫不應該永遠保持不變:你應該添加一些特性,使你的庫更適合用戶。例如,如果你有一個爬行動物類,並且如果有個可以飛行的翅膀是有用的,那就去添加吧。

  1. class Reptile:
  2. @property
  3. def teeth(self):
  4. return 'sharp fangs'
  5. # 如果 wings 是有用的,那就添加它!
  6. @property
  7. def wings(self):
  8. return 'majestic wings'

但要註意,特性是有風險的。考慮 Python 標準庫中以下功能,看看它出了什麼問題。

  1. bool(datetime.time(9, 30)) == True
  2. bool(datetime.time(0, 0)) == False

這很奇怪:將任何時間物件轉換為布林值都會得到 True,但午夜時間除外。(更糟糕的是,時區感知時間的規則更加奇怪。)

我已經寫了十多年的 Python 了,但直到上周才發現這條規則。這種奇怪的行為會在用戶代碼中引起什麼樣的 bug?

比如說一個日曆應用程式,它帶有一個創建事件的函式。如果一個事件有一個結束時間,那麼函式也應該要求它有一個開始時間。

  1. def create_event(day,
  2. start_time=None,
  3. end_time=None):
  4. if end_time and not start_time:
  5. raise ValueError("Can't pass end_time without start_time")
  6. # 女巫集會從午夜一直開到凌晨 4
  7. create_event(datetime.date.today(),
  8. datetime.time(0, 0),
  9. datetime.time(4, 0))

不幸的是,對於女巫來說,從午夜開始的事件無法通過校驗。當然,一個瞭解午夜怪癖的細心程式員可以正確地編寫這個函式。

  1. def create_event(day,
  2. start_time=None,
  3. end_time=None):
  4. if end_time is not None and start_time is None:
  5. raise ValueError("Can't pass end_time without start_time")

但這種微妙之處令人擔憂。如果一個庫作者想要創建一個傷害用戶的 API,那麼像午夜的布爾轉換這樣的“特性”很有效。

Man being chased by an alligator

但是,負責任的創建者的標的是使你的庫易於正確使用。

這個功能是由 Tim Peters 在 2002 年首次編寫 datetime 模塊時造成的。即時是像 Tim 這樣的奠基 Python 的高手也會犯錯誤。這個怪異之處後來被消除了,現在所有時間的布林值都是 True。

  1. # Python 3.5 以後
  2. bool(datetime.time(9, 30)) == True
  3. bool(datetime.time(0, 0)) == True

不知道午夜怪癖的古怪之處的程式員現在可以從這種晦澀的 bug 中解脫出來,但是一想到任何依賴於古怪的舊行為的代碼現在沒有註意變化,我就會感到緊張。如果從來沒有實現這個糟糕的特性,情況會更好。這就引出了庫維護者的第一個承諾:

第一個約定:避免糟糕的特性

最痛苦的變化是你必須刪除一個特性。一般來說,避免糟糕特性的一種方法是少添加特性!沒有充分的理由,不要使用公共方法、類、功能或屬性。因此:

第二個約定:最小化特性

特性就像孩子:在充滿激情的瞬間孕育,但是它們必須要支持多年(LCTT 譯註:我懷疑作者在開車,可是我沒有證據)。不要因為你能做傻事就去做傻事。不要畫蛇添足!

Serpents with and without feathers

但是,當然,在很多情況下,用戶需要你的庫中尚未提供的東西,你如何選擇合適的功能給他們?以下另一個警示故事。

一個來自 asyncio 的警示故事

你可能知道,當你呼叫一個協程函式,它會傳回一個協程物件:

  1. async def my_coroutine():
  2. pass
  3. print(my_coroutine())
  1. object my_coroutine at 0x10bfcbac8>

你的代碼必須 “等待await” 這個物件以此來運行協程。人們很容易忘記這一點,所以 asyncio 的開發人員想要一個“除錯樣式”來捕捉這個錯誤。當協程在沒有等待的情況下被銷毀時,除錯樣式將打印一個警告,併在其創建的行上進行回溯。

當 Yury Selivanov 實現除錯樣式時,他添加了一個“協程裝飾器”的基礎特性。裝飾器是一個函式,它接收一個協程並傳回任何內容。Yury 使用它在每個協程上接入警告邏輯,但是其他人可以使用它將協程轉換為字串 “hi!”。

  1. import sys
  2. def my_wrapper(coro):
  3. return 'hi!'
  4. sys.set_coroutine_wrapper(my_wrapper)
  5. async def my_coroutine():
  6. pass
  7. print(my_coroutine())
  1. hi!

這是一個地獄般的定製。它改變了 “異步async“ 的含義。呼叫一次 set_coroutine_wrapper將在全域性永久改變所有的協程函式。正如 Nathaniel Smith 所說:“一個有問題的 API” 很容易被誤用,必須被刪除。如果 asyncio 開發人員能夠更好地按照其標的來設計該特性,他們就可以避免刪除該特性的痛苦。負責任的創建者必須牢記這一點:

第三個約定:保持特性單一

幸運的是,Yury 有良好的判斷力,他將該特性標記為臨時,所以 asyncio 用戶知道不能依賴它。Nathaniel 可以用更單一的功能替換 set_coroutine_wrapper,該特性只定製回溯深度。

  1. import sys
  2. sys.set_coroutine_origin_tracking_depth(2)
  3. async def my_coroutine():
  4. pass
  5. print(my_coroutine())
  1. object my_coroutine at 0x10bfcbac8>
  2. RuntimeWarning:'my_coroutine' was never awaited
  3. Coroutine created at (most recent call last)
  4. File "script.py", line 8, in
  5. print(my_coroutine())

這樣好多了。沒有可以更改協程的型別的其他全域性設置,因此 asyncio 用戶無需編寫防禦代碼。造物主應該像 Yury 一樣有遠見。

第四個約定:標記實驗特征“臨時”

如果你只是預感你的生物需要犄角和四叉舌,那就引入這些特性,但將它們標記為“臨時”。

Serpent with horns

你可能會發現犄角是無關緊要的,但是四叉舌是有用的。在庫的下一個版本中,你可以刪除前者並標記後者為正式的。

刪除特性

無論我們如何明智地指導我們的生物進化,總會有一天想要刪除一個正式特征。例如,你可能已經創建了一隻蜥蜴,現在你選擇刪除它的腿。也許你想把這個笨拙的家伙變成一條時尚而現代的蟒蛇。

Lizard transformed to snake

刪除特性主要有兩個原因。首先,通過用戶反饋或者你自己不斷增長的智慧,你可能會發現某個特性是個壞主意。午夜怪癖的古怪行為就是這種情況。或者,最初該特性可能已經很好地適應了你的庫環境,但現在生態環境發生了變化,也許另一個神發明瞭哺乳動物,你的生物想要擠進哺乳動物的小洞穴里,吃掉裡面美味的哺乳動物,所以它不得不失去雙腿。

A mouse

同樣,Python 標準庫會根據語言本身的變化刪除特性。考慮 asyncio 的 Lock 功能,在把 await 作為一個關鍵字添加進來之前,它一直在等待:

  1. lock = asyncio.Lock()
  2. async def critical_section():
  3. await lock
  4. try:
  5. print('holding lock')
  6. finally:
  7. lock.release()

但是現在,我們可以做“異步鎖”:

  1. lock = asyncio.Lock()
  2. async def critical_section():
  3. async with lock:
  4. print('holding lock')

新方法好多了!很短,並且在一個大函式中使用其他 try-except 塊時不容易出錯。因為“儘量找一種,最好是唯一一種明顯的解決方案”,舊語法在 Python 3.7 中被棄用,並且很快就會被禁止。

不可避免的是,生態變化會對你的代碼產生影響,因此要學會溫柔地刪除特性。在此之前,請考慮刪除它的成本或好處。負責任的維護者不會願意讓用戶更改大量代碼或邏輯。(還記得 Python 3 在重新添加會 u 字串前綴之前刪除它是多麼痛苦嗎?)如果代碼刪除是機械性的動作,就像一個簡單的搜索和替換,或者如果該特性是危險的,那麼它可能值得刪除。

是否刪除特性

Balance scales

< 如顯示不全,請左右滑動 >
反對 支持
代碼必須改變 改變是機械性的
邏輯必須改變 特性是危險的

就我們饑餓的蜥蜴而言,我們決定刪除它的腿,這樣它就可以滑進老鼠洞里吃掉它。我們該怎麼做呢?我們可以刪除 walk 方法,像下麵一樣修改代碼:

  1. class Reptile:
  2. def walk(self):
  3. print('step step step')

變成這樣:

  1. class Reptile:
  2. def slither(self):
  3. print('slide slide slide')

這不是一個好主意,這個生物習慣於走路!或者,就庫而言,你的用戶擁有依賴於現有方法的代碼。當他們升級到最新庫版本時,他們的代碼將會崩潰。

  1. # 用戶的代碼,哦,不!
  2. Reptile.walk()

因此,負責任的創建者承諾:

第五條預定:溫柔地刪除

溫柔地刪除一個特性需要幾個步驟。從用腿走路的蜥蜴開始,首先添加新方法 slither。接下來,棄用舊方法。

  1. import warnings
  2. class Reptile:
  3. def walk(self):
  4. warnings.warn(
  5. "walk is deprecated, use slither",
  6. DeprecationWarning, stacklevel=2)
  7. print('step step step')
  8. def slither(self):
  9. print('slide slide slide')

Python 的 warnings 模塊非常強大。預設情況下,它會將警告輸出到 stderr,每個代碼位置只顯示一次,但你可以禁用警告或將其轉換為異常,以及其它選項。

一旦將這個警告添加到庫中,PyCharm 和其他 IDE 就會使用刪除線呈現這個被棄用的方法。用戶馬上就知道該刪除這個方法。

Reptile().walk()

當他們使用升級後的庫運行代碼時會發生什麼?

  1. $ python3 script.py
  2. DeprecationWarning: walk is deprecated, use slither
  3. script.py:14: Reptile().walk()
  4. step step step

預設情況下,他們會在 stderr 上看到警告,但腳本會成功並打印 “step step step”。警告的回溯顯示必須修複用戶代碼的哪一行。(這就是 stacklevel 引數的作用:它顯示了用戶需要更改的呼叫,而不是庫中生成警告的行。)請註意,錯誤訊息有指導意義,它描述了庫用戶遷移到新版本必須做的事情。

你的用戶可能會希望測試他們的代碼,並證明他們沒有呼叫棄用的庫方法。僅警告不會使單元測試失敗,但異常會失敗。Python 有一個命令列選項,可以將棄用警告轉換為異常。

  1. > python3 -Werror::DeprecationWarning script.py
  2. Traceback (most recent call last):
  3. File "script.py", line 14, in <module>
  4. Reptile().walk()
  5. File "script.py", line 8, in walk
  6. DeprecationWarning, stacklevel=2)
  7. DeprecationWarning: walk is deprecated, use slither

現在,“step step step” 沒有輸出出來,因為腳本以一個錯誤終止。

因此,一旦你發佈了庫的一個版本,該版本會警告已啟用的 walk 方法,你就可以在下一個版本中安全地刪除它。對吧?

考慮一下你的庫用戶在他們專案的 requirements 中可能有什麼。

  1. # 用戶的 requirements.txt 顯示 reptile 包的依賴關係
  2. reptile

下次他們部署代碼時,他們將安裝最新版本的庫。如果他們尚未處理所有的棄用,那麼他們的代碼將會崩潰,因為代碼仍然依賴 walk。你需要溫柔一點,你必須向用戶做出三個承諾:維護更改日誌,選擇版本化方案和編寫升級指南。

第六個約定:維護變更日誌

你的庫必須有更改日誌,其主要目的是宣佈用戶所依賴的功能何時被棄用或刪除。

版本 1.1 中的更改

新特性

◈ 新功能 Reptile.slither()

棄用

◈ Reptile.walk() 已棄用,將在 2.0 版本中刪除,請使用 slither()

負責任的創建者會使用版本號來表示庫發生了怎樣的變化,以便用戶能夠對升級做出明智的決定。“版本化方案”是一種用於交流變化速度的語言。

第七個約定:選擇一個版本化方案

有兩種廣泛使用的方案,語意版本控制和基於時間的版本控制。我推薦任何庫都進行語意版本控制。Python 的風格在 PEP 440 中定義,像 pip 這樣的工具可以理解語意版本號。

如果你為庫選擇語意版本控制,你可以使用版本號溫柔地刪除腿,例如:

1.0: 第一個“穩定”版,帶有 walk() 1.1: 添加 slither(),廢棄 walk()2.0: 刪除 walk()

你的用戶依賴於你的庫的版本應該有一個範圍,例如:

  1. # 用戶的 requirements.txt
  2. reptile>=1,<2

這允許他們在主要版本中自動升級,接收錯誤修正並可能引發一些棄用警告,但不會升級到個主要版本並冒著更改破壞其代碼的風險。

如果你遵循基於時間的版本控制,則你的版本可能會編號:

2017.06.0: 2017 年 6 月的版本 2018.11.0: 添加 slither(),廢棄 walk()2019.04.0: 刪除 walk()

用戶可以這樣依賴於你的庫:

  1. # 用戶的 requirements.txt,基於時間控制的版本
  2. reptile==2018.11.*

這非常棒,但你的用戶如何知道你的版本方案,以及如何測試代碼來進行棄用呢?你必須告訴他們如何升級。

第八個約定:寫一個升級指南

下麵是一個負責任的庫創建者如何指導用戶:

升級到 2.0

從棄用的 API 遷移

請參閱更改日誌以瞭解已棄用的特性。

啟用棄用警告

升級到 1.1 並使用以下代碼測試代碼:

python -Werror::DeprecationWarning

現在可以安全地升級了。

你必須通過向用戶顯示命令列選項來教會用戶如何處理棄用警告。並非所有 Python 程式員都知道這一點 —— 我自己就每次都得查找這個語法。註意,你必須發佈一個版本,它輸出來自每個棄用的 API 的警告,以便用戶可以在再次升級之前使用該版本進行測試。在本例中,1.1 版本是小版本。它允許你的用戶逐步重寫代碼,分別修複每個棄用警告,直到他們完全遷移到最新的 API。他們可以彼此獨立地測試代碼和庫的更改,並隔離 bug 的原因。

如果你選擇語意版本控制,則此過渡期將持續到下一個主要版本,從 1.x 到 2.0,或從 2.x 到 3.0 以此類推。刪除生物腿部的溫柔方法是至少給它一個版本來調整其生活方式。不要一次性把腿刪掉!

A skink

版本號、棄用警告、更改日誌和升級指南可以協同工作,在不違背與用戶約定的情況下溫柔地改進你的庫。Twisted 專案的兼容性政策 解釋的很漂亮:

“先行者總是自由的”

運行的應用程式在沒有任何警告的情況下都可以升級為 Twisted 的一個次要版本。

換句話說,任何運行其測試而不觸發 Twisted 警告的應用程式應該能夠將其 Twisted 版本升級至少一次,除了可能產生新警告之外沒有任何不良影響。

現在,我們的造物主已經獲得了智慧和力量,可以通過添加方法來添加特性,並溫柔地刪除它們。我們還可以通過添加引數來添加特性,但這帶來了新的難度。你準備好了嗎?

添加引數

想象一下,你只是給了你的蛇形生物一對翅膀。現在你必須允許它選擇是滑行還是飛行。目前它的 move 功能只接受一個引數。

  1. # 你的庫代碼
  2. def move(direction):
  3. print(f'slither {direction}')
  4. # 用戶的應用
  5. move('north')

你想要添加一個 mode 引數,但如果用戶升級庫,這會破壞他們的代碼,因為他們只傳遞了一個引數。

  1. # 你的庫代碼
  2. def move(direction, mode):
  3. assert mode in ('slither', 'fly')
  4. print(f'{mode} {direction}')
  5. # 一個用戶的代碼,出現錯誤!
  6. move('north')

一個真正聰明的創建者者會承諾不會以這種方式破壞用戶的代碼。

第九條約定:兼容地添加引數

要保持這個約定,請使用保留原始行為的預設值添加每個新引數。

  1. # 你的庫代碼
  2. def move(direction, mode='slither'):
  3. assert mode in ('slither', 'fly')
  4. print(f'{mode} {direction}')
  5. # 用戶的應用
  6. move('north')

隨著時間推移,引數是函式演化的自然歷史。它們首先列出最老的引數,每個都有預設值。庫用戶可以傳遞關鍵字引數以選擇特定的新行為,並接受所有其他行為的預設值。

  1. # 你的庫代碼
  2. def move(direction,
  3. mode='slither',
  4. turbo=False,
  5. extra_sinuous=False,
  6. hail_lyft=False):
  7. # ...
  8. # 用戶應用
  9. move('north', extra_sinuous=True)

但是有一個危險,用戶可能會編寫如下代碼:

  1. # 用戶應用,簡寫
  2. move('north', 'slither', False, True)

如果在你在庫的下一個主要版本中去掉其中一個引數,例如 turbo,會發生什麼?

  1. # 你的庫代碼,下一個主要版本中 "turbo" 被刪除
  2. def move(direction,
  3. mode='slither',
  4. extra_sinuous=False,
  5. hail_lyft=False):
  6. # ...
  7. # 用戶應用,簡寫
  8. move('north', 'slither', False, True)

用戶的代碼仍然能編譯,這是一件壞事。代碼停止了曲折的移動並開始招呼 Lyft,這不是它的本意。我相信你可以預測我接下來要說的內容:刪除引數需要幾個步驟。當然,首先棄用 trubo引數。我喜歡這種技術,它可以檢測任何用戶的代碼是否依賴於這個引數。

  1. # 你的庫代碼
  2. _turbo_default = object()
  3. def move(direction,
  4. mode='slither',
  5. turbo=_turbo_default,
  6. extra_sinuous=False,
  7. hail_lyft=False):
  8. if turbo is not _turbo_default:
  9. warnings.warn(
  10. "'turbo' is deprecated",
  11. DeprecationWarning,
  12. stacklevel=2)
  13. else:
  14. # The old default.
  15. turbo = False

但是你的用戶可能不會註意到警告。警告聲音不是很大:它們可以在日誌檔案中被抑制或丟失。用戶可能會漫不經心地升級到庫的下一個主要版本——那個刪除 turbo 的版本。他們的代碼運行時將沒有錯誤、默默做錯誤的事情!正如 Python 之禪所說:“錯誤絕不應該被默默 pass”。實際上,爬行動物的聽力很差,所有當它們犯錯誤時,你必須非常大聲地糾正它們。

Woman riding an alligator

保護用戶的最佳方法是使用 Python 3 的星型語法,它要求呼叫者傳遞關鍵字引數。

  1. # 你的庫代碼
  2. # 所有 “*” 後的引數必須以關鍵字方式傳輸。
  3. def move(direction,
  4. *,
  5. mode='slither',
  6. turbo=False,
  7. extra_sinuous=False,
  8. hail_lyft=False):
  9. # ...
  10. # 用戶代碼,簡寫
  11. # 錯誤!不能使用位置引數,關鍵字引數是必須的
  12. move('north', 'slither', False, True)

有了這個星,以下是唯一允許的語法:

  1. # 用戶代碼
  2. move('north', extra_sinuous=True)

現在,當你刪除 turbo 時,你可以確定任何依賴於它的用戶代碼都會明顯地提示失敗。如果你的庫也支持 Python2,這沒有什麼大不了。你可以模擬星型語法(歸功於 Brett Slatkin):

  1. # 你的庫代碼,兼容 Python 2
  2. def move(direction, **kwargs):
  3. mode = kwargs.pop('mode', 'slither')
  4. turbo = kwargs.pop('turbo', False)
  5. sinuous = kwargs.pop('extra_sinuous', False)
  6. lyft = kwargs.pop('hail_lyft', False)
  7. if kwargs:
  8. raise TypeError('Unexpected kwargs: %r'
  9. % kwargs)
  10. # ...

要求關鍵字引數是一個明智的選擇,但它需要遠見。如果允許按位置傳遞引數,則不能僅在以後的版本中將其轉換為僅關鍵字。所以,現在加上星號。你可以在 asyncio API 中觀察到,它在建構式、方法和函式中普遍使用星號。儘管到目前為止,Lock 只接受一個可選引數,但 asyncio 開發人員立即添加了星號。這是幸運的。

  1. # In asyncio.
  2. class Lock:
  3. def __init__(self, *, loop=None):
  4. # ...

現在,我們已經獲得了改變方法和引數的智慧,同時保持與用戶的約定。現在是時候嘗試最具挑戰性的進化了:在不改變方法或引數的情況下改變行為。

改變行為

假設你創造的生物是一條響尾蛇,你想教它一種新行為。

Rattlesnake

橫向移動!這個生物的身體看起來是一樣的,但它的行為會發生變化。我們如何為這一進化步驟做好準備?

Image by HCA [CC BY-SA 4.0], via Wikimedia Commons, 由 Opensource.com 修改

當行為在沒有新函式或新引數的情況下發生更改時,負責任的創建者可以從 Python 標準庫中學習。很久以前,os 模塊引入了 stat 函式來獲取檔案統計信息,比如創建時間。起初,這個時間總是整數。

  1. >>> os.stat('file.txt').st_ctime
  2. 1540817862

有一天,核心開發人員決定在 os.stat 中使用浮點數來提供亞秒級精度。但他們擔心現有的用戶代碼還沒有做好準備更改。於是他們在 Python 2.3 中創建了一個設置 stat_float_times,預設情況下是 False 。用戶可以將其設置為 True 來選擇浮點時間戳。

  1. >>> # Python 2.3.
  2. >>> os.stat_float_times(True)
  3. >>> os.stat('file.txt').st_ctime
  4. 1540817862.598021

從 Python 2.5 開始,浮點時間成為預設值,因此 2.5 及之後版本編寫的任何新代碼都可以忽略該設置並期望得到浮點數。當然,你可以將其設置為 False 以保持舊行為,或將其設置為 True 以確保所有 Python 版本都得到浮點數,併為刪除 stat_float_times 的那一天準備代碼。

多年過去了,在 Python 3.1 中,該設置已被棄用,以便為人們為遙遠的未來做好準備,最後,經過數十年的旅程,這個設置被刪除。浮點時間現在是唯一的選擇。這是一個漫長的過程,但負責任的神靈是有耐心的,因為我們知道這個漸進的過程很有可能於意外的行為變化拯救用戶。

第十個約定:逐漸改變行為

以下是步驟:

◈ 添加一個標誌來選擇新行為,預設為 False,如果為 False 則發出警告
◈ 將預設值更改為 True,表示完全棄用標記
◈ 刪除該標誌

如果你遵循語意版本控制,版本可能如下:

< 如顯示不全,請左右滑動 >
庫版本 庫 API 用戶代碼
1.0 沒有標誌 預期的舊行為
1.1 添加標誌,預設為 False,如果是 False,則警告 設置標誌為 True,處理新行為
2.0 改變預設為 True,完全棄用標誌 處理新行為
3.0 移除標誌 處理新行為

你需要個主要版本來完成該操作。如果你直接從“添加標誌,預設為 False,如果是 False 則發出警告”變到“刪除標誌”,而沒有中間版本,那麼用戶的代碼將無法升級。為 1.1 正確編寫的用戶代碼必須能夠升級到下一個版本,除了新警告之外,沒有任何不良影響,但如果在下一個版本中刪除了該標誌,那麼該代碼將崩潰。一個負責任的神明從不違反扭曲的政策:“先行者總是自由的”。

負責任的創建者

Demeter

我們的 10 個約定大致可以分為三類:

謹慎發展

1. 避免不良功能
2. 最小化特性
3. 保持功能單一
4. 標記實驗特征“臨時”
5. 溫柔刪除功能

嚴格記錄歷史

1. 維護更改日誌
2. 選擇版本方案
3. 編寫升級指南

緩慢而明顯地改變

1. 兼容添加引數
2. 逐漸改變行為

如果你對你所創造的物種保持這些約定,你將成為一個負責任的造物主。你的生物的身體可以隨著時間的推移而進化,一直在改善和適應環境的變化,而不是在生物沒有準備好就突然改變。如果你維護一個庫,請向用戶保留這些承諾,這樣你就可以在不破壞依賴該庫的代碼的情況下對庫進行更新。

已同步到看一看
赞(0)

分享創造快樂