什麼是API
本章節的一開始, 首先要釐清 API 的意思。API - Application Programming Interface
其代表的意義為應用程式的互動介面, Protocol、函式簽章等, 寬鬆意義上也可以說是 API
而另一方面的意義, 則是程式設計的相關實作
舉例來說:
- 需要使用 OpenGL 繪製, 會使用到
gl[FunctionName]- glVertex2f、glBegin、glEnd ... 等
- 透過URL抓取資料, 比方說:
http://www.example.com/data - 調用System Call, 如 C 語言的
#include<sys/*.h>等
重點在於介面 interface, 從資料結構、常數、函式, 到圖形化介面的按鈕
比方說同樣使用Postgresql資料庫, 可以寫一個JavaScript程式碼:
const { appendFileSync } = require('fs');
const { Client } = require('pg');
const conf = {/* Database connection infomation */}
const client = new Client(conf);
const command = `SELECT * FROM datas`;
client
.query(command)
.then({ rows } => {
const transfromData = rows.map( row => JSON.stringify(row));
appendFileSync('data.json', transfromData);
process.exit();
});
或者使用 shell 去存取:
psql -d mydatabase -U user --password
psql (12.11 (Ubuntu 12.11-0ubuntu0.20.04.1))
Type "help" for help.
user=# SELECT * FROM datas;
或許透過不同的方式去存取資料庫, 但是最終目的都是 從某個地方把資料讀出來, 而資料庫則是提供了客戶端的程式psql 或是相關的函式庫(libpg)
針對特定功能, 提供一系列操作的介面, 就是 API 的本質
Note! 請注意, 重點是介面
比方說 Firefox 與 Chrome 都有 console.log 來輸出一些資料在 DevTools 上
他們內部的實現不一定是相同的, 但都可以用 console.log 做到這件事情
Interface 至關重要
介面的概念非常重要, 因為介面隱藏了細節, 以生活的例子來說:
無論是電腦、電視、手機充電器、微波爐 ... 等家具, 都可以統一透過 110V 或是 220V 的插座使用
也許他們的功率分別是 700W、300W、65W、1200W, 但是對於電源輸入的介面, 所有的電器大抵上都是相同的
而對於程式的設計, 介面的重要性有幾點:
- 對於第三方開發人員來說, 他隱藏了內部的實現, 僅把操作的函式暴露出去
- 對於使用者來說, 提供了大致相同的操作方法
- 對於模組的規畫人員, 提供了一個好的設計模式
對於1、2點, 算是非常直白, 因此這裡著重在第3點
介面隔離原則 & 依賴反轉原則
雖然設計模式的理解在程式碼的開發上也相當重要, 但是這裡不打算細談, 只會提出這兩個重要的原則:
介面隔離原則 Interface Segregation Principle
拆分非常龐大臃腫的介面成為更小的和更具體的介面, 這樣客戶將會只需要知道他們感興趣的方法 從而容易重構, 更改和重新部署
依賴反轉原則 Dependency inversion principle
高層次的模組, 不應該依賴於底層的模組, 而是依賴於抽象介面;以及, 抽象介面不應該依賴於具體實現。而具體實現則應該依賴於抽象介面
這裡把兩個原則同時提出, 以下舉出兩個例子:
倘若我是資料庫的開發人員, 早期階段只有磁碟機時, 做出以下規劃:
class HardDisk {
public:
void write(char* content);
void read(char* filename);
/* more member */
}
現在, 處理好了設備的處理函式, 接下來實現資料庫本身的邏輯:
class DataBase {
public:
int connect(const std::string user, const std::string password);
void load(HardDisk disk);
void store(HardDisk disk);
/* more member */
}
// 成員可能的實作:
void DataBase::load(HardDisk disk) {
/* 根據連線資訊以及查詢指令, 匹配索引、資料庫、資料表等 */
disk.read(filename);
}
void DataBase::store(HardDisk disk) {
/* 建立 BTree、檢查索引、資料型別、格式化等 */
disk.write(content);
}
這看起來很不錯對嗎?但是隨著時間過去, 有了更多設備, 比方說SSD:
class SolidStateDisk {
public:
void write(char* content);
void read(char* filename);
/* more member */
}
class DataBase {
public:
int connect(const std::string user, const std::string password);
void load(HardDisk disk);
void store(HardDisk disk);
// function overload
void load(SolidStateDisk disk);
void store(SolidStateDisk disk);
/* more member */
}
好的, 現在解決了問題, 但是新的問題又出現了:SSD又分成PCIe、SATA、M.2 等介面, 於是可能會新增:
class SolidStateDisk_SATA {}
class SolidStateDisk_PCIe {}
class SolidStateDisk_M2 {}
而在 Database 類別中, 則要擴充更多成員函式, 這顯然會導致程式碼越來越難以維護...
這時候依照依賴反轉原則:
- 高層次的模組, 不應該依賴於底層的模組
- 以及, 抽象介面不應該依賴於具體實現
- 具體實現則應該依賴於抽象介面
首先釐清高層次與底層次的模組, 簡單來說, 越接近應用層(或是說商業邏輯), 就相對高階
DataBase 是高層次的;HardDisk 是低層次的
其次, 高階的模組不應該依賴於低層次的模組, 也就是說這種設計方式是有問題的
class DataBase {
public:
int connect(const std::string user, const std::string password);
void load(HardDisk disk);
void store(HardDisk disk);
void load(SolidStateDisk disk);
void store(SolidStateDisk disk);
/* more member */
}
處理的方式有很多種, 但是具體來說, 有個明確的目標:減少 DataBase 的依賴性
可以先提出一個高階介面, 且不提供具體的實現, 比方說
class IO_Device {
public:
virtual void write(char* content) = 0;
virtual void read(char* filename) = 0;
}
接下來, 移轉高階模組的依賴:
class DataBase {
public:
int connect(const std::string user, const std::string password);
// 依賴於 IO_Device 介面
void load(IO_Device disk);
void store(IO_Device disk);
// void load(HardDisk disk);
// void store(HardDisk disk);
// void load(SolidStateDisk disk);
// void store(SolidStateDisk disk);
/* more member */
}
最後, 修改低階模組的宣告, 使其必須實現高階介面:
class HardDisk: public IO_Device {}
class SolidStateDisk_SATA: public IO_Device {}
class SolidStateDisk_PCIe: public IO_Device {}
class SolidStateDisk_M2: public IO_Device {}
回頭來看的目標:
- 高層次的模組, 不應該依賴於底層的模組:DataBase 現在依賴於
IO_Device之上 - 以及, 抽象介面不應該依賴於具體實現:
IO_Device並未提供實作, 而是交由底層的模組實現 - 具體實現則應該依賴於抽象介面:
HardDisk與SolidStateDisk繼承 Pure Virtual Functionwrite與read
倘若未來, 想要再加入新的儲存媒介, 比方說使用記憶體 RAMDisk:
// 倘若未繼承 IO_Device, 便無法提供給 Database類別使用
class RAMDisk : public IO_Device {
// 因為繼承了 IO_Device, 必須實作兩個虛擬函式
void write(char* content);
void read(char* filename);
}
以上是一個很經典的例子, 另外一個例子式 C++ 的 STL, 試想一下:
有許多需要操作的容器, 以一維容器來說, 有 vector、list、queue、stack 等
假設他們都有排序的需求, 實作有可能有兩種情況:
void sort(std::vector, int startIndex, int endIndex);
void sort(std::list, int startIndex, int endIndex);
void sort(std::queue, int startIndex, int endIndex);
void sort(std::stack, int startIndex, int endIndex);
sort(vec, 0, vec.size() -1 );
或者是依賴於迭代器(Iterator)介面
template<typename T>
class Iterator<T> {
public:
virtual T* begin() = 0;
virtual T* end() = 0;
virtual operator--();
virtual operator++();
}
/* Impl interface Iterator<T> */
template<typename T> class std::vector<T> : public Iterator<T>{}
template<typename T> class std::list<T> : public Iterator<T>{}
template<typename T> class std::queue<T> : public Iterator<T>{}
template<typename T> class std::stack<T> : public Iterator<T>{}
// function signature
void sort(Iterator<T>, Iterator<T>);
sort(vec.begin(), vec.end());
各位可以思考看看, 哪一個才是可能的實現
在撰寫程式的過程中, 多思考看看的介面的意義:
- 使用者如何調用
- 模組之間的依賴關係
- 程式碼中的抽象與實現
再提出一個例子, 比方說今天我是 STEAM 上遊戲的開發人員, 我希望我的遊戲可以下載 Plugin
但是這麼多第三方人員, 要如何規範他們呢?
能不能提出一個基礎類別:
interface Plugin {
init()
load()
disable()
}
並規範所有 Plugin 的開發人員遵守呢?
RESTful
內容是參考以下網站, 並根據開發人員的經驗摘要, 可能會與網路上的一些資訊有出入
https://www.restapitutorial.com/、https://restcookbook.com/
筆者推薦的內容, 則是 Mircosoft 撰寫的雲端應用程式中的最佳做法
這裡先提一個懶人包: API 不等於 RESTful API, RESTful API 只是一種 API 風格而已, 不是規範
前言
再談論 RESTful API 之前, 先簡單說明一下現今的 Web App 所需的 API 會有幾個特色:
- 平台獨立
- 服務迭代
平台獨立的意思是, 不論 API 的內部如何實作, 都應該可以透過公開的、標準的協議進行存取
好比說有個註冊帳號 API, 對於客戶端來說, 不必在乎他是使用什麼程式語言開發的, 也不用知道使用什麼作業系統開發的
只要依照某種標準協議 Ex. HTTP, 就可以讓用戶端與Web服務進行資料交換
服務迭代的概念是, 客戶端與服務端是分開的
伺服器端的服務會不斷迭代更新並且新增功能, 而隨著服務端的更新, 客戶端應該要不進行修改, 依然可以順利運作
我認為的意義是這樣:舉例來說, 今日有個訂餐系統, 會上傳餐點的編號, 如 GHR201 之類的
而原先的程式設計如下:
async function order(foodId) {
const data = await fetch(apiEndpoint, {
headers: { 'Content-type': 'application/json' },
method: 'post',
body: JSON.stringify({ foodId });
}).then(res => res.json());
console.log( data ) /* 內容可能為
{
foodId: 'GHR201',
name: '肉絲炒飯',
price: 60
}
*/
dosomething(data) // 可能會客戶端的畫面顯示"下訂成功" 還有相關資訊
}
但是假設伺服端的回應修改了, 例如:
{
"uniqueId": "GHR201",
"foodName": "肉絲炒飯",
"orderTime": "14:19",
"expectTime": 1200
}
看起來多出了 "下訂的時間" 與 "預期送達時間", 像是 14:19 下訂, 20分鐘後左右會送到
但是 "name" 欄位被移除了, 修改為 "foodName", 這可能導致客戶端的程式無法順利運作
對於伺服端來說, 盡可能使得客戶端的輸入/輸出不需要額外的變動, 就是服務迭代的概念
SOAP(早期的API實現)
上個章節大概理清了:伺服器與客戶端需要有資料交換的機制, 且最好是與實作隔離
僅依賴在某種協定之上, 因此早期衍生出了 SOAP(Simple Object Access Protocol)
透過把 SOAP 繫結到 HTTP, 同時利用了SOAP與HTTP的優點
在HTTP上傳送SOAP並不意味著SOAP會覆蓋現有的HTTP語意, 而是HTTP上的SOAP語意會自然地對映到HTTP語意
在使用HTTP作為協定繫結的場合中, RPC請求對映到HTTP請求上, 而RPC應答對映到HTTP應答。然而, 在RPC上使用SOAP並不僅限於HTTP協定繫結
由 SOAP 發出的請求內容可能會是:
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<req:echo xmlns:req="http://localhost:8080/wxyc/login.do">
<req:category>classifieds</req:category>
</req:echo>
</soapenv:Body>
</soapenv:Envelope>
而收到的回應可能會是:
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<soapenv:Header>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:From>
<wsa:Address>http://localhost:8080/axis2/services/MyService</wsa:Address>
</wsa:From>
<wsa:MessageID>ECE5B3F187F29D28BC11433905662036</wsa:MessageID>
</soapenv:Header>
<soapenv:Body>
<req:echo xmlns:req="http://localhost:8080/axis2/services/MyService/">
<req:category>classifieds</req:category>
</req:echo>
</soapenv:Body>
</soapenv:Envelope>
REST
在 2000 年時, Roy Fielding 提出了以具象狀態傳輸(REST)的架構來設計 Web 服務
REST 全稱 REpresentational State Transfer, 是一種設計風格, 請注意, 是設計風格
而不是標準或是協議, 相較 SOAP, REST 有幾個重要的特徵:
- Uniform Interface
- Stateless
- Cacheable
和 SOPA 或是 XML-RPC 相比之下, REST顯得相對簡單, 並且善用了 HTTP 的語意機制
除此之外, 也不局限於XML, JSON、YAML等格式, 都可作為資源的表達
重點在於 資源的表達
- 透過 URI 來指定存取的資源
- 針對資源, 常需要進行CRUD(Create, Read, Update, Delete), 剛好可對應到 HTTP Method
- 通過資源的表現形式來操作資源
大部分的實例, 都會通過 HTTP 設計 REST 風格的 API, 大致原則如下:
REST API 是依照資源來設計
架設今天有查詢餐廳訂單的API, 可能會是以下形式:
http://example.com/orders
並且可以透過「識別碼」, 存取特定的資源
http://example.com/orders/1
並且會透過交換資源的表示法, 與服務進行互動
{
"orderID":3,
"productID":2,
"quantity":4,
"orderValue":16.60
}
而統一的介面, 通常指的就是 HTTP 協議, 並透過常見的 HTTP Method 來分割 API 如 GET、POST、PUT、PATCH、DELETE 等方法
而 REST API 盡可能要求無狀態, 所有的HTTP要求應該是獨立的, 並且可能會以任何順序發生
這裡不好說明"無狀態的"HTTP是什麼意思, 因此反過來舉例包含狀態的HTTP: 像是常見的購物車系統, 今天在某個電商網站加入物品至購物車中, 關閉網頁後, 下次訪問還是知道購物車的內容; 或是某個網站, 如果以前登入過了, 就直接導向到會員頁面, 反之則是導向登入介面 以上兩個例子, 都是包含狀態性的HTTP請求
接下來, 直接看可能實作的 REST API 實例:
比方說電子商務系統中, 客戶與訂單的關係
可以透過包含訂單資訊的 POST API 建立訂單, HTTP 回應成功與否
https://adventure-works.com/orders // Good
https://adventure-works.com/create-order // Avoid
前面有提到, REST 傾向於使用 HTTP Method 描述行為, 因此 URI 不應包含動詞(create-order), 應該僅包含資源
要注意的事情是, REST 不是單純反映資料庫內部結構的API, 最終目的在於把實體與資源模型化
當然有很多REST的最佳做法, 但是暫且不提, 最後總結一下常用的方法與例子
| URI | POST | GET | PUT | DELETE |
|---|---|---|---|---|
| /customers | 建立新客戶 | 取得所有客戶 | 大量更新客戶 | 刪除所有用戶 |
| /customers/{id} | - | 擷取 {id} 的客戶資料 | 更新 {id} 的客戶資料 | 刪除 {id} 的客戶資料 |
| /customers/{id}/order | 為客戶 {id} 建立訂單 | 擷取 {id} 客戶的所有訂單 | 更新 {id} 的客戶訂單 | 刪除 {id} 的客戶訂單 |
這只是其中幾個例子, 這裡建議可以看看 MOTC API的真實案例