API 演進的正確方式

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

Browser of things

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

Serpents
Serpents

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

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

添加有用的特性

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

class Reptile:
    @property
    def teeth(self):
        return 'sharp fangs'

    # 若是 wings 是有用的,那就添加它!
    @property
    def wings(self):
        return 'majestic wings'
複製代碼

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

bool(datetime.time(9, 30)) == True
bool(datetime.time(0, 0)) == False
複製代碼

這很奇怪:將任什麼時候間對象轉換爲布爾值都會獲得 True,但午夜時間除外。(更糟糕的是,時區感知時間的規則更加奇怪。)api

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

好比說一個日曆應用程序,它帶有一個建立事件的函數。若是一個事件有一個結束時間,那麼函數也應該要求它有一個開始時間。ruby

def create_event(day,
                 start_time=None,
                 end_time=None):
    if end_time and not start_time:
        raise ValueError("Can't pass end_time without start_time")

# 女巫集會從午夜一直開到凌晨 4 點
create_event(datetime.date.today(),
             datetime.time(0, 0),
             datetime.time(4, 0))
複製代碼

不幸的是,對於女巫來講,從午夜開始的事件沒法經過校驗。固然,一個瞭解午夜怪癖的細心程序員能夠正確地編寫這個函數。bash

def create_event(day,
                 start_time=None,
                 end_time=None):
    if end_time is not None and start_time is None:
        raise ValueError("Can't pass end_time without start_time")
複製代碼

但這種微妙之處使人擔心。若是一個庫做者想要建立一個傷害用戶的 API,那麼像午夜的布爾轉換這樣的「特性」頗有效。

Man being chased by an alligator
Man being chased by an alligator

可是,負責任的建立者的目標是使你的庫易於正確使用。

這個功能是由 Tim Peters 在 2002 年首次編寫 datetime 模塊時形成的。即時是像 Tim 這樣的奠定 Python 的高手也會犯錯誤。這個怪異之處後來被消除了,如今全部時間的布爾值都是 True。

# Python 3.5 之後

bool(datetime.time(9, 30)) == True
bool(datetime.time(0, 0)) == True
複製代碼

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

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

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

第二個約定:最小化特性

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

Serpents with and without feathers
Serpents with and without feathers

可是,固然,在不少狀況下,用戶須要你的庫中還沒有提供的東西,你如何選擇合適的功能給他們?如下另外一個警示故事。

一個來自 asyncio 的警示故事

你可能知道,當你調用一個協程函數,它會返回一個協程對象:

async def my_coroutine():
    pass

print(my_coroutine())
複製代碼
<coroutine object my_coroutine at 0x10bfcbac8>
複製代碼

你的代碼必須 「等待await」 這個對象以此來運行協程。人們很容易忘記這一點,因此 asyncio 的開發人員想要一個「調試模式」來捕捉這個錯誤。當協程在沒有等待的狀況下被銷燬時,調試模式將打印一個警告,並在其建立的行上進行回溯。

當 Yury Selivanov 實現調試模式時,他添加了一個「協程裝飾器」的基礎特性。裝飾器是一個函數,它接收一個協程並返回任何內容。Yury 使用它在每一個協程上接入警告邏輯,可是其餘人可使用它將協程轉換爲字符串 「hi!」。

import sys

def my_wrapper(coro):
    return 'hi!'

sys.set_coroutine_wrapper(my_wrapper)

async def my_coroutine():
    pass

print(my_coroutine())
複製代碼
hi!
複製代碼

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

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

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

import sys

sys.set_coroutine_origin_tracking_depth(2)

async def my_coroutine():
    pass

print(my_coroutine())

複製代碼
<coroutine object my_coroutine at 0x10bfcbac8>

RuntimeWarning:'my_coroutine' was never awaited

Coroutine created at (most recent call last)
    File "script.py", line 8, in <module>
        print(my_coroutine())
複製代碼

這樣好多了。沒有能夠更改協程的類型的其餘全局設置,所以 asyncio 用戶無需編寫防護代碼。造物主應該像 Yury 同樣有遠見。

第四個約定:標記實驗特徵「臨時」

若是你只是預感你的生物須要犄角和四叉舌,那就引入這些特性,但將它們標記爲「臨時」。

Serpent with horns
Serpent with horns

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

刪除特性

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

Lizard transformed to snake
Lizard transformed to snake

刪除特性主要有兩個緣由。首先,經過用戶反饋或者你本身不斷增加的智慧,你可能會發現某個特性是個壞主意。午夜怪癖的古怪行爲就是這種狀況。或者,最初該特性可能已經很好地適應了你的庫環境,但如今生態環境發生了變化,也許另外一個神發明了哺乳動物,你的生物想要擠進哺乳動物的小洞穴裏,吃掉裏面美味的哺乳動物,因此它不得不失去雙腿。

A mouse
A mouse

一樣,Python 標準庫會根據語言自己的變化刪除特性。考慮 asyncio 的 Lock 功能,在把 await 做爲一個關鍵字添加進來以前,它一直在等待:

lock = asyncio.Lock()

async def critical_section():
    await lock
    try:
        print('holding lock')
    finally:
        lock.release()
複製代碼

可是如今,咱們能夠作「異步鎖」:

lock = asyncio.Lock()

async def critical_section():
    async with lock:
        print('holding lock')
複製代碼

新方法好多了!很短,而且在一個大函數中使用其餘 try-except 塊時不容易出錯。由於「儘可能找一種,最好是惟一一種明顯的解決方案」,舊語法在 Python 3.7 中被棄用,而且很快就會被禁止。

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

是否刪除特性

Balance scales
Balance scales

反對 支持
代碼必須改變 改變是機械性的
邏輯必須改變 特性是危險的

就咱們飢餓的蜥蜴而言,咱們決定刪除它的腿,這樣它就能夠滑進老鼠洞裏吃掉它。咱們該怎麼作呢?咱們能夠刪除 walk 方法,像下面同樣修改代碼:

class Reptile:
    def walk(self):
        print('step step step')
複製代碼

變成這樣:

class Reptile:
    def slither(self):
        print('slide slide slide')
複製代碼

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

# 用戶的代碼,哦,不!
Reptile.walk()
複製代碼

所以,負責任的建立者承諾:

第五條預約:溫柔地刪除

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

import warnings

class Reptile:
    def walk(self):
        warnings.warn(
                "walk is deprecated, use slither",
                 DeprecationWarning, stacklevel=2)
        print('step step step')

    def slither(self):
        print('slide slide slide')
複製代碼

Python 的 warnings 模塊很是強大。默認狀況下,它會將警告輸出到 stderr,每一個代碼位置只顯示一次,但你能夠禁用警告或將其轉換爲異常,以及其它選項。

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

Reptile().walk()

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

$ python3 script.py

DeprecationWarning: walk is deprecated, use slither
    script.py:14: Reptile().walk()

step step step
複製代碼

默認狀況下,他們會在 stderr 上看到警告,但腳本會成功並打印 「step step step」。警告的回溯顯示必須修復用戶代碼的哪一行。(這就是 stacklevel 參數的做用:它顯示了用戶須要更改的調用,而不是庫中生成警告的行。)請注意,錯誤消息有指導意義,它描述了庫用戶遷移到新版本必須作的事情。

你的用戶可能會但願測試他們的代碼,並證實他們沒有調用棄用的庫方法。僅警告不會使單元測試失敗,但異常會失敗。Python 有一個命令行選項,能夠將棄用警告轉換爲異常。

> python3 -Werror::DeprecationWarning script.py

Traceback (most recent call last):
    File "script.py", line 14, in <module>
        Reptile().walk()
    File "script.py", line 8, in walk
        DeprecationWarning, stacklevel=2)
DeprecationWarning: walk is deprecated, use slither
複製代碼

如今,「step step step」 沒有輸出出來,由於腳本以一個錯誤終止。

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

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

# 用戶的 requirements.txt 顯示 reptile 包的依賴關係
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()

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

# 用戶的 requirements.txt
reptile>=1,<2
複製代碼

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

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

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

用戶能夠這樣依賴於你的庫:

# 用戶的 requirements.txt,基於時間控制的版本
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
A skink

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

「先行者老是自由的」

運行的應用程序在沒有任何警告的狀況下均可以升級爲 Twisted 的一個次要版本。

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

如今,咱們的造物主已經得到了智慧和力量,能夠經過添加方法來添加特性,並溫柔地刪除它們。咱們還能夠經過添加參數來添加特性,但這帶來了新的難度。你準備好了嗎?

添加參數

想象一下,你只是給了你的蛇形生物一對翅膀。如今你必須容許它選擇是滑行仍是飛行。目前它的 move 功能只接受一個參數。

# 你的庫代碼
def move(direction):
    print(f'slither {direction}')

# 用戶的應用
move('north')
複製代碼

你想要添加一個 mode 參數,但若是用戶升級庫,這會破壞他們的代碼,由於他們只傳遞了一個參數。

# 你的庫代碼
def move(direction, mode):
    assert mode in ('slither', 'fly')
    print(f'{mode} {direction}')

# 一個用戶的代碼,出現錯誤!
move('north')
複製代碼

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

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

要保持這個約定,請使用保留原始行爲的默認值添加每一個新參數。

# 你的庫代碼
def move(direction, mode='slither'):
    assert mode in ('slither', 'fly')
    print(f'{mode} {direction}')

# 用戶的應用
move('north')
複製代碼

隨着時間推移,參數是函數演化的天然歷史。它們首先列出最老的參數,每一個都有默認值。庫用戶能夠傳遞關鍵字參數以選擇特定的新行爲,並接受全部其餘行爲的默認值。

# 你的庫代碼
def move(direction,
         mode='slither',
         turbo=False,
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用戶應用
move('north', extra_sinuous=True)
複製代碼

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

# 用戶應用,簡寫
move('north', 'slither', False, True)
複製代碼

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

# 你的庫代碼,下一個主要版本中 "turbo" 被刪除
def move(direction,
         mode='slither',
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用戶應用,簡寫
move('north', 'slither', False, True)
複製代碼

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

# 你的庫代碼
_turbo_default = object()

def move(direction,
         mode='slither',
         turbo=_turbo_default,
         extra_sinuous=False,
         hail_lyft=False):
    if turbo is not _turbo_default:
        warnings.warn(
                "'turbo' is deprecated",
                DeprecationWarning,
                stacklevel=2)
    else:
        # The old default.
        turbo = False
複製代碼

可是你的用戶可能不會注意到警告。警告聲音不是很大:它們能夠在日誌文件中被抑制或丟失。用戶可能會漫不經心地升級到庫的下一個主要版本——那個刪除 turbo 的版本。他們的代碼運行時將沒有錯誤、默默作錯誤的事情!正如 Python 之禪所說:「錯誤毫不應該被默默 pass」。實際上,爬行動物的聽力不好,全部當它們犯錯誤時,你必須很是大聲地糾正它們。

Woman riding an alligator
Woman riding an alligator

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

# 你的庫代碼
# 全部 「*」 後的參數必須以關鍵字方式傳輸。
def move(direction,
         *,
         mode='slither',
         turbo=False,
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用戶代碼,簡寫
# 錯誤!不能使用位置參數,關鍵字參數是必須的
move('north', 'slither', False, True)
複製代碼

有了這個星,如下是惟一容許的語法:

# 用戶代碼
move('north', extra_sinuous=True)
複製代碼

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

# 你的庫代碼,兼容 Python 2
def move(direction, **kwargs):
    mode = kwargs.pop('mode', 'slither')
    turbo = kwargs.pop('turbo', False)
    sinuous = kwargs.pop('extra_sinuous', False)
    lyft = kwargs.pop('hail_lyft', False)

    if kwargs:
        raise TypeError('Unexpected kwargs: %r' 
                        % kwargs)

# ...
複製代碼

要求關鍵字參數是一個明智的選擇,但它須要遠見。若是容許按位置傳遞參數,則不能僅在之後的版本中將其轉換爲僅關鍵字。因此,如今加上星號。你能夠在 asyncio API 中觀察到,它在構造函數、方法和函數中廣泛使用星號。儘管到目前爲止,Lock 只接受一個可選參數,但 asyncio 開發人員當即添加了星號。這是幸運的。

# In asyncio.
class Lock:
    def __init__(self, *, loop=None):
        # ...
複製代碼

如今,咱們已經得到了改變方法和參數的智慧,同時保持與用戶的約定。如今是時候嘗試最具挑戰性的進化了:在不改變方法或參數的狀況下改變行爲。

改變行爲

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

Rattlesnake
Rattlesnake

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

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

當行爲在沒有新函數或新參數的狀況下發生更改時,負責任的建立者能夠從 Python 標準庫中學習。好久之前,os 模塊引入了 stat 函數來獲取文件統計信息,好比建立時間。起初,這個時間老是整數。

>>> os.stat('file.txt').st_ctime
1540817862
複製代碼

有一天,核心開發人員決定在 os.stat 中使用浮點數來提供亞秒級精度。但他們擔憂現有的用戶代碼尚未作好準備更改。因而他們在 Python 2.3 中建立了一個設置 stat_float_times,默認狀況下是 False 。用戶能夠將其設置爲 True 來選擇浮點時間戳。

>>> # Python 2.3.
>>> os.stat_float_times(True)
>>> os.stat('file.txt').st_ctime
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
Demeter

咱們的 10 個約定大體能夠分爲三類:

謹慎發展

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

嚴格記錄歷史

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

緩慢而明顯地改變

  1. 兼容添加參數
  2. 逐漸改變行爲

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


這篇文章最初是在 A. Jesse Jiryu Davis 的博客上'出現的,經容許轉載。

插圖參考:


via: opensource.com/article/19/…

做者:A. Jesse 選題:lujun9972 譯者:MjSeven 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章
相關標籤/搜索