如何構建一個優雅擴充套件

blankqwq發表於2020-09-11

如何構建一個優雅擴充套件

小白隨筆,希望大佬多提建議

SDK 是什麼

SDK是對基礎程式庫(函式庫)的一種封裝,又稱之為軟體開發工具包

這種闡述過於泛化,我們將範圍縮小到擴充套件這個概念

SDK是一種DSL(Domain Specific Language)領域特定語言,對某需求的使用性闡述可執行語言,實際上也就是將某種擴充套件性需求用程式碼的方式闡述出來

擴充套件 的型別

Laravel

根據不同的應用場景可以分為一下幾種型別:

  • 應用上劃分
    • 對接型別:將要對接的某些介面封裝起來,例如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

將擴充套件抽象成一個完善功能的類。

  • 在功能上,有明確的界限劃分
  • 在設計上,五大原則
    • 單一責任原則
    • 里氏替換原則

基礎模型

Laravel

  • 成為框架中的一部分(熱插拔)

    • 將例項物件注入框架中,獲取配置等方式可以與框架內部方法相互結合
    • 無需自建工廠
    • 元件工具型別,可能是多個擴充套件組合,通常具有完整的某一類功能,並提供相應的其他元件
  • 對接SDK模型

Laravel

核心是獲取資料,但是需要傳入配置陣列,後期有新的資訊介面需要對接 可以實現介面/繼承父類,並提供良好的語義呼叫。

  • 功能型別

主要為語言/框架 提供某種原本不支援的功能

  • 獨立執行

Laravel

每個工具都可以單獨使用,只需要傳遞需要的引數即可,這樣既可以提供非常語義化和明瞭話的引數,又可以減少前期開發成本。

呼叫方式如下:

//我們需要統一呼叫方式,抽象出介面
Tencent::getInfo($appKey,$name)
Aliyun::getInfo($appKey,$name)

Tencent::parseManage()
Aliyun::parseManage()

$tencent = new Tencent()
$tencent->getInfo();
$aliyun = new Aliyun();
$aliyun->getInfo();
....
  • 統一入口

Laravel

統一入口執行,可以減少物件的建立,將內容集中化管理

一般使用組合的方式體現出多型

呼叫方式如下:

$tools = new XxxTool($config);
$tools->tencent()->get();
$tools->aliyun()->get();


$tools->tencent->get();
$tools->aliyun->get();

案例分析

下面將以一個例項來說明擴充套件建模與分析的過程

Laravel

開發SDK第一步,準確劃分型別和具體規模

需求:從指定介面獲取我們想要的資料

  • 型別分析
    • 根據主要需求,獲取xxx系統介面的資料,且無需對框架進行適配,說明組要是對接型別。
    • 從當前規模上劃分,該sdk目前屬於工具型別,模型單一隻有獲取資料介面,無需額外提供日誌/特殊服務
根據型別合理建模

Laravel

語義化能力較弱,但是擴充套件性和可維護性高,封裝程度高,對於其他開發者來說使用不需要在意內部邏輯即可以進行使用。

開始設計

Laravel

  • 我們的需求具有以下幾種基本需求:
    • 方便擴充套件
      • 只需要增加模組和對應方法即可
    • 簡單易用
      • 傳入配置資訊就可以快速使用,並對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          入口

測試

  1. 編寫單元測試基類
  2. 針對每一個模組進行不同的測試行為(說明每一個子模組都是可以單獨測試的,需要保證程式碼的可測試性)
  3. 編寫大量的測試用例

文件編寫

  • 目錄

  • 專案介紹

    • 重要依賴(環境版本,要求等)
    • 專案維護、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

你可以透過以下幾種方式:

  1. 擴充套件bug請提交到 issue tracker.
  2. 回答問題或修復bug issue tracker.
  3. 提供新功能或更新文件.
  4. 貢獻說明
    …..

TO DO

License

MIT


Tag規範

示例 命名型別
v1.0.01 1,主版本號:當你做了不相容的 API 修改,
0,次版本號:當你做了向下相容的功能性新增,
01,修訂號:當你做了向下相容的問題修正。

測試

單元測試

  • 效能最佳化
  • 找出邏輯錯誤
  • 確保模組可以正常執行,結果正確

整合測試

  • todo

測試覆蓋率

  • todo

其他

碰到的問題

  1. 最開始違背了簡單設計的原則,考慮的範圍過於長遠,導致長時間都在思考設計
  2. 對於程式碼的設計粒度考量不夠正確,導致程式碼修修改改

總結

  1. 簡單設計
  2. 明確定位
  3. 文件清晰
  4. 解決問題

推薦閱讀

手把手教你如何構建一個優秀的開源專案

一步步帶你開發 Laravel 5.5 擴充套件包(實戰教程)

pds/skeleton PHP 擴充套件架構規範(擴充套件開發必讀)

學習擴充套件包的開發到釋出

本作品採用《CC 協議》,轉載必須註明作者和本文連結
學習,沖沖衝~

相關文章