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

blankqwq發表於2020-09-11

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

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

SDK 是什麼

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

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

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

擴充套件 的型別

Laravel

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

  • `應用上劃分
    • 對接型別:將要對接的某些介面封裝起來,例如easywechart,pay
    • 功能型別:實現某類系統/特殊功能的呼叫guzzleHttp
    • 規範型別:為某些服務提供對於擴充套件的規範,大部分為介面,psr/log
    • 工具包型別: 封裝某些常用的方法
    • 外掛型別:為某些框架提供額外功能/優化功能,jqhph/laravel-wherehasin,laravel-admin的外掛
  • 規模上劃分
    • 工具型別:模型較為單一,沒有太多的模型進行復合,例如ml,factoryCode,supports
    • 元件型別:多個模型組合所形成的擴充套件,提供某功能的一整套/完整的解決方案 例如symphony-console的多種元件,laravel-s,pay,easywechart,laravel-pay,mybaits
    • 框架型別: 多種元件的組合,已經脫離了我們的sdk範圍,laravel/framework,thinphp/framework,blankphp/framework
  • 使用上劃分
    • 獨立執行:每個子模組都可以單獨執行
    • 統一入口:通過特定模組使用入口進行執行(通常通過工廠模式等)
    • 抽象型別:將內容抽象化,一個類可以完成所有功能(通常同功能的外部連結: 資料庫操作)
    • 組合型別:依賴其他元件(在框架中)才可執行
  • DSL型別上劃分(其實和使用方式相似)
    • 內部DSL,用宿主語言實現,例如psr
    • 外部DSL,自己設計的呼叫方式,例如easywechart

將SDK抽象成一個完善功能的類。

  • 在功能上,有明確的界限劃分
  • 在設計上,五大原則

基礎模型

  • 外掛型別

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();

案例分析

下面將以一個例項來說明sdk建模與分析的過程

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('TestingToolsSdk\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' => 'http://testing-tools-webapp.mingchao.com/',
            'timeout' => 10.0
        ],
    ];

    /**
     * TestingToolsSdk 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 TestingTools
     */
    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
    {
        /** @var ResponseInterface $res */
        $res = $this->getClient()->{$method}($url, $data);
        $result = [
            'headers' => $res->getHeaders(),
            'uri' => $url,
            'method' => $method,
            'statusCode' => $res->getStatusCode(),
            'data' => $res->getBody()->getContents(),
        ];
        if (!$this->checkSuccess($res->getStatusCode())) {
            // 丟擲異常
            $data['successRange'] = $this->successCodeRange;
            throw new HttpClientErrorException('Status Code not in successRange', $result);
        }
        return new Result($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)
    • 聯絡方式
    • 鳴謝
  • README最佳實踐 Best-README-Template
  • 專案簡介及創作動機
  • features & 適用人群
  • 根據規模提供不同的文件型別
  • 版本支援Supported Versions
  • 其它特有的資訊

例如

//todo

Tag規範

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

測試

單元測試

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

整合測試

測試覆蓋率

其他

碰到的問題

  1. 分類和規劃比較糾結,前期的優化比較多,所以導致後續時間延誤

    //todo

總結

推薦閱讀

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

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

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

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

相關文章