什麼是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! 請注意, 重點是介面

比方說 FirefoxChrome 都有 console.log 來輸出一些資料在 DevTools 上 他們內部的實現不一定是相同的, 但都可以用 console.log 做到這件事情

Interface 至關重要

介面的概念非常重要, 因為介面隱藏了細節, 以生活的例子來說:

無論是電腦、電視、手機充電器、微波爐 ... 等家具, 都可以統一透過 110V 或是 220V 的插座使用

也許他們的功率分別是 700W、300W、65W、1200W, 但是對於電源輸入的介面, 所有的電器大抵上都是相同的

而對於程式的設計, 介面的重要性有幾點:

  1. 對於第三方開發人員來說, 他隱藏了內部的實現, 僅把操作的函式暴露出去
  2. 對於使用者來說, 提供了大致相同的操作方法
  3. 對於模組的規畫人員, 提供了一個好的設計模式

對於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並未提供實作, 而是交由底層的模組實現
  • 具體實現則應該依賴於抽象介面:HardDiskSolidStateDisk 繼承 Pure Virtual Function writeread

倘若未來, 想要再加入新的儲存媒介, 比方說使用記憶體 RAMDisk

// 倘若未繼承 IO_Device, 便無法提供給 Database類別使用
class RAMDisk : public IO_Device {
  // 因為繼承了 IO_Device, 必須實作兩個虛擬函式
  void write(char* content);
  void read(char* filename);
}

以上是一個很經典的例子, 另外一個例子式 C++ 的 STL, 試想一下:

有許多需要操作的容器, 以一維容器來說, 有 vectorlistqueuestack

假設他們都有排序的需求, 實作有可能有兩種情況:

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的最佳做法, 但是暫且不提, 最後總結一下常用的方法與例子

URIPOSTGETPUTDELETE
/customers建立新客戶取得所有客戶大量更新客戶刪除所有用戶
/customers/{id}-擷取 {id} 的客戶資料更新 {id} 的客戶資料刪除 {id} 的客戶資料
/customers/{id}/order為客戶 {id} 建立訂單擷取 {id} 客戶的所有訂單更新 {id} 的客戶訂單刪除 {id} 的客戶訂單

這只是其中幾個例子, 這裡建議可以看看 MOTC API的真實案例