设计模式

1.什么是模式

设计模式是对某些典型易变问题的特定解决方案,这些问题和解决方案经过分类总结,并且为了方便交流给每个解决方案都起了特定的名字。

模式是为了解决变化的问题,将变化的问题进行封装,让变化单独变化而互不影响,以达到项目系统的扩展性,模式是一种更高层次的代码复用。

设想一下,假如我们的项目是一次性项目,一旦发布,以后再也不会修改,那么我们完全没有必要使用设计模式,反而使用了设计模式会增加系统的复杂度,但是一次性的项目在我们实际工作环境中是极少的,如果实际工作环境中都是一次项目,那个时候我们开发人员都应该转行了。

2.为什么要使用模式

  • 每一个设计模式都是为了解决一类特定的问题而存在,是一种更高层级的复用

  • 每一个设计模式都有一个特定的名字,团队协作中,既可以减少沟通成本,同时也可以保证项目质量

  • 模式有助于开发人员提高解决问题的思考层次,能够让我们的系统更容易扩展

3.六大原则

前辈们经过长期经验积累总结了许多设计模式,如单例模式、工厂模式、享元模式、策略模式、观察者模式等等,这些模式都是为了解决某一类特定的问题而设计,但是我们在日常编码过程中可以把这些模式作为一种参考即可,我们在编码过程中满足模式的原则即认为是好的代码。

模式有以下六大原则

名称解释
单一职责要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责
里式代换子类可以扩展父类的功能,但不能改变父类原有的功能
依赖倒置高层模块不应该依赖低层模块,二者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象
接口隔离客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上
迪米特法则一个对象应该对其他对象保持最少的了解
开闭原则一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

3.1 单一职责

单一职责看似简单,其实并不简单,这个原则告诉了我们划分一个类功能的标准,下面我们看一段常见的代码片段

<?php
/**
 * Class Login
 * @datetime 2020/7/12 4:13 PM
 * @author roach
 * @email jhq0113@163.com
 */
abstract class Login
{
    /**微信appId
     * @var string
     * @datetime 2020/7/12 4:11 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    public $wxAppid;

    /**微信密钥
     * @var string
     * @datetime 2020/7/12 4:11 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    public $wxAppSecret;

    /**微信登录
     * @return mixed
     * @datetime 2020/7/12 4:12 PM
     * @author roach
     * @email jhq0113@163.com
     */
    abstract public function wxLogin();

    /**手机号验证码登录
     * @return mixed
     * @datetime 2020/7/12 4:12 PM
     * @author roach
     * @email jhq0113@163.com
     */
    abstract public function phoneLogin();
}

以上例程代码是一个用户登录的代码片段,为了演示方便作者把wxLoginphoneLogin两个方法实现细节进行了屏蔽。

以上代码有什么问题吗?不都是这么做登录的吗?

从模式原则来分析,假如微信登录实现细节发生修改,我们要修改Login类,手机号验证码登录实现细节发生修改,我们同样要修改Login类,如果我们要增加一种QQ授权登录方式,同样我们要修改Login

在模式的理论中,只要发生修改就会有风险,那么我们怎么解决这个问题呢?按照单一职责原则,作者把代码做了如下拆分优化

<?php
/**
 * Class Login
 * @datetime 2020/7/12 4:13 PM
 * @author roach
 * @email jhq0113@163.com
 */
abstract class WxLogin
{
    /**微信appId
     * @var string
     * @datetime 2020/7/12 4:11 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public $wxAppid;

    /**微信密钥
     * @var string
     * @datetime 2020/7/12 4:11 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public $wxAppSecret;

    /**
     * @param array $params
     * @return mixed
     * @datetime 2020/7/12 4:20 PM
     * @author roach
     * @email jhq0113@163.com
     */
    abstract public function login($params = []);
}

/**
 * Class PhoneLogin
 * @datetime 2020/7/12 4:20 PM
 * @author roach
 * @email jhq0113@163.com
 */
abstract class PhoneLogin
{
    /**
     * @param array $params
     * @return mixed
     * @datetime 2020/7/12 4:20 PM
     * @author roach
     * @email jhq0113@163.com
     */
    abstract public function login($params = []);
}

作者将Login类一分为二,微信登录的appId等依赖也放到各自类中,这样调用端如果想实现登录功能选择调用类就可以了。

现在我们分析,假如微信登录实现要发生修改,我们直接修改WxLogin类就可以了,同样的,假如手机号验证码登录实现要发生修改,我们只需要修改PhoneLogin类就可以了;假如我们要增加一种QQ授权登录的方式,此时我们增加一个QqLogin类就可以了。

3.2 里式代换

里式代换原则强调,我们能够扩展基类的原有功能,但不能改变基类原有的功能,这样很容易出现问题,下面还是看一段示例代码

<?php
/**
 * Class Model
 * @datetime 2020/7/12 4:30 PM
 * @author roach
 * @email jhq0113@163.com
 */
class Model 
{
    /**
     * @var \PDO
     * @datetime 2020/7/12 4:29 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    private $_db;
    
    /**
    * @param \PDO $db
    * @datetime 2020/7/12 4:45 PM
    * @author roach
    * @email jhq0113@163.com
     */
    public function setDb(\PDO $db)
    {
        $this->_db = $db;
    }

    /**
     * @return \PDO
     * @datetime 2020/7/12 4:32 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function getDb()
    {
        if(is_null($this->_db)) {
            $this->setDb(new \PDO('mysql:host=127.0.0.1;port=3306;dbname=roach', 'roach', 'roach'));
        }
        return $this->_db;
    }

    /**
     * @param null $name
     * @return string
     * @datetime 2020/7/12 4:36 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function lastInsertId($name = null)
    {
        return $this->_db->lastInsertId($name);
    }
}

/**
 * Class UserModel
 * @datetime 2020/7/12 4:35 PM
 * @author roach
 * @email jhq0113@163.com
 */
class UserModel extends Model
{
    /**
     * @return PDO|string
     * @datetime 2020/7/12 4:35 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function getDb()
    {
        return MyModel::db();
    }
}

/**
 * Class MyModel
 * @datetime 2020/7/12 4:34 PM
 * @author roach
 * @email jhq0113@163.com
 */
class MyModel
{
    /**
     * @var \PDO
     * @datetime 2020/7/12 4:33 PM
     * @author roach
     * @email jhq0113@163.com
     */
    private static $_db;

    /**
     * @return PDO
     * @datetime 2020/7/12 4:34 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public static function db()
    {
        if(is_null(self::$_db)) {
            self::$_db = new \PDO('mysql:host=127.0.0.1;port=3306;dbname=roach', 'roach', 'roach');
        }
        return self::$_db;
    }
}

以上代码也是真实遇到的,分析以上代码,在多人并行开发的团队,一个开发发现Model类的db并非真正的单例,于是这个开发在不改变业务调用代码的基础上,增加了MyModel类实现了约定单例,但是在实现UserModelgetDb方法的时候,改变了基类Model原有的getDb方法给_db私有属性赋值,这样导致基类lastInsertId方法不能使用,此时如果我们使用基类可以正常调用lastInsertId方法,但是使用子类UserModel调用lastInsertId就会发生异常。

以上代码违背了里式代换原则,那么我们应该如何修改呢?作者对UserModel进行了如下优化

<?php
/**
 * Class UserModel
 * @datetime 2020/7/12 4:35 PM
 * @author roach
 * @email jhq0113@163.com
 */
class UserModel extends Model
{
    /**
     * @return \PDO
     * @datetime 2020/7/12 4:35 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function getDb()
    {
        $this->setDb(MyModel::db());
        return parent::getDb();
    }
}

这样修改后保证了单例,和里式代换

3.3 依赖倒置

依赖倒置的思想是个很经典扩展性思维,开发人员如果能够深刻理解依赖倒置原则,将有很大层次的提升,还是看如下代码

<?php
/**
 * Class FileLog
 * @datetime 2020/7/12 6:09 PM
 * @author roach
 * @email jhq0113@163.com
 */
class FileLog
{
    /**
     * @var string
     * @datetime 2020/7/12 6:05 PM
     * @author roach
     * @email jhq0113@163.com
     */
    protected $_logFile;

    /**
     * @param $fileName
     * @datetime 2020/7/12 6:06 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function setLogFile($fileName)
    {
        $this->_logFile = $fileName;
    }

    /**
     * @param string $message
     * @param array  $context
     * @param string $leftPlace
     * @param string $rightPlace
     * @return string
     * @datetime 2019/8/30 18:00
     * @author roach
     * @email jhq0113@163.com
     */
    static public function interpolate($message, array $context = [], $leftPlace='{', $rightPlace='}')
    {
        if(empty($context)) {
            return $message;
        }

        $replace = [];
        foreach ($context as $key => $val) {
            $replace[$leftPlace . $key . $rightPlace] = $val;
        }
        return strtr($message, $replace);
    }

    /**
     * @param string $level
     * @param string $message
     * @param array  $params
     * @datetime 2020/7/12 6:07 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function log($level, $message, $params = [])
    {
        $message = self::interpolate($message, $params);
        $message = json_encode([
            'datetime' => date('Y-m-d H:i:s'),
            'level'    => $level,
            'msg'      => $message,
            'clientIp' => $_SERVER['REMOTE_ADDR'],
            'url'      => $_SERVER['REQUEST_URI'],
            'method'   => $_SERVER['REQUEST_METHOD'],
            'host'     => $_SERVER['HTTP_HOST']
        ], JSON_UNESCAPED_UNICODE);
        file_put_contents($this->_logFile, $message.PHP_EOL, FILE_APPEND | LOCK_EX);
    }
}

/**
 * Class DbLog
 * @datetime 2020/7/12 6:11 PM
 * @author roach
 * @email jhq0113@163.com
 */
class DbLog
{
    /**
     * @var \PDO
     * @datetime 2020/7/12 6:11 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    protected $_pdo;

    /**
     * @param \PDO $db
     * @datetime 2020/7/12 6:12 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function setDb(\PDO $db)
    {
        $this->_pdo = $db;
    }

    /**
     * @param string $message
     * @param array  $context
     * @param string $leftPlace
     * @param string $rightPlace
     * @return string
     * @datetime 2019/8/30 18:00
     * @author roach
     * @email jhq0113@163.com
     */
    static public function interpolate($message, array $context = [], $leftPlace='{', $rightPlace='}')
    {
        if(empty($context)) {
            return $message;
        }

        $replace = [];
        foreach ($context as $key => $val) {
            $replace[$leftPlace . $key . $rightPlace] = $val;
        }
        return strtr($message, $replace);
    }

    /**
     * @param string $level
     * @param string $message
     * @param array  $params
     * @return int
     * @datetime 2020/7/12 6:16 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function log($level, $message, $params = [])
    {
        $message = self::interpolate($message, $params);
        
        $stmt = $this->_pdo->prepare('INSERT INTO `logs`(`datetime`, `level`, `msg`, `clientIp`, `url`, `method`, `host`)VALUES(?,?,?,?,?,?,?)');
        
        $stmt->execute([
            date('Y-m-d H:i:s'),
            $level,
            $message,
            $_SERVER['REMOTE_ADDR'],
            $_SERVER['REQUEST_URI'],
            $_SERVER['REQUEST_METHOD'],
            $_SERVER['HTTP_HOST']
        ]);
        
        return $stmt->rowCount();
    }
}

/**
 * Class LogFactory
 * @datetime 2020/7/12 6:21 PM
 * @author roach
 * @email jhq0113@163.com
 */
class Logger
{
    /**
    * @param string $loggerName
    * @param string $level
    * @param string $message
    * @param array  $params
    * @return int|void
    * @datetime 2020/7/12 6:25 PM
    * @author roach
    * @email jhq0113@163.com
    */
    public function log($loggerName, $level, $message, $params = [])
    {
        if($loggerName === 'db') {
           $logger = new DbLog();
           $logger->setDb(new \PDO('mysql:host=127.0.0.1;port=3306;dbname=roach', 'roach', 'roach'));
           return $logger->log($level, $message, $params);
        }
                
        $logger = new FileLog();
        $logger->setLogFile('/tmp/logs/'.date('Y').'/'.date('m-d').'.log');
        return $logger->log($level, $message, $params);
    }
}

以上代码示例中,Logger类的log方法通过传入参数$loggerName来决定用哪个类去记录日志,这样Logger类直接依赖FileLogDbLog类,当我们再增加一个KafkaLog类打日志时,Logger同样需要依赖KafkaLog类,这样Logger类就需要发生修改,在模式领域,这样的代码认为不可以扩展,那么怎么优化呢?

<?php
/**
 * Class ILog
 * @datetime 2020/7/12 6:32 PM
 * @author roach
 * @email jhq0113@163.com
 */
abstract class ILog
{
    /**
     * @param string $message
     * @param array  $context
     * @param string $leftPlace
     * @param string $rightPlace
     * @return string
     * @datetime 2019/8/30 18:00
     * @author roach
     * @email jhq0113@163.com
     */
    static public function interpolate($message, array $context = [], $leftPlace='{', $rightPlace='}')
    {
        if(empty($context)) {
            return $message;
        }

        $replace = [];
        foreach ($context as $key => $val) {
            $replace[$leftPlace . $key . $rightPlace] = $val;
        }
        return strtr($message, $replace);
    }
    
    /**
     * @param string $level
     * @param string $message
     * @param array  $params
     * @return mixed
     * @datetime 2020/7/12 6:30 PM
     * @author roach
     * @email jhq0113@163.com
     */
    abstract public function log($level, $message, $params = []);
}

/**
 * Class FileLog
 * @datetime 2020/7/12 6:09 PM
 * @author roach
 * @email jhq0113@163.com
 */
class FileLog extends ILog
{
    /**
     * @var string
     * @datetime 2020/7/12 6:05 PM
     * @author roach
     * @email jhq0113@163.com
     */
    protected $_logFile;

    /**
     * @param $fileName
     * @datetime 2020/7/12 6:06 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function setLogFile($fileName)
    {
        $this->_logFile = $fileName;
    }

    /**
     * @param string $level
     * @param string $message
     * @param array  $params
     * @datetime 2020/7/12 6:07 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function log($level, $message, $params = [])
    {
        $message = self::interpolate($message, $params);
        $message = json_encode([
            'datetime' => date('Y-m-d H:i:s'),
            'level'    => $level,
            'msg'      => $message,
            'clientIp' => $_SERVER['REMOTE_ADDR'],
            'url'      => $_SERVER['REQUEST_URI'],
            'method'   => $_SERVER['REQUEST_METHOD'],
            'host'     => $_SERVER['HTTP_HOST']
        ], JSON_UNESCAPED_UNICODE);
        file_put_contents($this->_logFile, $message.PHP_EOL, FILE_APPEND | LOCK_EX);
    }
}

/**
 * Class DbLog
 * @datetime 2020/7/12 6:11 PM
 * @author roach
 * @email jhq0113@163.com
 */
class DbLog extends ILog
{
    /**
     * @var \PDO
     * @datetime 2020/7/12 6:11 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    protected $_pdo;

    /**
     * @param \PDO $db
     * @datetime 2020/7/12 6:12 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function setDb(\PDO $db)
    {
        $this->_pdo = $db;
    }

    /**
     * @param string $level
     * @param string $message
     * @param array  $params
     * @return int
     * @datetime 2020/7/12 6:16 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function log($level, $message, $params = [])
    {
        $message = self::interpolate($message, $params);
        
        $stmt = $this->_pdo->prepare('INSERT INTO `logs`(`datetime`, `level`, `msg`, `clientIp`, `url`, `method`, `host`)VALUES(?,?,?,?,?,?,?)');
        
        $stmt->execute([
            date('Y-m-d H:i:s'),
            $level,
            $message,
            $_SERVER['REMOTE_ADDR'],
            $_SERVER['REQUEST_URI'],
            $_SERVER['REQUEST_METHOD'],
            $_SERVER['HTTP_HOST']
        ]);
        
        return $stmt->rowCount();
    }
}

/**
 * Class LogFactory
 * @datetime 2020/7/12 6:21 PM
 * @author roach
 * @email jhq0113@163.com
 */
class Logger
{
    /**
     * @param ILog   $logger
     * @param string $level
     * @param string $message
     * @param array  $params
     * @return mixed
     * @datetime 2020/7/12 6:32 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function log(ILog $logger, $level, $message, $params = [])
    {
        return $logger->log($level, $message, $params);
    }
}

我们可以看到,在优化过程中,我们增加了ILog抽象类,并增加了抽象方法logLogger类中log方法不再依赖$loggerName,改为依赖抽象类ILog,这样,当我们增加KafkaLog类打日志时,Logger类不需要再做任何修改。

这就是典型的依赖倒置思想。

3.4 接口隔离

我们刚刚学会了依赖倒置,高层模块不应该依赖于低层模块儿,二者应该依赖其抽象。接口隔离告诉我们,这个抽象要有个度,应该建立在最小接口上。

看如下代码

<?php
/**
 * Interface IOrderAndLogin
 * @datetime 2020/7/12 6:45 PM
 * @author roach
 * @email jhq0113@163.com
 */
interface IOrderAndLogin
{
    /**
     * @param array $params
     * @return mixed
     * @datetime 2020/7/12 6:44 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function order($params = []);

    /**
     * @param array $params
     * @return mixed
     * @datetime 2020/7/12 6:46 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function login($params = []);
}

/**
 * Class ProductOrder
 * @datetime 2020/7/12 6:48 PM
 * @author roach
 * @email jhq0113@163.com
 */
class ProductOrder implements IOrderAndLogin
{
    /**
     * Order constructor.
     * @param array $params
     */
    public function order($params = [])
    {
        //各种下单操作
        return true;
    }

    /**
     * @param array $params
     * @return bool|mixed
     * @datetime 2020/7/12 6:48 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function login($params = [])
    {
        return false;
    }
}

/**
 * Class WxLogin
 * @datetime 2020/7/12 6:50 PM
 * @author roach
 * @email jhq0113@163.com
 */
class WxLogin implements IOrderAndLogin
{
    /**
     * @param array $params
     * @return bool|mixed
     * @datetime 2020/7/12 6:49 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function order($params = [])
    {
        return false;
    }

    /**
     * @param array $params
     * @return array|mixed
     * @datetime 2020/7/12 6:50 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function login($params = [])
    {
        // 各种操作
        return [
            'userId' => time(),
            'nickname' => uniqid()
        ];
    }
}

/**
 * Class OrderController
 * @datetime 2020/7/12 6:55 PM
 * @author roach
 * @email jhq0113@163.com
 */
class OrderController
{
    /**
     * @param IOrderAndLogin $order
     * @return mixed
     * @datetime 2020/7/12 6:55 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function orderAction(IOrderAndLogin $order)
    {
        return $order->order([
            'time' => time(),
        ]);
    }
}

以上代码我们可以看到,OrderController通过接口IOrderAndLogin依赖ProductOrder,但是IOrderAndLogin接口还有一个OrderController用不到的接口方法login,同时ProductOrder类根本不需要login方法,同样的,WxLogin类也不需要order方法,但是他们各自都必须去实现不需要的方法,这就不符合接口的单一职责原则。

这种优化思路很简单,把IOrderAndLogin接口拆分成IOrderILogin两个接口,ProductOrder类实现IOrder接口,WxLogin类实现ILogin接口,OrderController类依赖IOrder接口即可,由于代码很简单,不在提供实现细节。

3.5 迪米特法则

迪米特法则简单来说就是高内聚、低耦合,一个对象应该对其他对象保持最少的了解。

从耦合度来讲,继承>依赖,所以当我们使用继承的时候,除非是有明确的父子关系,否则不要乱用继承。

下面举一个简单的高内聚、低耦合的例子

<?php
/**
 * Created by PhpStorm.
 * User: Jiang Haiqiang
 * Date: 2020/7/12
 * Time: 4:09 PM
 */
class RedisCache
{
    /**
     * @var string
     * @datetime 2020/7/12 7:10 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    public $host;

    /**
     * @var int
     * @datetime 2020/7/12 7:10 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    public $port;

    /**
     * @var string
     * @datetime 2020/7/12 7:10 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    public $password;

    /**
     * @var int
     * @datetime 2020/7/12 7:10 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    public $db;

    /**
     * @var \Redis
     * @datetime 2020/7/12 7:11 PM
     * @author roach
     * @email jhq0113@163.com 
     */
    private $_instance;

    /**
     * @return \Redis
     * @datetime 2020/7/12 7:13 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function getRedis()
    {
        if(is_null($this->_instance)) {
            $redis = new \Redis();
            $redis->connect($this->host, $this->port);
            $redis->auth($this->password);
            $redis->select($this->db);
            $this->_instance = $redis;
        }
        return $this->_instance;
    }

    /**
     * @param string $key
     * @return bool|string
     * @datetime 2020/7/12 7:14 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function get($key)
    {
        return $this->getRedis()->get($key);
    }

    /**
     * @param string $key
     * @param string $value
     * @param int $timeout
     * @return bool
     * @datetime 2020/7/12 7:14 PM
     * @author roach
     * @email jhq0113@163.com
     */
    public function set($key, $value, $timeout = 60)
    {
        return $this->getRedis()->set($key, $value, $timeout);
    }
}

以上RedisCache封装类,也许大家在自己的项目中肯定看到过类似的代码,大家会觉得这有什么问题吗?

其实从高内聚低耦合的角度来讲,host,port,passworddb属性以及getRedis方法,对于其他类来说根本不需要知道,本着高内聚低耦合的思想,这些的访问权限至少要控制在protected级别,这样从技术角度避免了泄露和被修改的可能性。

3.6 开闭原则

开闭原则简单来说就是对修改关闭,对扩展开放,此原则理解起来稍复杂,我们可以狭义的这样理解。

如果我们的系统软件已经上线运行了,当发生新需求变化时,我们是通过增加代码实现来满足新需求而不是修改原来的代码来实现新需求,当然这是一个狭义的理解。

我们回头看依赖倒置的例子就是一个符合开闭原则的典型案例,当我们增加KafkaLog需求时,我们是通过增加KafkaLog类实现来满足需求,原有的FileLog类和DbLog类没有发生任何更改,这样就是一个典型的对扩展开放,对修改关闭的一个例子。

好了,设计模式的六大原则我们都通过案例学习完了,你都学会了吗?

学习更多内容: https://404.360tryst.com

我的视频课程: https://edu.csdn.net/course/detail/9933

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苍穹0113

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值