专栏首页网管叨bi叨在程序设计中使用Interface

在程序设计中使用Interface

在PHP和Java中都有Interface的概念,刚接触开发时大家都知道在面向对象中Interface负责定义一些抽象方法来抽象和界定类对象的行为,更有一个“鸭式辩型”理论大概的意思就是使用者并不关心对象的内部是怎么实现的只要你会“呱呱叫(method)”就认为这是一个鸭子对象,但是很多人实际开发的时候并不会去定义Interface,认为多定义这么一层额外增加了工作量并且对程序开发看起来没有明显的增益效果。这篇文章里我就结合着Laravel框架来说一下为什么要使用Interface以及通过Interface给程序在长期维护、团队协作和测试带来收益。

首先在Interface在Laravel框架中被称为契约, 例如我们在介绍用户认证的章节中到的用户看守器契约Illumninate\Contracts\Auth\Guard 和用户提供器契约Illuminate\Contracts\Auth\UserProvider

以及框架自带的 App\User模型所实现的Illuminate\Contracts\Auth\Authenticatable契约。

为什么使用契约

通过上面几个契约的源码文件我们可以看到,Laravel提供的契约是为核心模块定义的一组interface。Laravel为每个契约都提供了相应的实现类,下表列出了Laravel为上面提到的三个契约提供的实现类。

契约

Laravel内核提供的实现类

Illumninate\Contracts\Auth\Guard

Illuminate\Auth\SessionGuard

Illuminate\Contracts\Auth\UserProvider

Illuminate\Auth\EloquentUserProvider

Illuminate\Contracts\Auth\Authenticatable

Illuminate\Foundation\Auth\Authenticatable(User Model的父类)

所以在自己开发的项目中,如果Laravel提供的用户认证系统无法满足需求,你可以根据需求定义看守器和用户提供器的实现类,比如我之前做的项目就是用户认证依赖于公司的员工管理系统的API,所以我就自己写了看守器和用户提供器契约的实现类,让Laravel通过自定义的Guard和UserProvider来完成用户认证。自定义用户认证的方法在介绍用户认证的章节中我们介绍过,读者可以去翻阅那块的文章。

所以Laravel为所有的核心功能都定义契约接口的目的就是为了让开发者能够根据自己项目的需要自己定义实现类,而对于这些接口的消费者(比如:Controller、或者内核提供的 AuthManager这些)他们不需要关心接口提供的方法具体是怎么实现的, 只关心接口的方法能提供什么功能然后去使用这些功能就可以了,我们可以根据需求在必要的时候为接口更换实现类,而消费端不用进行任何改动。

定义和使用契约

上面我们提到的都是Laravel内核提供的契约, 在开发大型项目的时候我们也可以自己在项目中定义契约和实现类,你有可能会觉得自带的Controller、Model两层就已经足够你编写代码了,凭空多出来契约和实现类会让开发变得繁琐。我们先从一个简单的例子出发,考虑下面的代码有什么问题:

class OrderController extends Controller
{
    public function getUserOrders()
    {
        $orders= Order::where('user_id', '=', \Auth::user()->id)->get();
        return View::make('order.index', compact('orders'));
    }
}

这段代码很简单,但我们要想测试这段代码的话就一定会和实际的数据库发生联系。也就是说, ORM和这个控制器有着紧耦合。如果不使用Eloquent ORM,不连接到实际数据库,我们就没办法运行或者测试这段代码。这段代码同时也违背了“关注分离”这个软件设计原则。简单讲:这个控制器知道的太多了。 控制器不需要去了解数据是从哪儿来的,只要知道如何访问就行。控制器也不需要知道这数据是从MySQL或哪儿来的,只需要知道这数据目前是可用的。

Separation Of Concerns 关注分离 Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. 每个类都应该只有单一的职责,并且职责里所有的东西都应该由这个类封装

接下来我们定义一个接口,然后实现该接口

interface OrderRepositoryInterface 
{
    public function userOrders(User $user);
}

class OrderRepository implements OrderRepositoryInterface
{
    public function userOrders(User $user)
    {
        Order::where('user_id', '=', $user->id)->get();
    }
}

将接口的实现绑定到Laravel的服务容器中

App::singleton('OrderRepositoryInterface', 'OrderRespository');

然后我们将该接口的实现注入我们的控制器

class UserController extends Controller
{
    public function __construct(OrderRepositoryInterface $orderRepository)
    {
        $this->orders = $orderRespository;
    }

    public function getUserOrders()
    {
        $orders = $this->orders->userOrders();
        return View::make('order.index', compact('orders'));
    }
}

现在我们的控制器就完全和数据层面无关了。在这里我们的数据可能来自MySQL,MongoDB或者Redis。我们的控制器不知道也不需要知道他们的区别。这样我们就可以独立于数据层来测试Web层了,将来切换存储实现也会很容易。

接口与团队开发

当你的团队在开发大型应用时,不同的部分有着不同的开发速度。比如一个开发人员在开发数据层,另一个开发人员在做控制器层。写控制器的开发者想测试他的控制器,不过数据层开发较慢没法同步测试。那如果两个开发者能先以interface的方式达成协议,后台开发的各种类都遵循这种协议。一旦建立了约定,就算约定还没实现,开发者也可以为这接口写个“假”实现

class DummyOrderRepository implements OrderRepositoryInterface 
{
    public function userOrders(User $user)
    {
        return collect(['Order 1', 'Order 2', 'Order 3']);
    }
}

一旦假实现写好了,就可以被绑定到IoC容器里

App::singleton('OrderRepositoryInterface', 'DummyOrderRepository');

然后这个应用的视图就可以用假数据填充了。接下来一旦后台开发者写完了真正的实现代码,比如叫 RedisOrderRepository。那么使用IoC容器切换接口实现,应用就可以轻易地切换到真正的实现上,整个应用就会使用从Redis读出来的数据了。

接口与测试

建立好接口约定后也更有利于我们在测试时进行Mock

public function testIndexActionBindsUsersFromRepository()
{    
    // Arrange...
    $repository = Mockery::mock('OrderRepositoryInterface');
    $repository->shouldReceive('userOrders')->once()->andReturn(['order1', 'order2']);
    App::instance('OrderRepositoryInterface', $repository);
    // Act...
    $response  = $this->action('GET', 'OrderController@getUserOrders');

    // Assert...
    $this->assertResponseOk();
    $this->assertViewHas('order', ['order1', 'order2']);
 }

总结

接口在程序设计阶段非常有用,在设计阶段与团队讨论完成功能需要制定哪些接口,然后设计出每个接口具体要实现的方法,方法的入参和返回值这些,每个人就可以按照接口的约定来开发自己的模块,遇到还没实现的接口完全可以先定义接口的假实现等到真正的实现开发完成后再进行切换,这样既降低了软件程序结构中上层对下层的耦合也能保证各部分的开发进度不会过度依赖其他部分的完成情况。

本文分享自微信公众号 - 网管叨bi叨(kevin_tech),作者:KevinYan11

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-01-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go Web编程--深入学习解析HTTP请求

    之前这个系列的文章一直在讲用 Go语言怎么编写HTTP服务器来提供服务,如何给服务器配置路由来匹配请求到对应的处理程序,如何添加中间件把一些通用的处理任务从具体...

    KevinYan
  • 《Go 语言程序设计》读书笔记(四)接口

    Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型这是一个接口类型定义如下:

    KevinYan
  • 七张图了解Kubernetes内部的架构

    Kubernetes是用于管理容器化应用程序集群的工具。在计算机领域中,此过程通常称为编排。

    KevinYan
  • 海思HI3559A硬件说明出炉

    天睿视迅
  • 程序员带你学习安卓开发-安卓基础之网络编程 大汇总

    本系列教程致力于可以快速的进行学习安卓开发,按照项目式的方法,通常一篇文章会做一个小程序。提高学习的兴趣。

    做全栈攻城狮
  • 程序员带你学习安卓开发-安卓基础之网络编程 大汇总

    本系列教程致力于可以快速的进行学习安卓开发,按照项目式的方法,通常一篇文章会做一个小程序。提高学习的兴趣。

    做全栈攻城狮
  • 推荐三款我常备开发辅助神器

    五一假期过完了,大家都去哪些地方浪了?上班第一天是不是倍感无趣?哈哈,不要紧,今天我来给大家推荐几个神器,让你明天神清气爽,这可是程序员开发必备之良品呀。

    大愚
  • 记一次批量查看ffmpeg抽帧后的图片(格式为H264)

    当前的公司刚好做一个抽帧的项目,具体是通过抽取本地已经录制保存的TF(SD)卡里的视频文件,以每秒/帧的方式抽取,生成的图片文件格式为H264,如下图所示

    用户6367961
  • 21.SpringCloud实战项目-后台题目类型功能(网关、跨域、路由问题一文搞定)

    用renren-generator自动生成前端代码,可以参考这篇:13.SpringCloud实战项目-自动生成前后端代码

    Jackson0714
  • Android 优化——网络优化

    七适散人

扫码关注云+社区

领取腾讯云代金券