如何構建一個優雅擴充套件
小白隨筆,希望大佬多提建議
SDK 是什麼
SDK是對基礎程式庫(函式庫)的一種封裝,又稱之為軟體開發工具包
。
這種闡述過於泛化,我們將範圍縮小到擴充套件
這個概念
SDK是一種DSL(Domain Specific Language)
領域特定語言,對某需求的使用性闡述可執行語言,實際上也就是將某種擴充套件性需求用程式碼的方式闡述出來
擴充套件 的型別
根據不同的應用場景可以分為一下幾種型別:
- 應用上劃分
- 對接型別:將要對接的某些介面封裝起來,例如
easywechat
,pay
等等 - 功能型別:實現某類系統/特殊功能的呼叫,例如
guzzleHttp
,ml
等等 - 規範型別:為某些服務提供對於擴充套件的規範,大部分為介面,例如
psr/log
等等 - 工具包型別: 封裝某些常用的方法
- 外掛型別:為某些框架提供額外功能/最佳化功能,例如
jqhph/laravel-wherehasin
,laravel-admin的外掛
,laravel-s
等等
- 對接型別:將要對接的某些介面封裝起來,例如
- 規模上劃分
- 工具型別:模型較為單一,沒有太多的模型進行復合,例如
ml
,factoryCode
,supports
等等 - 元件型別:多個模型組合所形成的擴充套件,提供某功能的一整套/完整的解決方案 例如
symphony-console
,laravel-s
,pay
,easywechart
,laravel-pay
,laravel-admin
的多種元件 - 框架型別: 多種元件的組合,已經脫離了我們擴充套件範圍,例如:
laravel/framework
,thinphp/framework
- 工具型別:模型較為單一,沒有太多的模型進行復合,例如
- 使用上劃分
- 獨立執行:每個子模組都可以單獨執行
- 統一入口:透過特定模組使用入口進行執行(通常透過工廠模式等)
- 抽象型別:將內容抽象化,一個類可以完成所有功能(通常同功能的外部連結: 資料庫操作)
- 組合型別:依賴其他元件(在框架中)才可執行
- DSL型別上劃分(其實和使用方式相似)
- 內部DSL,用宿主語言實現,例如
psr
- 外部DSL,自己設計的呼叫方式,例如
easywechat
- 內部DSL,用宿主語言實現,例如
將擴充套件抽象成一個完善功能的類。
- 在功能上,有明確的界限劃分
- 在設計上,五大原則
- 單一責任原則
- 里氏替換原則
基礎模型
成為框架中的一部分(熱插拔)
- 將例項物件注入框架中,獲取配置等方式可以與框架內部方法相互結合
- 無需自建工廠
- 元件工具型別,可能是多個擴充套件組合,通常具有完整的某一類功能,並提供相應的其他元件
對接SDK模型
核心是獲取資料,但是需要傳入配置陣列,後期有新的資訊介面需要對接 可以實現介面/繼承父類,並提供良好的語義呼叫。
- 功能型別
主要為語言/框架 提供某種原本不支援的功能
- 獨立執行
每個工具都可以單獨使用,只需要傳遞需要的引數即可,這樣既可以提供非常語義化和明瞭話的引數,又可以減少前期開發成本。
呼叫方式如下:
//我們需要統一呼叫方式,抽象出介面
Tencent::getInfo($appKey,$name)
Aliyun::getInfo($appKey,$name)
Tencent::parseManage()
Aliyun::parseManage()
$tencent = new Tencent()
$tencent->getInfo();
$aliyun = new Aliyun();
$aliyun->getInfo();
....
- 統一入口
統一入口執行,可以減少物件的建立,將內容集中化管理
一般使用組合的方式體現出多型
呼叫方式如下:
$tools = new XxxTool($config);
$tools->tencent()->get();
$tools->aliyun()->get();
$tools->tencent->get();
$tools->aliyun->get();
案例分析
下面將以一個例項來說明擴充套件建模與分析的過程
開發SDK第一步,準確劃分型別和具體規模
需求:從指定介面獲取我們想要的資料
- 型別分析
- 根據主要需求,獲取xxx系統介面的資料,且無需對框架進行適配,說明組要是對接型別。
- 從當前規模上劃分,該sdk目前屬於工具型別,模型單一隻有獲取資料介面,無需額外提供日誌/特殊服務
根據型別合理建模
語義化能力較弱,但是擴充套件性和可維護性高,封裝程度高,對於其他開發者來說使用不需要在意內部邏輯即可以進行使用。
開始設計
- 我們的需求具有以下幾種基本需求:
- 方便擴充套件
- 只需要增加模組和對應方法即可
- 簡單易用
- 傳入配置資訊就可以快速使用,並對ide提示有良好支援
- 封裝程度高
- 基類完成大部分複雜邏輯,模組只提供簡單的獲取資料的方式
- 優雅的錯誤提示
- 方便擴充套件
- 介面和實現定義
- 入口
- 配置
- 例項化工廠
- 具體功能模組
- 規模可能會隨著後續的增大而增大,只需要增加聯結器和模組即可
php
程式碼示例(其他語言擴充套件正在計劃更新中)
工廠
class Factory{
private $tools;
private $clients = [];
private $config;
private const CLIENT_NAME = 'client';
private const DEFAULT_CLIENT = Client::class;
/**
* ToolsFactory constructor.
* 工廠類 生產tool
* @param $config
*/
public function __construct($config)
{
$this->config = $config;
}
/**
* @param $name
* @param array $option
* @return mixed|void
* @throws \ReflectionException
* 生產工具
*/
public function make($name, $option)
{
$key = $this->getKey($name, $option);
return $this->tools[$key] ?? $this->tools[$key] = $this->build($name, $option);
}
/**
* @param $clientClass
* @param $options
* @return Client|mixed
* 生產連線工具
*/
public function createClient($clientClass, $options)
{
if (class_exists($clientClass)) {
// 返回
if (empty($options)) {
$options = $this->getDefaultClientOptions($clientClass);
}
$key = $this->getKey($clientClass, $options);
return $this->clients[$key] ?? $this->clients[$key] = new $clientClass($options);
}
}
/**
* @param $name
* @param $option
* @return string
* 根據配置和模組名生成對應key,在物件中查詢是否存在
*/
public function getKey($name, array $option): string
{
return md5($name . json_encode($option));
}
/**
* @param $className
* @param $option
* @return mixed
* @throws \ReflectionException
*/
public function build($className, $option): BaseTool
{
if (!class_exists($className)) {
$className = sprintf('XxxxSdk\Tools\%s', ucfirst($className));
}
[$clientClass, $parameters] = $this->getClient($className);
$parameters = $this->getFromConfig($parameters);
return new $className($this->createClient($clientClass, $option), ...$parameters);
}
/**
* @param $className
* @return array
* @throws \ReflectionException
* 獲取所需要的聯結器
*/
public function getClient($className): array
{
$reflection = new \ReflectionClass($className);
$constructor = $reflection->getConstructor();
$client = self::DEFAULT_CLIENT;
$parameters = [];
if ($constructor->isPublic()) {
$res = $constructor->getParameters();
foreach ($res as $item) {
if ($item->getName() === self::CLIENT_NAME && $item->getClass()) {
$client = $item->getClass()->getName();
} else {
$parameters[] = $item->getName();
}
}
}
return [$client, $parameters];
}
}
入口
class Index{
/**
* @var string
* 版本
*/
private static $version = '0.1.0-dev';
/***
* @var self
*/
private static $instance;
/***
* @var ToolsFactory
*/
private $factory;
/**
* @var array
*/
private $config = [
'appKey' => "",
'http' => [
'base_uri' => 'xxx.com',
'timeout' => 10.0
],
];
/**
* XxxxSdk constructor.
* @param array $config
*/
public function __construct($config = [])
{
$this->setConfig($config);
$this->createFactory();
}
private function createFactory(): void
{
$className = $this->config['factory'] ?? ToolsFactory::class;
$this->factory = new $className($this->config);
}
/**
* @return XxxxSdk
*/
private static function getInstance(): TestingTools
{
return self::$instance ?: self::$instance = new self();
}
/**
* @return string
*/
private function getVersion(): string
{
return self::$version;
}
private function setConfig($config = []): TestingTools
{
if (!empty($config)) {
$this->config = array_merge($this->config,$config);
}
return $this;
}
private function callTools($name, $option = [])
{
return $this->factory->make($name, $option);
}
/**
* @param $name
* @return mixed
*/
public function __get($name)
{
return $this->callTools($name);
}
public function __call($name, $arguments)
{
return $this->callTools($name, ...$arguments);
}
}
基類
class Tools{
protected $auth = true;
protected $client;
protected $appKey;
public $successCodeRange = [200, 300];
public function __construct(Client $client, $appKey)
{
$this->client = $client;
$this->appKey = $appKey;
}
/**
* @param $url
* @param $method
* @param $data
* @return Result
* @throws HttpClientErrorException
*/
private function request($url, $data, $method = 'get'): Result
{
...
}
/**
* @param $url
* @param $data
* @return Result
* @throws HttpClientErrorException
*/
public function get($url, $data): Result
{
return $this->request($url, ['query' => $this->buildData($data)], 'get');
}
/**
* @param $code
* @return bool
*/
public function checkSuccess($code): bool
{
[$start, $stop] = $this->successCodeRange;
return $code >= $start && $code < $stop;
}
/**
* @return Client
*/
protected function getClient(): Client
{
return $this->client;
}
/**
* @param $data
* @return array
*/
public function buildData($data): array
{
if (!isset($data['appKey']) && $this->auth) {
$data['appKey'] = $this->appKey;
}
return array_filter($data);
}
/**
* 簡單版本
* @param $data
* @param $rules
* @return array
* @throws \Exception
*/
public function validParameter($data,$rules): array
{
$validate = new Valid($rules);
return $validate->valid($data);
}
}
目錄結構
|---src
|--- Tools 模組
|--- Libs 工具
|--- Contracts 契約
|--- Exceptions 異常
|--- XXXFactory.php 工廠
|--- Index.php 入口
測試
- 編寫單元測試基類
- 針對每一個模組進行不同的測試行為(說明每一個子模組都是可以單獨測試的,需要保證程式碼的可測試性)
- 編寫大量的測試用例
文件編寫
目錄
專案介紹
- 重要依賴(環境版本,要求等)
- 專案維護、CI、依賴更新狀態(如果有)
- 快速開始
- 安裝
- 使用
- 貢獻指南
- 許可(License)
- 聯絡方式
- 鳴謝
最佳實踐 Best-README-Template
專案簡介及創作動機
features & 適用人群
根據規模提供不同的文件型別
版本支援Supported Versions
其它特有的資訊
不最佳實踐
Xxxx的sdk
Introduction
重要依賴
- PHP 7.1 以上
- guzzlehttp/guzzle 6.5.*
- ext-json
- ext-mbstring
安裝 配置好
composer.json
{
"name": "blank/sdk-t",
"license": "MIT",
"type": "project",
"authors": [],
"require": {
"sdk/testing-tools-sdk": "dev-master"
}
}
安裝
composer install xxxx -vvv
快速使用
use XxxxxSdk\Xxxxx;
include "vendor/autoload.php";
// debug環境
$config = [
// 必須傳入 AppKey
'appKey' => 'xxxxxx',
'http' => [
'base_uri' => 'http://xxx.xxx.com/'
]
];
// 正式環境
$config = [
// 必須傳入 AppKey
'appKey' => 'xxxxxxx',
];
$arr =[];
// Xxxx
$getter = new Xxxx($config);
// 呼叫其中模組
$res = $getter->user->getJson($arr)->toArray();
// 列印結果
var_dump($res);
API
Xxxx 主要入口
方法 | 功能 | 引數 |
---|---|---|
__construct | 建構函式 | config【配置項】 |
user | 獲取使用者設定 | 無 |
config配置項
appKey 金鑰
http Guzzle配置項 詳情請看官方文件【https://guzzle-cn.readthedocs.io/zh_CN/latest/】
base_uri 基礎地址 可以用於切換不同環境地址
....
Contributing
你可以透過以下幾種方式:
- 擴充套件bug請提交到 issue tracker.
- 回答問題或修復bug issue tracker.
- 提供新功能或更新文件.
- 貢獻說明
…..
TO DO
License
MIT
Tag規範
示例 | 命名型別 |
---|---|
v1.0.01 | 1,主版本號:當你做了不相容的 API 修改, 0,次版本號:當你做了向下相容的功能性新增, 01,修訂號:當你做了向下相容的問題修正。 |
測試
單元測試
- 效能最佳化
- 找出邏輯錯誤
- 確保模組可以正常執行,結果正確
整合測試
- todo
測試覆蓋率
- todo
其他
碰到的問題
- 最開始違背了簡單設計的原則,考慮的範圍過於長遠,導致長時間都在思考設計
- 對於程式碼的設計粒度考量不夠正確,導致程式碼修修改改
總結
- 簡單設計
- 明確定位
- 文件清晰
- 解決問題
推薦閱讀
一步步帶你開發 Laravel 5.5 擴充套件包(實戰教程)
pds/skeleton PHP 擴充套件架構規範(擴充套件開發必讀)
本作品採用《CC 協議》,轉載必須註明作者和本文連結