laravel 依赖注入 接口设计

假设我现在需要做一个支付服务,那么我先设计一个接口

interface PayInterface{
    public function pay(Order $order) : string;
}

然后实现这个接口

class WeiXinPay implements PayInterface{
    public function pay(Order $order) :string{
        //具体实现
    }
}

开始发现一个问题
微信支付是需要三个关键参数的 (appID , appSecret , key)
我就接着修改代码,我希望这三个参数是通过外部注入的,而不是写死在WeiXinPay里面,于是乎我就修改了WeiXinPay的构造函数,并新增了一个接口与实现

interface PaySettingsInterface {
    public function getSettings() : array;
}

class WeixinPaySettings implements PaySettingsInterface{
    public function getSettings() : array{
        return [
            'app_id' => 'xxxx',
            'app_secret' => 'yyyyy',
            'key' => 'zzzz'
        ];
    }
}

class WeiXinPay implements PayInterface{

    protected $settings;
    public __construct(PaySettingsInterface $settings){
        $this->settings = $settings->getSettings();
    }

    public function pay(Order $order) :string{
        //具体实现
    }
}

好了。感觉到这里这个PayInterface的实现应该是没问题了。我开始写服务提供者

    $this->app->bind(PaySettingsInterface::class,WeiXinPaySettings::class);
    $this->app->bind(PayInterface::class , WeiXinPay::class);

写一段测试代码来跑跑看

    public function testPay(){
        $orderSn = Strings::randomString('NUMBER+',12);
        $order = factory(Order::class)->make([
            'sn' => $orderSn,
            'uid' => 111,
            'to_uid' => 109,
            'relation_user' => json_encode([109,108,107,104,102,12]),
            'amount' => 1,
            'attach' => 1,
            'status' => Constans::ORDER_NO_PAY,
            'is_settle' => Constans::NO_SETTLE,
        ]);
        
        /**
         * @var $service PayInterface
        */
        $service = $this->app->make(PayInterface::class);
        
        $res = $service->pay($order);
        $this->assertIsString($res);
    }

没有问题,一切都如预期一样。(未来我也可以很容置的将微信支付换成支付宝,只需要在服务提供者切换实现即可)

过了两天,又有一个新的需求了。终极问题来了,老板希望每一次支付的时候收款人都不一样,也就是说微信支付的appId,appSecret,appKey每次都不一样

我开始修改我的代码,我想着:我让这些有变动的参数通过构造函数的方式传递进来总可以吧。

interface PaySettingsInterface {
    public function getSettings() : array;
}

class WeixinPaySettings implements PaySettingsInterface{
    protected $appID;
    protected $appKey;
    protected $appSecret;

    public function __construct($appID ,$appKey ,$appSecret){
        $this->appID = $appID;
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
    }

    public function getSettings() : array{
        return [
            'app_id' => $this->appID,
            'app_secret' => $this->appSecret,
            'key' => $this->appKey
        ];
    }
}

class WeiXinPay implements PayInterface{

    protected $settings;
    public __construct(PaySettingsInterface $settings){
        $this->settings = $settings->getSettings();
    }

    public function pay(Order $order) :string{
        //具体实现
    }
}

//然后我修改我的服务提供者
$this->app->bind(PaySettingsInterface::class,function($app){
    //怎么new 呢? 老板的需求是可能每次都不同,这些数据又可能来自数据库,也可能来自缓存。
    $instance = new WeixinPaySettings(???);
    return $instance;
});
$this->app->bind(PayInterface::class , WeiXinPay::class);
//到这里,看来是无法通过容器自动注入PaySettingsInterface的实现了。那么我就只能这样了。在测试代码中:

public function testPay(){
    $orderSn = Strings::randomString('NUMBER+',12);
    $order = factory(Order::class)->make([
        'sn' => $orderSn,
        'uid' => 111,
        'to_uid' => 109,
        'relation_user' => json_encode([109,108,107,104,102,12]),
        'amount' => 1,
        'attach' => 1,
        'status' => Constans::ORDER_NO_PAY,
        'is_settle' => Constans::NO_SETTLE,
    ]);
    
    //查询数据库得到settings
    $settings = Db::get();
    $paySettings  = new WeixinPaySettings($settings['app_id'],$settings['app_secret'],$settings['app_key']);
    
    $payService = new WeixinPay($paySettings);
    
    $res = $payService->pay($order);
    $this->assertIsString($res);
}

这样看起来也可以,但是我困扰了

  1. 我没有办法简单的替换支付方式了 (WeixinPay 替换成 AliPay)
  2. 调用方手动的去new 相关的实现,产生了严重的依赖。

我希望能够:

  1. 能够简单的替换支付方式(服务提供者)
  2. 调用方只需要 调用 pay(Order $order) 方法即可完成支付,即使我切换支付对于调用方来说都是不需要改变的,符合里氏替换规则

求破

阅读 3.6k
1 个回答

看代码感觉你是为了OOP而OOP
OOP很重要的一点是需要从上到下进行设计

你的需求能够确定的是:

  1. 有个PayInterface,只有一个pay方法,接收订单参数
  2. 需要根据不同的支付方式选择不同的PayInterface实现类

所以建议的设计模式是工厂模式

interface PayInterface {
    function pay(Order $order);
}

class WechatPay implements PayInterface {
    private $appId;
    private $secretKey;
    private $mchId;
    private $mchKey;
    
    function __construct(array $setting) {
        // 赋值
    }
    
    function pay(Order $order) {
        // 支付处理
    }
}

class Alipay implements PayInterface {
    private $key;
    function __construct(array $setting) {
        // 赋值
    }
    function pay(Order $order) {
        // 订单处理
    }
}

class PayFactory {
    public static function getChannel($type, array $setting) {
        if($type == 'wechat') {
            return new WechatPay($setting);
        } elseif($type == 'alipay') {
           return new Alipay($setting);
       } 
    }
}

外部调用只依赖PayInterface接口,如果需要实例,请使用工厂静态方法返回
如果需要加支付类型,直接新建一个class继承支付接口(这是加了文件,叫扩展,这是允许的),然后更改PayFactory的判断(这个修改无法避免,等下会讲到避免的方法)

总之就是一句如果需求有修改,你的代码肯定需要修改,只不过我们不修改已经实现好的代码


工厂模式避免if

class PayFactory {
    const PAY_WECHAT = 1;
    const PAY_ALI = 2;
    const HANDLERS = [
       self::PAY_WECHAT => WechatPay::class,
       self::PAY_ALI => Alipay::class
    ];
    
    public static function getChannel($type,$setting) {
        if(!isset(self::HANDLERS[$type])) {
            throw new Exception('支付渠道不存在');
        }
        return new self::HANDLERS[$type]($setting);
    }
}
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题