一个类只做一件事

说明

本文主要结合代码说明如何去在写代码时做到 一个类只做一件事

需求

实现一个 cURL 类,针对不同平台的 API 进行操作,并最终返回 array 给方法的调用方。

思路

分析需求设计到的功能点:

  • cURL 操作类
  • 不同平台的验证规则可能不一样,所以每一个平台都应该对应一个类
  • cURL 请求一般返回的是字符串,需要将结果转化为 array

实现

  1. 定义好 CurlInterfaceDriver 类的 Interface 规范。

    主要定义三个常用方法

    namespace App\Curl\bin;
    
    interface CurlInterfaceDriver
    {
        public function get($url, $options = []);
    
        public function post($url, $options = []);
    
        public function request($method, $url, $options = []);
    }
  2. 实现抽象类 AbstractHttpCurlDriver

    namespace App\Curl\bin;
    
    use GuzzleHttp\Client;
    
    abstract class AbstractHttpCurlDriver implements CurlInterfaceDriver
    {
        /**
         * @var null|Client
         */
        protected $client = null;
    
        protected $response = null;
    
        public function __construct(Client $client = null)
        {
            if (is_null($client)) {
                $client = new Client();
            }
    
            $this->setClient($client);
        }
    
        /**
         * @param $url
         * @param array $options
         * @return string
         * @date 2019/01/25
         * @author ycz
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function get($url, $options = [])
        {
            $response = $this->getClient()->request('GET', $url, $options)->getBody()->getContents();
    
            $this->setResponse($response);
    
            return $response;
        }
    
        /**
         * @param $url
         * @param array $options
         * @return string
         * @date 2019/01/25
         * @author ycz
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function post($url, $options = [])
        {
            $response = $this->getClient()->request('POST', $url, $options)->getBody()->getContents();
    
            $this->setResponse($response);
    
            return $response;
        }
    
        /**
         * @param $method
         * @param $url
         * @param array $options
         * @return string
         * @date 2019/01/25
         * @author ycz
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function request($method, $url, $options = [])
        {
            $response = $this->getClient()->request($method, $url, $options)->getBody()->getContents();
    
            $this->setResponse($response);
    
            return $response;
        }
    
        /**
         * @return null|Client
         */
        private function getClient()
        {
            return $this->client;
        }
    
        /**
         * @param Client $client
         */
        private function setClient(Client $client)
        {
            $this->client = $client;
        }
    
        /**
         * @return null|string
         */
        public function getResponse()
        {
            return $this->response;
        }
    
        /**
         * @param null|string $response
         */
        private function setResponse($response)
        {
            $this->response = $response;
        }
    }
  3. 每个平台返回数据格式可能不一样,所以抽象类中只将结果字符串返回,针对不同返回数据格式,我们可以创建不同的 Driver 继承 AbstractHttpCurlDriver

    针对 JSON 返回结果的类。

    namespace App\Curl\bin;
    
    class JsonHttpCurlDriver extends AbstractHttpCurlDriver
    {
    
    }

    针对 XML 返回结果的类。

    namespace App\Curl\bin;
    
    class XMLHttpCurlDriver extends AbstractHttpCurlDriver
    {
    
    }

    因为 JSONXML 两个驱动的只有返回结果不一样,其他内容基本一样,所以不用在自己类里面改写抽象类里面的东西。

  4. 实现将 response 转换为 array 的类

    根据使用的 driver 自动转换结果。

    namespace App\Curl\bin;
    
    use App\Exceptions\ResponseNotJsonException;
    use App\Exceptions\ResponseNotXMLException;
    use SimpleXMLElement;
    
    class ApiData2ArrayFactory
    {
        public static function make(CurlInterfaceDriver $curl)
        {
            if ($curl instanceof JsonHttpCurlDriver) {
                return static::json2Array($curl->getResponse());
            }
    
            if ($curl instanceof XMLHttpCurlDriver) {
                return static::xml2Array($curl->getResponse());
            }
        }
    
        /**
         * json 转数组
         *
         * @param $json
         * @return array
         * @date 2019/01/25
         * @author ycz
         * @throws ResponseNotJsonException
         */
        private static function json2Array($json)
        {
            try {
                $data = \GuzzleHttp\json_decode($json, true);
            } catch (\InvalidArgumentException $e) {
                throw new ResponseNotJsonException();
            }
    
            return $data;
        }
    
        /**
         * @param $xml
         * @return array
         * @date 2019/01/25
         * @author ycz
         * @throws ResponseNotXMLException
         */
        private static function xml2Array($xml)
        {
            if (!static::isXml($xml)) {
                throw new ResponseNotXMLException();
            }
            function _XML2Array(SimpleXMLElement $parent)
            {
                $array = array();
    
                foreach ($parent as $name => $element) {
                    ($node = &$array[$name])
                    && (1 === count($node) ? $node = array($node) : 1)
                    && $node = &$node[];
    
                    $node = $element->count() ? _XML2Array($element) : trim($element);
                }
    
                return $array;
            }
    
            $xml = new SimpleXMLElement($xml);
            $data = _XML2Array($xml);
    
            return $data;
        }
    
        /**
         * @param $xml
         * @return int
         * @date 2019/01/25
         * @author ycz
         */
        private static function isXml($xml)
        {
            return xml_parse(xml_parser_create(), $xml, true);
        }
    }
  5. 实现对外调用的 cURL

    namespace App\Curl\bin;
    /**
     * Class Curl
     *
     * @package App\Curl\bin
     * @method get
     * @method post
     * @method request
     * @see AbstractHttpCurlDriver
     */
    class Curl
    {
        private $curlDriver = null;
    
        public function __construct(CurlInterfaceDriver $curl)
        {
            $this->curlDriver = $curl;
        }
    
        public function __call($name, $arguments)
        {
            $this->curlDriver->$name(...$arguments);
    
            return ApiData2ArrayFactory::make($this->curlDriver);
        }
    }
  6. 实现单个平台的 cURL 类,以亚马孙为例,我们创建一个 AmazonCurl

    namespace App\Curl\Amazon;
    
    use App\Curl\BaseCurl;
    use App\Curl\bin\Curl;
    use App\Curl\bin\JsonHttpCurlDriver;
    
    class AmazonCurl extends BaseCurl
    {
    
        public function __construct()
        {
            $this->curl = new Curl(new JsonHttpCurlDriver());//假设亚马逊返回的是 json 数据
        }
    
        public function getProductList()
        {
            return $this->curl->get('http://amazon.com/get-product-list',[]);
        }
    }
  7. 外部调用

    $amazonCurl = new AmazonCurl();
    
    return $amazonCurl->getProductList();
  8. 说明

    如果哪天亚马逊的接口返回变成了 XML 数据,我们只用将 AmazonCurl 构造函数中的 JsonHttpCurlDriver 替换为 XMLHttpCurlDriver 即可。

    后续如果新增了其他格式的数据返回,支持新数据只用实现两个东西:1. 继承于 AbstractHttpCurlDriver 的驱动类。2. 在 ApiData2ArrayFactory 实现改格式转 array 的方法。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 23

第一次发布文章,希望各位大佬能轻拍 :satisfied: :satisfied: :satisfied: :satisfied:

5年前 评论

很好,基本思路不错,既然有了自己的思考,可以多看看现有的 设计模式,互通有无,同时可以避免前人踩过的坑。不过,有一个地方我觉得比较值得注意:

既然已经有了 Interface,可以避免 Abstract Class 的存在;或者直接使用 Abstract Class。

另外,Abstract Class 和 Interface 的用途是完全不同的,尽管都是「抽象」。可以自行谷歌了解一下。

最后,以我的经验来看,尽可能多使用 Interface,避免 Abstract Class。

以上信息仅供参考。

5年前 评论

@minororange Interface 可用于要求某个类具备某某能力。例如:SerializableArrayAccess 等。多个能力组合实现,便形成了一个真实的类。Abstract Class 通常则用于表述某个类最根本「是什么」,就像你前面引用所讲的「表示的是 "is a" 关系」。个人拙见。

5年前 评论

@lovecn guzzle 封装了一遍 json_decode ,解析错误会有异常,原生的没有异常。

5年前 评论

$this->curl = new Curl(new JsonHttpCurlDriver()); 请问JsonHttpCurlDriver这里面怎么写啊

3年前 评论

@Hachiko :kissing_heart: 感谢老大指点,转 xml 的方法和判断都是百度的 :joy: :joy: 没有用过

4年前 评论

思路很不错,isXml 方法中,只有 create 没有 free 大量调用,会有内存累积问题

4年前 评论
leung0826

@minororange 类似于这样

    public function __construct(Curl $curl)
    {
        $this->curl = $curl;
    }
5年前 评论

@minororange 回复都能回复歪来?/滑稽

5年前 评论

@leung0826 AmazonCurl 已经到业务层了,你指的外部传入是什么

5年前 评论
leung0826

JsonHttpCurlDriver(),修改为外部传递是不是更好些呢?

5年前 评论
YLR

@minororange 谢谢了,你回复好快!

5年前 评论

@YLR 这个类是业务基础类,没有代码,方便后续扩展用的

5年前 评论
YLR

没有找到App\Curl\BaseCurl.php类啊?

5年前 评论

@minororange 谢谢,guzzle\src\functions.php 文件找到了

/**
 * Wrapper for json_decode that throws when an error occurs.
 *
 * @param string $json    JSON data to parse
 * @param bool $assoc     When true, returned objects will be converted
 *                        into associative arrays.
 * @param int    $depth   User specified recursion depth.
 * @param int    $options Bitmask of JSON decode options.
 *
 * @return mixed
 * @throws \InvalidArgumentException if the JSON cannot be decoded.
 * @link http://www.php.net/manual/en/function.json-decode.php
 */
function json_decode($json, $assoc = false, $depth = 512, $options = 0)
{
    $data = \json_decode($json, $assoc, $depth, $options);
    if (JSON_ERROR_NONE !== json_last_error()) {
        throw new \InvalidArgumentException(
            'json_decode error: ' . json_last_error_msg()
        );
    }

    return $data;
}
5年前 评论
   学习了
` $data = \GuzzleHttp\json_decode($json, true);`这个前缀GuzzleHttp没必要加吧
5年前 评论

abstract class 表示的是 "is a" 关系,interface 表示的是 "like a" 关系

5年前 评论

@Wi1dcard :joy: 好的,其实我对 Interface 的定义也不是很了解,一直把他当做一个类的规范

5年前 评论

@minororange

我的想法是初步定义好 Interface ,先实现一个用 Guzzle 发送请求的抽象类

那这个抽象类的意义何在?为何不直接实现一个 GuzzleDriver Class?

比如 Soap ,到时候 Soap 的抽象类也继承于这个 Interface。

这里说法不太准确,应当是:实现接口(Implement a Interface)或继承类(Inherit a Class)。

另外,关于请求的话,其实有现成的 PSR-7 可以参照。

5年前 评论

@Wi1dcard 我的想法是初步定义好 Interface ,先实现一个用 Guzzle 发送请求的抽象类,后续可能会有别的请求方式,比如 Soap ,到时候 Soap 的抽象类也继承于这个 Interface

5年前 评论

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