如何构建一个优雅扩展

如何构建一个优雅扩展

小白随笔,希望大佬多提建议

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

  • 其它特有的信息


不最佳实践

XxxxxSdk

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 协议》,转载必须注明作者和本文链接
学习,冲冲冲~
本帖由系统于 1个月前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 4
xiaoAgiao

很干货

7个月前 评论
blankqwq

@xiaoAgiao 谢谢认可!

7个月前 评论

老哥图是用什么工具做的 :smiley:

7个月前 评论
blankqwq

@guanguans Axure RP

7个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!