http://www.cuteqt.com/blog/?p=855
譯者vrcats,轉自CuteQt
前言
給想寫函式庫的人。本文是vrcats按照一個Qt開發工程師寫的《A Little Manual of API Design》翻譯而成的。其中總結了多年Qt開發中關於API設計的一些經驗和教訓,提煉出了一系列關於API設計和物件導向函式庫設計的原則和方法論。水平很高,門檻不高,是vrcats目前看到的最好的關於C API設計資料。
本文並非嚴格翻譯。有許多段落思維太跳躍了不搭界,作了刪節處理。有些例子不恰當的也作了一些修改,也加入了一點個人對API設計的理解。譯者對於API設計其實也是初哥,相信國內做API設計的也不多,從這方面資料的量就看得出來。翻譯得雖然有點糙,總的思想不會錯。全文萬字左右,不長,大家慢慢咀嚼。發現錯誤回帖告訴我,我會及時更正。轉貼請註明原始來源,譯者和轉自CuteQt。
原著:http://twiki-nokia.troll.no/twiki/pub/Development/Training/api-design.pdf
目錄:
*簡介
*好的API長啥樣
*設計流程
*設計原則
簡介
API是應用程式介面的縮寫。說白了就是開放給程式設計者的一堆符號,這樣他們才能使用你的函式庫。 API設計是函式庫設計中最難搞的一步,會影響到上層應用程式的結構。正如丹尼爾杰克遜大叔說的,軟體就跟砍竹子是一樣一樣的,都那麼抽象。你要找對了口兒就勢如破竹,那叫順水行舟一路狂奔,加新功能也不需要怎麼大改。你要是一開始設計錯了,那就兩步一個坎三步一個癤子,等著無窮的驚訝吧。
本手冊來自多年Qt開發的經驗。設計的時候要仔細考慮API設計,同時兼顧實現難度和性能。不管你是設計公用API還是本地庫,這些規則都挺有用的。
好的API長啥樣?
這個問題太主觀。在跟一些大牛研究之後,大家一致認為好的API要有如下特點:
*好學好記
*看了就知道是乾啥的
*不容易用錯
*容易擴展升級
*完整
有倆特點這裡沒寫,“簡潔”和“一致”,因為這倆是廢話。一套不一致的API不可能易學易用,也不可能很容易擴展。簡潔當然應該是我們追求的目標,但是簡潔的最終目的也是為了強化以上幾個特點。
但是設計API的時候,一定要注意一致性。在系統設計中,一致性是非常重要的,尤其是複雜的系統。所有的想法都要圍繞一個原則,採用一套方法。如果你手頭已經有了一套很好的庫,你想擴展它的時候一定要盡量學著原來的風格,這是成功的捷徑。
好學好記
讓API好學好記可不容易。命名,符號,概念和推測在這裡都很重要。同一概念應該有同樣的名字,不同的概念必須有不同的名字。
簡潔的API好記,因為短。一致的API好記,因為能夠聯想推測。
API不僅僅是類和方法的名字那麼簡單,還包含其中隱含的語義。這個語義應該是簡單明確的,讓使用者能夠猜到。
有些API門檻很高,用起來很麻煩,要寫一大堆的程式碼才能工作,這樣就很難用。比如自己定義一個widget,3D界面,搞一個model,這都很讓人頭疼。如果API設計得好,使用者應該能很容易寫出一個”Hello World”,然後從這裡開始慢慢擴展他們的知識。
比如說QPushButton就是個容易學的API代表。只要生成一個物件,甚至都不用呼叫show(),就可以完成一個完整的”Hello World”:
- 代碼: 選擇全部
int main()
{
QPushButton button("Hello world");
return QApplication::run();
}
有時候,提供一些便利API有利於使用者記憶和使用你的庫。比如你有一個API叫insertItem(int, Item),一般就應該提供一個addItem(Item)來方便使用者使用。
看了就知道是幹啥的
有的計算機語言容易理解,但也有不好理解的,比如APL語言,簡直就是數學公式的堆砌。 Perl的名聲也不好,看Perl程式簡直就像在讀密碼本。
應用程式只寫一次,但是是要被讀很多次的。程式易讀則易寫文件易維護,也會比較少有錯誤,因為錯誤會比較明顯容易發現。
比如在Qt3裡,QSlider可以這樣初始化:
- 代碼: 選擇全部
slider=new QSlider(8,128,1,6,Qt::Vertical,0,"volume");
在Qt4裡,一般這樣寫:
- 代碼: 選擇全部
slider=new QSlider(Qt::Vertical);
slider->setRange(8,128);
slider->setValue(6);
slider->setObjectName("volume");
這樣比較容易讀,而且容易發現錯誤:設置值6對於slider越界了。
可讀性強的程式碼來自於適當的抽象。不要隱藏重要的信息,也不要讓使用者提供無關的信息。
比如Qt Jambi的一段程式:
- 代碼: 選擇全部
QGridLayout layout=new QGridLayout;
layout.addWidget(slider,0,0);
layout.addWidget(spinBox,0,1);
layout.addWidget(resetButton,2,1);
layout.setRowStretch(1,1);
setLayout(layout);
相比之下Java Swing的程式碼就傻多了:
- 代碼: 選擇全部
GridBagLayout layout=new GridBagLayout();
GridBagConstraints constraint=new GridBagConstraints();
constraint.fill=GridBagConstraints.HORIZONTAL;
constraint.insets=new Insets(10,10,10,0);
constraint.weightx=1;
layout.setConstraints(slider,constraint);
constraint.gridwidth=GridBagConstraints.REMAINDER;
constraint.insets=new Insets(10,5,10,10);
constraint.weightx=0;
layout.setConstraints(spinner,constraint);
constraint.anchor=GridBagConstraints.SOUTHEAST;
constraint.fill=GridBagConstraints.REMAINDER;
constraint.insets=new Insets(10,10,10,10);
constraint.weighty=1;
layout.setConstraints(resetButton, constraint);
JPanel panel=new JPanel(layout);
panel.add(slider);
panel.add(spinner);
panel.add(resetButton);
不容易用錯
好的API會引導使用者正確使用,甚至引導使用者採取更好的程式設計風格,想用錯都難。好的API不應該限制使用者以某些特定的順序和方式來呼叫API。
比如我們看如下三種語言的語法,html,tex和latex:
- 代碼: 選擇全部
html: the <b>goto <u>label</u></b> statement
tex: the {\bf goto \underline{label}} statement
latex: the \texbf{goto \underline{label}} statement
html的語法明顯重複,而且容易搞錯順序,造成錯嵌套。而tex的語法很容易忘記括號,造成錯誤蔓延。 latex就解決了這個問題,使用比較長的命令,使用者不太容易打錯。
設計API其實和設計腳本語言很類似。你應當把API本身看作一種語言,而不是語言的擴展。以下的C程式碼打印了上文的html文件:
- 代碼: 選擇全部
stream.writeCharacters("the");
stream.writeStartElement("b");
stream.writeCharacters("goto ");
stream.writeStartElement("i");
stream.writeCharacters("label");
stream.writeEndElement("i");
stream.writeEndElement("b");
stream.writeCharacters(" statement");
方便一點,你可以讓C編譯器檢測是否有錯誤嵌套:
- 代碼: 選擇全部
stream.write(Text("the ") Element("b",Text("goto ") Element("u","label")) Text(" statement"));
這種問題其實很常見。比如在配置檔案的使用中,你可能會寫這樣的程式碼:
- 代碼: 選擇全部
QSettings settings;
settings.beginGroup("mainwindow");
settings.setValue("size",win->size());
settings.setValue("fullScreen",win->isFullScreen());
settings.endGroup();
- 代碼: 選擇全部
還有一個來自腳本語言的教訓。 html開發者曾經試圖用<em>來代替<i>,用<strong>來代替<b>,但是使用者根本不買帳。因為敲<i>,<b>容易得多,而且意義更加明確。如果真的要修改成語義明確的符號,他們也該改成<span> 之類的,這樣不上不下的API設計讓人很不爽。
在Qt3中大家經常把qPushButton, QLabel, QLineEdit的ObjectName和parent兩個參數順序弄錯。你如果寫成:
- 代碼: 選擇全部
button=new QPushButton(this, "Hello");
編譯能夠通過,可是沒有文字,因為參數順序錯了。所以在Qt4裡改為:
- 代碼: 選擇全部
button=new QPushButton(this);
button->setObjectName("Hello");
這樣就明確多了。
最後,去掉多餘的部分能夠避免使用者錯用API。比如addItem(Item)就比insertItem(int, Item)更不容易用錯。
容易擴展
庫一直在成長。新的類加入,舊的類獲得新的方法,新的枚舉值。這都是API設計需要考慮的問題。在原始API設計的時候,更應該考慮到二進制兼容的問題。
Qt2的QStyle是一個擴展性極差的反面典型。它定義了一系列的虛函數畫widget,使用者想要在不破壞二進制兼容的情況下實現自己的style幾乎不可能。 Qt3注意到了這個問題,把style變成了基於枚舉的,使用者只要增加枚舉元素就可以輕鬆定義新的style了。
有完整性
API必須提供使用者需要的所有功能。這通常是困難的,一般來說,API可能不直接提供所有的功能,而是提供基礎的功能,高級功能由使用者自行設計實現,如subclassing。
完整性也是與時俱進的。使用中可能會不斷出現使用者需求。但是至少一開始要瞄準正確的方向,每一個新的需求都是往正確方向前進的一步。
設計流程
設計一套API可能要幾人年的工作量。設計的每一步都是完善的過程,當然也可能是搞砸API的過程。以下的原則可能有助於你更好地設計API。
仔細研究需求
設計之前要仔細研究需求,知道需要的是什麼。多諮詢大家,比如你老闆同事和使用者,看他們想要怎樣的功能。
Qt4.3的MDI實現之前就在內部開發郵件列表上徵求了很多人的意見。大家對以前的MDI框架存在的問題和沒有的功能進行了深入的討論,對API設計的幫助很大。
設計之前先寫用例
一般設計API的通病是先實現功能,然後設計API,最後發布。其實應該先設計再實現。
開始設計API之前,先寫幾個使用這套API的程式碼片段。在這個階段先不要考慮實現的難度。用例寫完,API的雛形也就出來了。總的原則是“讓事情簡化,不可能變為可能”。
一個例子就是QWizard。 QWizard有兩種,一種是簡單的線性Wizard,不能跳來跳去的,另一種是複雜的。經過使用用例我們發現簡單的Wizard可以看作是複雜Wizard的一個特例,這樣就簡化了API設計。
研究同一函式庫中類似的API設計
要設計XmlQuery,一個好的辦法就是參考本類庫中的SqlQuery。這兩個概念很相似,都是完成查詢,瀏覽結果,顯示等。熟悉SqlQuery的使用者不用費勁就能學會新的API。你也可以參考SqlQuery的構架方式,減輕設計工作量。
當然,完全照抄也是很傻的。你應該批判地繼承,加以發展。首先完善這個設計,然後加以學習。
如果要給一套API寫新版本,第一件事就是要透徹地了解這套API。不要全盤否定舊的設計,不要試圖代替,而要創造性地設計。為了兼容的需要,你可能要包含所有上一版本的功能。
腦殘的例子:Qt4.0中,QDockWindow被改名為QDockWidget,沒有任何原因;QTextEdit::setOverwriteMode()被取消了,後來4.1又重新加入。
先設計,後實現
實現API之前,要確定API的語法。對於使用者很多的庫,認可你自己的實現麻煩複雜一些,也要讓使用者用著簡單直接。
Qt4中,可以先生成一個QWidget,以後再設置它的parent。在4.0和4.1中,這將會在後台創造一個窗口句柄,開銷很大。在4.2中,實現了delayed window creation,解決了這個問題,這是一個很好的API驅動設計的例子。在Qt3裡,這個問題是通過QWidget::recreate()解決的。這個API就是純粹為了實現而實現的。
要記住,API和它的語法才是庫提供的最終產品。很多產品的實現變了多次,但是API設計始終如初,如UNIX/POSIX, OpenGL, QFileDialog。
實現API的過程中,要不斷寫unit test。這樣你才能發現很多漏洞和空白,然後細化你的設計。但是不要讓實現細節過多影響API的設計,除非是為了一些特殊的原因如性能。
QGraphicsScene::setBspTreeDepth()就是這樣一個例子。這個API純粹是為了提高性能。使用者控制BSP樹的深度可以提高性能,但是大多數情況下,系統預設的樹深度也可以滿足性能需要。因此這個API用了一個比較專業的詞Bsp,表明了這是深入到API實現內部的一個高級 API。初哥一看這個詞不認識就不會輕易嘗試了。
找人幫你評測API
你應該像孫子一樣乞求別人多給你的API一些評測意見,特別是負面的。這些意見更能幫助你改進設計。
多寫幾個例子程式
設計好API後,一定要寫幾個例子。你可以使用設計之前寫的用例。如果有人能幫你寫這些例子程式那就更好了。
Qt所帶的Class Wizard和License Wizard例子都來自於設計用例。
做好擴展的準備
有兩類人會擴展你的API:API維護者:他們會增減你的API介面;使用者:他們會通過定制和繼承來豐富你API的功能。
擴展性設計要仔細分析實際的目標。對於那些有虛函數的類,至少要試著寫3個子類來驗證這些API實現了所有需要的功能,這個我們一般叫作“3個原則”。
在設計Qt4.0的時候,QAbstractSocket設計得就不怎麼好。 Qt4.3要加入QSSLSocket的時候,我們不得不手工降格其中好幾個API,因為他們沒被設計成虛函數。好在它們是在同一個庫中,可以用“手工多態”解決,否則悲劇就無法避免。
內部API沒評測之前不要發布
有些API一開始是內部使用的,後來大家覺得很有用,才公開發布。一個常見的錯誤就是發布之前沒有進行完整的測試。比如Qt就曾經發布過帶有拼寫錯誤的API,不堪回首。
寧缺毋濫
如果對API的功能不是很確定,萬萬不要發布,寧可暫時當作內部API,或者日後再說。
使用者的反饋很重要,但是實現使用者所期待的所有功能是不可能的。一般等3個客戶要求同樣的功能後再實現是比較明智的。
設計原則
這里羅列了一些API設計的基本原則,大部分都來自實際的API設計經驗。其中有些看似衝突,但是其實都有道理。掌握尺度的是你自己,沒有什麼能替代你自己的思考,原則只是原則而已。
命名
名字要能解釋自己,要遵從英語語法。 QPainterPath的作者建議,在文件裡把它叫做vector path,因為這是大家通用的叫法。另一個例子是MDI,儘管實現的是MDI,在Qt4.2之前卻叫做QWorkspace。在4.3之後,改為了 QMdiArea。
另外,參數的命名也要清楚明白。盡量少用bool類型的參數,這樣的程式碼不好讀。 QWidget::repaint()就帶了一個bool類型的參數,來指示是否在重畫之前擦除背景。如果有repaint(false)這樣的程式碼,就很容易讓人誤會,到底是不是不要repaint還是怎樣?解決的方法之一就是用枚舉代替bool,如
- 代碼: 選擇全部
repaint(QWidget::eraseBackground);
命名要統一。不要混用類似widget和control這類詞語,這會讓使用者亂猜。參數的順序也要一致,比如畫方框的函數參數為(x,y,width,height),別的地方也要類似,不要弄成(x,width,y,height)。
比如QStackArray,是一種變長的數組。由於用了stack這個詞,很容易和QStack混淆。 4.1之後,這個類被改為QVarLengthArray。
了解你的使用者也很重要。比如你實現了一套關於XML的API,名字裡帶有XML就是一個很好的主意。如果你自認為API很高檔,一定要叫做什麼IDREFs或者NCNames,使用者會很討厭的。
命名是API設計的一項重要內容。你設計的名字可能會出現在一些IDE的自動完成功能中,這些名字和參數名必須意義明確,簡短有力。尤其要避免一個字母長度的參數名。
避免二義性
一個名字要嚴格對應一個概念。假如你有兩種事件傳遞機制,一個是同步的,一個是異步的,分別叫做sendEventNow()和 sendEventLater()就不錯。如果使用者必須了解同步異步概念,你也可以叫做sendEventSynchronously()和 sendEventAsynchronously()。
如果你要鼓勵使用者多用同步方式,可能會把同步的方法改為sendEvent()。如果你希望使用者用異步方式,就可以反過來把異步方法命名為sendEvent()。
Qt中的sendEvent()是同步的,postEvent()是異步的。這裡就利用了英語中send和post的微妙語義差別。
在命名複製初始化函數的參數時尤其要注意。下列程式碼:
- 代碼: 選擇全部
Car &Car::operator=(const Car &car)
{
m_model=car.m_model;
m_year=car.m_year;
...
return *this;
}
這段程式碼很不好,兩個car很容易混淆。
注意完整性
API設計跟寫書一樣,要注意對稱和前後照應。格式盡量一樣,過程盡量一樣,這樣讀者能更容易了解你的意圖。比如所有的set函數都用set開頭,這樣使用者更容易習慣。
在Qt3中,有一個函數QStatusBar::message(text,msecs)能在狀態條上顯示一條信息msecs毫秒。但這個函數怎麼看都像一個get函數。 Qt4中,我們曾考慮改名為setMessage()以達到一致性。但是setMessage有兩個參數,不太像set函數,最終我們決定改名為 showMessage(),以便區分。
再看event那個例子。同步的時候,可以把event物件當作參數傳遞,因為馬上就會返回,函數可以直接刪除局部變量。但是異步時,就要建立一個新物件,完成以後刪掉,否則就會有記憶體洩露。所以我們應該把兩個API分別設計成:
- 代碼: 選擇全部
sendEventNow(Event event);
sendEventLater(Event* event);
以避免使用者亂用。很不幸地,Qt在這裡犯了腦殘的錯誤,sendEvent和postEvent都是接受Event*的參數,這就很容易造成記憶體洩露。當然,為了一致,你可以定義兩個都接受Event*,然後自己管理event物件的生存期,這樣效率很低下但是很安全。有時候我們就是要在平衡之間作出選擇。
別用縮寫
盡量避免縮寫。當然有一些常見的例外,如min,max,dir,rect,prev。但是要注意有一致性,不能有的用有的不用。 Qt本身在這方面做得其實相當不好。對於參數命名來說,可以適當放寬限制,但是也要保證意思清楚明晰。
名字要專不要通
API的名字空間是很寶貴的。盡量用專用名,否則一旦通用的名字被用了,以後就很難有機會收回來。 QRegExp其實被叫做QStringPattern也很恰當,但是這個名字太通用了,所以最後還是選擇了QRegExp。
比如你要給SQL添加一個錯誤報告類,最好叫做SqlErrorHandler而不是ErrorHandler,否則將來很難與XmlErrorHandler作出區分。將來擴展庫的時候,如果要用到ErrorHandler作為基類也不會頭疼。
Qt在某些方面做得也很不好。比如QDom系列類,就沒有區分SAX和DOM的分支,這造成了一定的混亂。
不要太過遷就下層API
如果你要包裝一系列API,不要被它的命名方式所支配。按照你自己的命名規則統一命名方式。你設計的目的是讓使用者使用方便高效,而不是遷就下層的庫。
選擇合適的預設值
在Qt中設計一個按鈕很容易:
- 代碼: 選擇全部
QPushButton * button=new QPushButton(text,paret);
如果你編過Cocoa程式,你就知道,要生成一個按鈕要設置9個參數,而99%的時間你選擇的初始參數都是一樣的。為什麼不用預設值呢?這就是Qt聰明的地方。盡量讓你的客戶省事,猜測他們需要什麼預設值,不要讓他們費勁,隱藏不必要的細節,這就是API的設計之道。
通過選擇合適的預設值,不僅可以減少程式碼量,還可以讓API簡單可預測。尤其當你有bool類型參數的時候,盡量讓預設值為false。不要以為參數越多API就越強大,你需要的是易用的API。
不要自作聰明
API應該簡單清楚,盡量少讓使用者產生驚訝的感覺。如果過於自作聰明把API弄得不易用,就遠離了API的本來目的。盡量貼近你使用者的習慣而不是試圖教他們怎麼做,否則你就等著寫文件去吧。
Qt3的QLabel就是一個自作聰明的例子。 QLabel::setText()集成了顯示普通文本和html文本的功能。貌似節省了一個API,但是這樣很容易被客戶誤用。如果客戶想顯示一些 html的源程式碼,還必須呼叫setTextFormat(),大部分人並不知道這個從而變得無所適從。避免自作聰明的方法是分開兩個setText() 和setHtml()。
注意邊界值
對於類庫來說,邊界值的處理相當重要,認為邊界值發生概率很小就不加註意是很幼稚的。邊界值造成的問題會在使用這個類的其他類中得到擴散和放大。比如字符串查找函數有邊界值問題,在正則表達式中,這個問題很可能就會被放大。
處理邊界值的一個常見錯誤是在函數開始的時候就加入邊界檢查。這樣做大多數時候並不是必要的。建議你先按照正常的情況進行處理,最後才對邊界值進行處理,這樣可以提高API的效率。另外就是要記得在unit test中加入邊界值的測試。
小心定義虛API
虛API一般更難定義,並且很容易在新版本發佈時出錯。這個問題叫做“fragile base class problem”。設計虛API時,要注意以下兩個問題:
第一是定義的虛API太少,以後發現不夠用。一開始很難知道將來要用什麼樣的API,要用多少。萬一定義的API不夠用,會限制使用者的擴展功能。
第二個就是濫用virtual。在C++中,虛函數效率是很低的。如果你的類別並不需要擴展這個功能,就不要定義成虛函數,否則不僅效率低下,還會誤導使用者。
設計API時,你必須全盤考慮,逐個過濾來決定哪些API應該是虛的,哪些不應該是,在文件裡應該詳細說明你的類如何使用這些虛方法。
在C++裡,大部分虛函數應該被聲明為保護的,以保證不被錯誤修改呼叫影響其他類別的訪問。
Qt4的QIODevice就是一個很好的例子。公用API為read(),write(),而虛函數為readData()和writeData()。這樣就避免了訪問混亂的情況發生。 QWidget也類似,公用API為show(),resize(),repaint(),而虛函數為 showEvent(),resizeEvent(),paintEvent()。
C++的一個很操蛋的地方就是加入虛函數肯定會破壞二進制兼容。有一個很噁心的辦法可以避免這個問題,那就是定義一個通用的虛函數佔位:
- 代碼: 選擇全部
virtual void virtual_hook(int id, void * data);
結構性
很多API在建立物件的時候要求使用者指定一大堆的屬性,比如Win32程式設計:
- 代碼: 選擇全部
m_hWindow = ::CreateWindow("AppWindow", /* class name */
m_pszTitle, /* title to window */
WS_OVERLAPPEDWINDOW, /* style */
CW_USEDEFAULT, /* start pos x */
CW_USEDEFAULT, /* start pos y */
m_nWidth, /* width */
m_nHeight, /* height */
NULL, /* parent HWND */
NULL, /* menu HANDLE */
hInstance, /* */
NULL); /* creatstruct param */
這麼多參數對於使用者來說是個噩夢。一般現代API會採用另外一種方式,就是基於屬性的設計。這樣使用者就可以用很多行程式碼慢慢設計一個類實例,不需要干預的非必須屬性完全可以不管。
- 代碼: 選擇全部
window = new Window;
window->setClassName("AppWindow");
window->setWindowTitle(winTitle);
window->setStyle(Window::Overlapped);
window->setSize(width, height);
window->setModuleHandle(moduleHandle);
這樣做有多個優點:
*看起來比較簡單
*不用記住參數的順序
*可讀性強,不需要特別說明註釋
*屬性可以有預設值,不是必須指定所有的屬性
*隨時可以更改屬性
*可以隨時取得屬性,便於除錯
*方便進行可視化圖形化設計
對於開發庫的牛人來說,對此要多多考慮一層。因為屬性設置的順序不確定,一般要進行”lazy initialization”來避免每一個屬性變化的時候都重新初始化整個物件。
比如QRegExp,使用者可以這樣初始化:
- 代碼: 選擇全部
QRegExp regExp("*.wk?", Qt::CaseInsensitive, QRegExp::Wildcard);
也可以這樣初始化:
- 代碼: 選擇全部
QRegExp regExp;
regExp.setPattern("*.wk?");
regExp.setCaseSensitivity(Qt::CaseInsensitive);
regExp.setPatternSyntax(QRegExp::Wildcard);
在實現中,QRegExp把編譯表達式的過程延後到第一次使用時,避免了多次編譯。
最高境界是手中無劍
劍客的最高境界是手中無劍,心中有劍。最好的API是讓使用者完全不覺得在用你的API,而是在用他們最熟悉的工具,完全沒有障礙和隔閡。
在Qt3中,QWidget的最大限制是32768×32768。在Qt4中,已經沒有了這種限制。 Qt4還增加了pdf格式的支持,StyleSheet支持,OpenGL支持。雖然這些功能很強大,但是API介面並沒有太大的變化,使用者體驗並沒有太多變化,也不用花費太多時間重新學習。在使用者不知不覺之間,新的功能,新的API已經進入了使用者的視野。使用者雖然渾然不知卻已不知不覺獲得了更加強大的工具而進入了程式設計的自由王國。什麼時候,你能讓使用者忘記API而快樂自然地使用你提供的功能時,你會發現自己已然是個API設計大師了。