为什么EloquentBuilder不在laravel中继承QueryBuilder ?


Why doesn't EloquentBuilder inherit from QueryBuilder in laravel?

在laravel中,Eloquent'Builder类将每个对它不需要的方法的调用发送给内部Query'Builder。对我来说,这听起来像是继承。有人知道为什么他们没有实现它让Eloquent'Builder扩展Query'Builder吗?我第一次注意到它的原因是我在ide中得到了"Call to undefined method"错误,尽管代码工作得很好,我想这是魔法方法的诅咒。

供参考,这里是Eloquent'Builder的相关来源。

/**
 * The base query builder instance.
 *
 * @var 'Illuminate'Database'Query'Builder
 */
protected $query;
protected $passthru = array(
    'toSql', 'lists', 'insert', 'insertGetId', 'pluck', 'count',
    'min', 'max', 'avg', 'sum', 'exists', 'getBindings',
);
public function __call($method, $parameters)
{
    if (method_exists($this->model, $scope = 'scope'.ucfirst($method)))
    {
        return $this->callScope($scope, $parameters);
    }
    else
    {
        $result = call_user_func_array(array($this->query, $method), $parameters);
    }
    return in_array($method, $this->passthru) ? $result : $this;
}

我将从结论开始。我认为它只是设计得很差。

让我做一些解释,并回复@Simon Bengtsson的公认答案

我绝不是想冒犯任何人,我只是想把我的想法说出来。


实际上,继承的全部要点是它们具有相似的特征,但是您希望添加额外的层来放置一些扩展的特性。显然,扩展后的"子"类将比"父"类知道的更多(比如了解Eloquent Model)

对我来说,"建设者"就是建设者。毕竟,两者实际上都在构建查询,而不管它检查或观察的是什么条件。这就是Builder的目的(至少在我看来是这样)。所以它在同一层(除了Eloquent'Builder有一些额外的特征)。

当前的实现重写了一些方法的性质,例如where()(基本上是Query'Builder中的那些,相同的名称,相同的参数)。它还添加了一些方法,这些方法最终会调用where()等方法。这都是关于继承的。如果我调用where(),并且它存在于子类中,则调用子类方法。如果没有,它将调用父类中的方法。


如果你假设你想使用一些新的NoSQL数据库来代替,那么将底层和ORM级组件解耦将变得更加困难并简单地编写一个Query'Builder签名兼容的插入类。

@Simon Bengtsson

我看不出解耦有什么困难。您所要做的就是编写一个新的类Query'MongoDBBuilder,它具有与Query'Builder相同的接口。如果你担心,你可以让这些类"实现"一个接口。如果你问我,实际上目前的方法更难解耦,因为现在"哪些功能被覆盖,哪些不被覆盖"是相当混乱的。

在某些情况下,我确实鼓励类解耦,但如果我要解耦这个(我认为这里没有必要),我会这样做:

  • Eloquent'Model
  • Eloquent'Adapter(这是某种中间层,你可以把连接设置等)
  • Eloquent'Builder extends Query'Builder(这是查询'生成器的"包装器",以便它可以做得更多,但实现相同的目标)

子类也可以访问父类上受保护的属性,这样Eloquent'ChildOfQueryBuilder就可以自由地依赖于底层实现Query'Builder的属性,并可以耦合到它。

@Simon Bengtsson

这就是继承的全部意义。你将能够访问受保护的属性来覆盖父属性给出的特性。让我们从另一个角度来看,如果您需要访问这些变量以修改功能,该怎么办?实际上,将这些成员声明为"protected"的作者已经暗示他已经为另一个类扩展它做好了准备。这是让它"受保护"的唯一目的,对吗?

这就是为什么你必须在继承父类之前确切地知道它在做什么,因为它可能是危险的。


我给出这么多解释的原因是因为我想修改Eloquent的一些功能。我一直在看天的实现(3个神类:Eloquent'Model, Eloquent'BuilderQuery'Builder)。图层和混叠真的很混乱。扩展Model类需要我重新声明几乎相等的函数,因为它的实现很差。但是这离题了。

总之,如果你问"为什么",我实际上会说(现在我知道人们会踢我),这只是糟糕的设计和实现。

我认为delmadord正在走向正确的答案,但我认为将IoC纳入其中模糊了问题。基本的答案是,这两个类代表了数据库在不同层次上的抽象。Query'Builder是较低层次的抽象,Eloquent'Builder是较高层次的抽象,这就是为什么,正如你所指出的,它使用Query'Builder作为其核心功能。

您可以从为每个类定义的属性中看出这一点。一个是围绕着诸如

这样的内容的属性
  • 连接设置,
  • 数据库语法和
  • 与将要组装的查询相关的各种东西

另一个是ORM,关注的是更高层次的问题,比如是否应该使用渴望关系加载,查询当前绑定到哪个模型。可以说,您可以在这里使用继承,并且可能具有几乎相同的功能,但从架构的角度来看,这存在问题。

一个是,严格地说,我应该总是能够使用类来替代类。如果对继承采用""的读法,就更容易理解了。这意味着,如果您对这对类使用继承,严格地说,任何开发人员都能够像使用Query'Builder一样轻松地使用Eloquent'Builder,并且实际上可能会鼓励在整个代码中这样做,以便在他们的脑海中少跟踪一个类。这将意味着代码可以调用$builder->getRelation()并依赖于(为了论证的缘故)$builder->wheres的结果,这些结果都在同一代码块中。

如果你假设想要使用一些新的NoSQL数据库,而只是简单地编写一个符合Query'Builder签名的插入类,那么将低级和ORM级组件解耦就变得更加困难了。

子类也可以访问父类上受保护的属性,这样Eloquent'ChildOfQueryBuilder就可以自由地依赖于Query'Builder的低层实现,并且可以耦合到它。你可能会争辩说,你可以让私有,但这样你就可以更长时间地为Query'Builder编写一个薄的低级扩展(可能添加一些集群,分片或安全逻辑),这可能(正确地)需要访问实现细节,这可能是低级功能的替代。

另一个是,如果你把所有相关的东西堆成一个"上帝对象",类的大小会迅速增长。这两个Builder类都有大量特定于抽象层的逻辑。如果您继承,那么Eloquent'Builder将有效地成为两者的联合,您将有几乎两倍的代码来扫描问题,您的IDE将为您提供更广泛的自动完成选项,其中很大一部分与您正在工作的抽象深度无关。


同样的模式有时在其他地方使用,其中两个类彼此非常相关,但是继承会导致感知问题,这将鼓励以开发人员不希望的方式使用它们。以Java为例,不可变类通常具有可变对应类,但两者都不继承对方,甚至可能没有共同的祖先。

这是因为不可变类不能有任何mutator方法。从可变版本继承将需要重写和禁用每个mutator方法,并永远确保正确发生。忘记这一点,你的不可变类可能会在几次修改中可变。你不能允许继承其他的方式,因为这样做意味着一个方法期望一个不可变的类可能会得到一个可变的,并增加白发的人。

这是另一个相关事物扮演不同角色并因此在概念上分离的例子。这并不是说你不能用一种特殊的方式来做事情,而是某些架构的选择会阻止你不想要的行为,并为你想要做的事情留下空间。有时对于非常大的代码库,沿着这样的接口解耦确实有助于使其更容易推理和使用。

如果你看一下Illuminate/Database/Eloquent/Builder的源码

<?php namespace Illuminate'Database'Eloquent;
use Closure;
use Illuminate'Database'Query'Expression;
use Illuminate'Database'Eloquent'Relations'Relation;
use Illuminate'Database'Query'Builder as QueryBuilder;
class Builder {
    /**
     * The base query builder instance.
     *
     * @var 'Illuminate'Database'Query'Builder
     */
    protected $query;

你可以看到,它正在使用实例的照亮'数据库'查询'生成器。它不是扩展类,可能是为了利用Laravel中的IoC。