ThinkPHP 面试精讲:Repository 模式与模型解耦

在 ThinkPHP 的中高级面试中,一旦聊到项目架构与代码分层,“Repository 模式”和“模型解耦”几乎是必考题。很多候选人能说出“用 Repository 包一层 Model”,但再往下追问“为什么要包”“怎么包才算解耦”“和直接调模型比到底解决了什么问题”,回答就开始模糊。这篇文章从面试实战角度,把 Repository 模式在 ThinkPHP 中的定位、实现与取舍讲清楚。

面试官为什么爱问 Repository

先看一个典型的“坏味道”代码。在 Controller 里直接写:

public function index()
{
    $list = User::where('status', 1)
        ->where('vip_level', '>', 2)
        ->order('create_time', 'desc')
        ->paginate(20);
    return view('user/index', ['list' => $list]);
}

这段代码在小型项目里没问题,但业务一复杂就会暴露三个问题:查询逻辑散落在各个 Controller,改一个筛选条件要全局搜索;Model 既承担数据映射,又承担业务查询,职责过载;单元测试时无法脱离数据库替换数据源。

Repository 模式的核心思路,就是在业务层和数据访问层之间加一个“仓储”抽象:业务代码只依赖 Repository 接口,不关心底层是 MySQL、Redis 还是远程 API。这样查询逻辑集中、Model 回归数据映射本职、测试可以注入假实现。

面试时你可以用一句话概括:Repository 解决的是“业务逻辑与数据访问的耦合”,而不是“让代码看起来更高级”。

Repository 与 Model 的职责边界

这是面试中最容易被追问的点。很多人的实现只是把 Model 的方法原样转发一遍,比如 UserRepository::find($id) 内部直接 return User::find($id),这叫“伪 Repository”,没有产生任何解耦价值。

正确的职责划分应该是:

  • Model:负责数据表映射、字段类型转换、关联定义、获取器和修改器。它回答的是“这条数据长什么样”。
  • Repository:负责查询条件的组装、复杂 SQL 的封装、缓存策略、多数据源切换。它回答的是“业务需要哪些数据,怎么取”。
  • Service:负责业务规则编排,比如下单要扣库存、写订单、发通知,它调用多个 Repository 完成一次业务动作。

举个有说服力的例子。假设“获取活跃 VIP 用户”这个需求,条件可能随运营策略变化。把它放进 Repository:

interface UserRepositoryInterface
{
    public function getActiveVipUsers(int $level, int $limit): array;
}

class UserRepository implements UserRepositoryInterface
{
    public function getActiveVipUsers(int $level, int $limit): array
    {
        return UserModel::where('status', 1)
            ->where('vip_level', '>=', $level)
            ->where('last_login_time', '>', time() - 86400 * 30)
            ->order('vip_level', 'desc')
            ->limit($limit)
            ->select()
            ->toArray();
    }
}

业务层只调用 getActiveVipUsers(3, 50),完全不知道背后用了哪些 where 条件。运营策略调整时,只改这一个方法。

在 ThinkPHP 中落地 Repository

ThinkPHP 没有 Laravel 那样开箱即用的容器绑定约定,但它的容器和依赖注入足够支撑这套模式。推荐的做法是“接口 + 实现 + 容器绑定”。

第一步,定义接口,放在 app/repository/contract 目录。接口的意义在于让业务层依赖抽象,而不是具体类。

第二步,实现类放在 app/repository 目录,内部可以自由使用 ThinkPHP 的 Db 门面或 Model。

第三步,在 app/provider.php 或自定义服务提供者中绑定:

// app/provider.php
use app\repository\contract\UserRepositoryInterface;
use app\repository\UserRepository;

return [
    UserRepositoryInterface::class => UserRepository::class,
];

之后在 Controller 或 Service 中通过构造函数注入:

class UserController extends BaseController
{
    public function __construct(
        protected UserRepositoryInterface $userRepo
    ) {
        parent::__construct();
    }

    public function index()
    {
        $list = $this->userRepo->getActiveVipUsers(3, 20);
        return view('user/index', ['list' => $list]);
    }
}

面试官如果追问“ThinkPHP 的容器怎么知道注入哪个实现”,你就答:通过 provider.php 的绑定映射,容器在解析 UserRepositoryInterface 时会实例化 UserRepository。这也是“依赖倒置”在框架里的具体体现。

常见追问与避坑

追问一:Repository 会不会让代码变啰嗦?

会,而且这是真实的成本。所以判断标准是“业务复杂度是否值得”。简单 CRUD 项目直接调 Model 完全合理,强行上 Repository 是过度设计。面试时主动说出这一点,比一味吹捧模式更加分。

追问二:Repository 和 Service 怎么区分?

一句话:Service 管“做什么业务”,Repository 管“取什么数据”。Service 可以调用多个 Repository,Repository 不应该调用 Service,否则就形成循环依赖。

追问三:事务放在哪一层?

事务属于业务编排,应该放在 Service 层。Repository 只负责单次数据访问,不感知跨表事务边界。这一点答错会暴露对分层理解不深。

追问四:返回数组还是模型对象?

建议 Repository 对外返回数组或自定义 DTO,而不是直接返回 Model 对象。因为一旦业务层拿到 Model,就可能顺手调用它的关联和查询方法,解耦就破功了。返回纯数据结构,边界更干净。

总结

Repository 模式在 ThinkPHP 面试中的价值,不在于你是否会写一个 UserRepository 类,而在于你是否理解“依赖抽象而非实现”的设计原则。回答时抓住三点:职责边界清晰、通过接口和容器实现依赖注入、根据业务复杂度决定是否引入。能把“什么时候不该用”也讲明白,才是真正吃透了这道题。

未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:Repository 模式与模型解耦

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏