我放弃使用 Laravel 的原因:太多 Magic!
接下来通过本文将告诉你,我为什么改变我所使用的工具!
首先,我想确保你知道我的意思,关于 Laravel 我不想乱说为什么选择其他更好的框架。
本文是我的个人看法,带有很强的主观意识。我会告诉你我的想法,并尝试让你重新思考如何更好的选择框架。当你重新评估后坚持使用 Laravel,这也没有问题。我不是想让大家从 Laravel 转移到其他框架或语言。但更重要的是要仔细考虑并确保你知道你正在使用什么并且为什么使用它。
开场简介
我已经在工作中使用Laravel大概2年了。我非常喜欢Laravel构建应用程序的便捷性。它提供了很多开箱即用的有用的工具。Artisan 命令行可以帮助你快速开发,他可以帮助你生成身份验证类和脚手架等等。
但是随着你的项目越来越大,越来越复杂,Laravel的开发也会随着变得很复杂。或者我换个说法,在项目变复杂后Laravel会不适合你的开发,相对应你会发现有更好的工具来辅助你的开发。这个也不是Laravel的错,绝大部分原因是PHP设计的问题。
好,让我们开始正文。
Eloquent ORM
如果你用过 Laravel 那么你肯定知道 Eloquent。一个有着许多简洁功能且被默认安装的 ORM. 但是 Eloquent 的设计使得应用不必要的复杂,同时 IDE 也没有办法正确的分析代码。
这部分归咎于 Eloquent 使用的 Active Record ORM 设计模式, 它试图让开发人员写更少的代码。为了做到这点, 它允许开发者往一个 Model 里面填充很多原本不该存在的内容。
满是好意, 但是我越来越不喜欢这样。
一起来看下面这个例子:
<?php
class User extends Model
{
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
}
能看到这个 model 里面 没有属性 . 这看起来也没什么但是对我来说这很重要。所有的属性都是通过读取数据表的元信息然后 “魔法般的” 注入到 class 当中。当然, IDE 在没有安装辅助插件的时候也没法正确理解这段代码。而且你也不能给数据表中的字段取一个不同的属性名称。
现在让我们来查看 scopes 方法。对于 Laravel 的用户来说,它的功能是非常清晰的。如果我调用这个方法,它会通过添加给定的 WHERE 子句来限定底层SQL查询。
你可以看到它并不是一个静态的方法。 这意味着这个方法对类的特定对象进行了操作。但是在这个例子中,它没有这么做。在查询构造器上调用了一个 scopes 方法。 它与模型对象本身并没有什么关系。我将解释通常在你调用 scopes 方法之后所发生的事:
<?php
namespace App\Http\Controllers;
use App\User;
class ExampleController extends Controller
{
public function showPopularUsers($id)
{
return view('users', ['users' => User::popular()->get()]);
}
}
你正在调用了一个并没有被定义的静态方法 popular()
。但由于 Laravel 定义了一个 __call()
和 __callStatic()
方法,通过他们的处理。将这些方法调用传递到查询构造器。
这不仅仅使你的 IDE 无法理解。而且还会让重构变得更难,使新加入的开发者感到困惑,以及 静态分析 也变得更加麻烦。
除此之外,当你在模型中写入更多的方法之后,你正在偏离SOLID原则中的“单一原则”。如果有不懂的,下面为大家解释一下什么是 SOLID 原则。
- S 单一功能原则 认为对象应该仅具有一种单一功能的概念。
- O 开闭原则 认为“软件体应该是对于扩展开放的,但是对于修改封闭的”的概念。
- L 里氏替换原则 认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念。
- I 接口隔离原则 认为“多个特定客户端接口要好于一个宽泛用途的接口” 的概念。
- D 依赖反转原则 认为一个方法应该遵从“依赖于抽象而不是一个实例” 的概念。
当你使用 Eloquent 的时候,你的模型拥有多种职责。它可以用来从数据库获取数据,这是它应当有的功能。但是它还拥有过滤功能,或者排序功能。你不会想要那样的。
全局辅助函数
Laravel 提供了很多的全局辅助函数。它们看起来非常的方便,是的,他们非常方便。
你只需要知道当你牺牲了你的独立性的时候,你的全局命名空间就会受到污染。它很少会导致冲突出现,但首选的是应该是完全避免。
让我们瞧瞧几个例子。以下是我们拥有但并不需要的三种辅助方法的列表,可事实上我们有更好的选择:
app_path()
— 啥?如果你需要知道 app 的路径,你应该去询问 app 对象,你可以从类型提示中获取它。app()
— 嗯?我们并不需要这个方法。我们可以注入 app 实例!collect()
— 这将创建一个新的 Collection 类的实例,但我们自己就可以去创建一个新的对象。
一个更具体的例子:
<?php
namespace App\Http\Controllers;
use App\Example;
class ExampleController extends Controller
{
public function createExampleObject()
{
return Example::create(request()->all());
}
}
我们使用 Laravel 的全局 request()
辅助函数来检索 POST 数据并将其作为属性,放入到我们的模型中。
我们可以在控制器方法中使用 Request
对象作为参数,而不是使用全局 request()
。 Laravel 知道如何向我们提供所需的数据。 他会帮我们处理妥当,我们无需关心如何进行的。
我们可以更进一步解决这个问题。 Laravel 符合 PSR-7 标准。 因此,您也可以键入提示,而不是提示请求对象的类型 ServerRequestInterface
。 这允许您用符合 PSR-7
的任何东西替换整个框架。 此方法中的所有内容都将继续有效。 如果您仍在使用辅助方法,则会失败。 新框架不会带有辅助方法,因此,您必须重写代码的这一部分。
你很少切换整个框架,但是有人会这样做。即使你可能永远不会切换框架,(框架之间的)互用性也是很重要的。相比于从外部解析和请求依赖的内部数据,能够注入依赖并且拥有简洁的数据流才是正确的做法。掌握这种技巧,能够使测试、重构等几乎所有事情都变得简单。
当我看到 Laravel 5.8 将字符串和数组辅助函数从核心中移除时我很欣慰,这是很好的改进。但与此同时,文档应该也让用户谨慎使用所有的辅助函数。
Facades
最后一部分的论点也在这里发挥作用。Facade
似乎是一个很好的工具,可以快速访问一些非静态的方法。但他们再一次将你绑在框架上。您可以使用它们手动解析依赖关系,而不是指示环境提供它们。
通过魔术方法传递一切,复杂性也是如此。
由于我们讨论的是IDE支持,我知道有些人可能会将我引导到 barryvdh 的 IDE helper package。我已经知道这个包了。但为什么需要呢?因为 Laravel 中的一些设计决策并不好。有些框架你不需要它。以 Symfony 为例。不需要 IDE 帮助文件,因为它设计和实现得很好。
在前面的例子中,我们同样也可以使用依赖注入来取代 Facade。这样一来,我们会获得一个真正的对象,然后调用该对象中真正的方法。这样要好多了。
我会再举个例子:
<?php
namespace App\Http;
use App\Example;
use Request;
use Response;
class ExampleController extends Controller
{
public function store()
{
$validatedData = Request::validate(['some_attribute' => 'required']);
$example = Example::create($validatedData);
return Response::view('example', ['new_example' => $example]);
}
}
改写上面的代码很容易。我们让 Laravel 给控制器注入一个 ResponseFactory
依赖,并在 store 方法中传入当前的请求:
<?php
namespace App\Http;
use App\Example;
use Illuminate\Http\Request;
use Illuminate\Contracts\Routing\ResponseFactory;
class ExampleController extends Controller
{
private $responseFactory;
public function __construct(ResponseFactory $responseFactory)
{
$this->responseFactory = $responseFactory;
}
public function store(Request $request)
{
$validatedData = $request->validate(['some_attribute' => 'required']);
$example = Example::create($validatedData);
return $this->responseFactory->view('example', ['new_example' => $example]);
}
}
到目前为止,我们成功地从控制器中移除了 Facade 的使用。代码看上去依旧简洁紧凑,嗯,即使不比之前的更好。既然我们的控制器都要继承基类的Controller
,那么不如进一步把 ResponseFactory
直接注入到父类当中去,刚好其他的控制器也需要它。
我听过一些人以“构造函数传入太多参数啦”作为论据来批评注入所有依赖。对于这种观点我不以为然。使用门面模式只是把依赖和复杂度给隐藏掉罢了。不过,如果你还是不喜欢构造函数有10到20个形参,好吧,就当你对吧。
其实解决这个问题不需要靠Laravel的神奇“魔法”(指门面)。一个类需要太多依赖的根源很可能在于这个类被设计得有太多的东西要去做了。你需要做的不是隐藏复杂度,而是重构这个类,通过把职责分到更小的类里面来改善应用的架构。
一个有趣的事实:在四人帮的设计模式这本书中的确是有门面这个设计模式的。不过这与Laravel的有完全不用的含义。Laravel的各种门面本质上是静态的Service Locators模式。但Laravel所谓的门面没有传达出这个信息。于是项目中关于架构的讨论变得更困难了,因为同一个术语被用来指代不用的设计理念,而参与各方对这个术语的认识是大相径庭的。
结论
就这样吧。我可能很快会写一篇关于我现在更喜欢使用哪些技术的后续文章。但现在,让我总结一下我们所学到的:
Laravel使一切方法尽可能简单是好的。但当你的应用程序变得更复杂时,你就很难与之相处了。我更喜欢出色的IDE支持、更强大的类型、真实的对象和良好的工程。当我想写一个更小的应用程序时,我甚至可能会回到Laravel。
我的很多观点不仅仅只是Laravel的错。我可以交换我不喜欢的部分,例如ORM。但是,我将切换工具箱,在那里默认值更适合我的需要。我认为使用一个框架没有任何意义,在这个框架中,我必须花更多的时间来避免它为糟糕的工程设置的陷阱,而不是开发我的应用程序。其他框架和工具都有更好的设计默认值和更少的魔力。
所以现在,我要和Laravel说再见
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: