在 laravel 个外观上使用依赖注入

Using dependency injection over laravel facades

我读过一些消息来源暗示 laravel facade 最终是为了方便而存在的,这些 classes 应该是 injected to allow loose coupling. Even Taylor Otwell has a post explaining how to do this. It seems I am not the only one to wonder this.

use Redirect;

class Example class
{
    public function example()
    {
         return Redirect::route("route.name");
    }
}

会变成

use Illuminate\Routing\Redirector as Redirect;

class Example class
{
    protected $redirect;

    public function __constructor(Redirect $redirect)
    {
        $this->redirect = $redirect
    }

    public function example()
    {
         return $this->redirect->route("route.name");
    }
}

这很好,只是我开始发现一些构造函数和方法开始采用四个以上的参数。

因为 Laravel IoC 似乎 只注入 class 构造函数和某些方法(控制器),即使我有相当精简的功能和 classes,我发现 classes 的构造函数被挤满了所需的 classes,然后注入到所需的方法中。

现在我发现,如果我继续采用这种方法,我将需要我自己的 IoC 容器,如果我使用 laravel?

这样的框架,这感觉就像是重新发明轮子

例如,我使用服务来控制业务/视图逻辑,而不是处理它们的控制器——它们只是路由视图。所以控制器会首先取其对应的service,然后是url中的parameter。一个服务功能还需要检查表单中的值,因此我需要 RequestValidator。就这样,我有四个参数。

// MyServiceInterface is binded using the laravel container
use Interfaces\MyServiceInterface;
use Illuminate\Http\Request;
use Illuminate\Validation\Factory as Validator;

...

public function exampleController(MyServiceInterface $my_service, Request $request, Validator $validator, $user_id) 
{ 
    // Call some method in the service to do complex validation
    $validation = $my_service->doValidation($request, $validator);

    // Also return the view information
    $viewinfo = $my_service->getViewInfo($user_id);

    if ($validation === 'ok') {
        return view("some_view", ['view_info'=>$viewinfo]);
    } else {
        return view("another_view", ['view_info'=>$viewinfo]);
    }
}

这是一个例子。实际上,我的许多构造函数已经注入了多个 classes(模型、服务、参数、外观)。我已经开始 'offload' 构造函数注入(如果适用)到方法注入,并让调用这些方法的 classes 使用它们的构造函数来注入依赖项。

有人告诉我,根据经验,一个方法或 class 构造函数的参数超过四个是不好的做法/代码味道。但是,如果您选择注入 laravel 门面的路径,我看不出如何真正避免这种情况。

我是不是理解错了?我的 classes / 功能不够精简吗?我错过了 laravels 容器的要点,还是我真的需要考虑创建自己的 IoC 容器? Some others answers 似乎暗示 laravel 容器能够解决我的问题?

也就是说,似乎没有就此问题达成明确的共识...

Class 方法构成 Laravel 中路由机制的一部分(中间件、控制器等)also have their type-hints used to inject dependencies - they don't all need to be injected in the constructor. This may help to keep your constructor slim, even though I'm not familiar with any four parameter limit rule of thumb; PSR-2 allows for the method definition to be stretched over multiple lines 大概是因为需要四个以上的参数并不少见。

在您的示例中,您可以在构造函数中注入 RequestValidator 服务作为折衷方案,因为它们通常被不止一种方法使用。

至于建立共识 - Laravel 必须更加自以为是才能使应用程序足够相似以使用 one-size-fits-all 方法。不过,一个更简单的说法是,我认为在未来的版本中,立面会像渡渡鸟一样。

与其说是一个答案,不如说是在与提出了一些非常有效的观点的同事交谈后的一些思考;

  1. 如果 laravel 的内部结构在版本之间发生了变化(这显然在过去发生过),注入已解析的外观 class 路径会破坏升级时的一切- 在大多数情况下(如果不是完全)使用默认外观和辅助方法可以避免这个问题。

  2. 虽然解耦代码通常是一件好事,但注入这些已解析外观 class 路径的开销会使 classes 变得混乱 - 对于接管项目的开发人员,更多时间花费在尝试遵循本可以更好地用于修复错误或测试的代码上。新开发人员必须记住哪些注入的 class 是开发人员,哪些是 laravel。不熟悉 laravel 的开发人员必须花时间查找 API。最终引入错误或缺少关键功能的可能性会增加。

  3. 开发速度变慢,可测试性并没有真正提高,因为外观已经可以测试了。首先,快速开发是使用 laravel 的强项。时间总是一个限制。

  4. 大多数其他项目使用 laravel 外观。大多数有 laravel 使用经验的人都使用外观。创建一个不遵循以前项目的现有趋势的项目通常会减慢速度。未来缺乏经验(或懒惰!)的开发人员可能会忽略门面注入,并且项目可能会以混合格式结束。 (即使代码审查员也是人)

嗯,你的想法和担忧是正确的,我也有。 Facades 有一些好处(我一般不使用它们),但如果你只使用它们,我建议只在控制器中使用它们,因为控制器至少对我来说只是入口和出口点。

对于您给出的示例,我将展示我通常如何处理它:

// MyServiceInterface is binded using the laravel container
use Interfaces\MyServiceInterface;
use Illuminate\Http\Request;
use Illuminate\Validation\Factory as Validator;

...
class ExampleController {

    protected $request;

    public function __constructor(Request $request) {
        // Do this if all/most your methods need the Request
        $this->request = $request;
    }

    public function exampleController(MyServiceInterface $my_service, Validator $validator, $user_id) 
    { 
        // I do my validation inside the service I use,
        // the controller for me is just a funnel for sending the data
        // and returning response

        //now I call the service, that handle the "business"
        //he makes validation and fails if data is not valid
        //or continues to return the result

        try {
            $viewinfo = $my_service->getViewInfo($user_id);
            return view("some_view", ['view_info'=>$viewinfo]);
        } catch (ValidationException $ex) {
            return view("another_view", ['view_info'=>$viewinfo]);
        }
    }
}



class MyService implements MyServiceInterface {

    protected $validator;

    public function __constructor(Validator $validator) {
        $this->validator = $validator;
    }

    public function getViewInfo($user_id, $data) 
    { 

        $this->validator->validate($data, $rules);
        if  ($this->validator->fails()) {
            //this is not the exact syntax, but the idea is to throw an exception
            //with the errors inside
            throw new ValidationException($this->validator);
        }

        echo "doing stuff here with $data";
        return "magic";
    }
}

请记住将您的代码分解成小的独立片段,每个片段都有自己的职责。 当你正确地破坏你的代码时,在大多数情况下你不会有那么多的构造函数参数,并且代码将很容易测试和模拟。

最后一点,如果您正在构建一个小型应用程序,甚至是大型应用程序中的一个页面,例如 "contact page" 和 "contact page submit",您肯定可以在控制器中使用外观来完成所有操作,这完全取决于项目的复杂程度。

我喜欢 laravel,因为它很漂亮 architecture.Now 从我的方法来看,我不会将所有外观都注入到控制器方法中,为什么?仅在控制器错误的做法中注入重定向外观,因为它可能需要在其他方面。主要是最常用的东西应该全部声明,而对于那些使用一些的人或者只有最好的做法是通过方法注入它们,因为当你在顶部声明时它会妨碍你的内存优化以及你的速度代码。希望这会有所帮助

外观的一个问题是在进行自动化单元测试时必须编写额外的代码来支持它们。

至于解决方案:

1.手动解决依赖关系

一种解决依赖关系的方法,如果您不想通过。构造函数或方法注入,就是直接调用app():

/* @var $email_services App\Contracts\EmailServicesContract
$email_services = app('App\Contracts\EmailServicesContract');

2。重构

有时当我发现自己将太多服务或依赖项传递给 class 时,可能我违反了单一职责原则。在这些情况下,可能需要重新设计,将服务或依赖项分解成更小的 classes。我会使用另一个服务来包装一组相关的 classes 来作为外观。本质上,它将是 services/logic classes.

的层次结构

示例:我有一项服务可以生成推荐产品并通过电子邮件将其发送给用户。我将该服务称为 WeeklyRecommendationServices,它接受其他 2 个服务作为依赖项 - 一个 Recommendation 服务,它是一个用于生成推荐的黑盒(并且它有自己的依赖项 - 也许是一个 repo产品,一两个助手)和一个 EmailService,它可能有 Mailchimp 作为依赖项)。一些较低级别的依赖项,例如重定向、验证器等,将在那些子服务中,而不是充当入口点的服务中。

3。使用Laravel全局函数

一些 Facades 在 Laravel 5 中作为函数调用可用。例如,您可以使用 redirect()->back() 代替 Redirect::back(),也可以使用 view('some_blade) 代替 View::make('some_blade')。我相信 dispatch 和其他一些常用的外观也是一样的。

(编辑添加)4. 使用特征 当我今天处理排队作业时,我还观察到另一种注入依赖项的方法是使用特征。例如,Laravel 中的 DispathcesJobs 特征具有以下几行:

   protected function dispatch($job)
    {
        return app('Illuminate\Contracts\Bus\Dispatcher')->dispatch($job);
    }

任何使用特征的 class 都可以访问受保护的方法和依赖项。它比在构造函数或方法签名中有许多依赖项更整洁,比全局更清晰(关于涉及哪些依赖项)并且比手动 DI 容器调用更容易定制。缺点是每次调用该函数时都必须从 DI 容器中检索依赖项,

这是构造函数注入的好处之一 - 当您 class 做得太多时,它会变得很明显,因为构造函数参数变得太大。

要做的第一件事是拆分责任太多的控制器。

假设你有一个页面控制器:

Class PageController
{

    public function __construct(
        Request $request,
        ClientRepositoryInterface $clientrepo,
        StaffRepositortInterface $staffRepo
        )
    {

     $this->clientRepository = $clientRepo;
     //etc etc

    }

    public function aboutAction()
    {
        $teamMembers = $this->staffRepository->getAll();
        //render view
    }

    public function allClientsAction()
    {
        $clients = $this->clientRepository->getAll();
        //render view
    }

    public function addClientAction(Request $request, Validator $validator)
    {
        $this->clientRepository->createFromArray($request->all() $validator);
        //do stuff
    }
}

这是拆分成两个控制器的主要候选者,ClientControllerAboutController

一旦你这样做了,如果你仍然有太多*依赖关系,是时候寻找我称之为间接依赖关系的东西了(因为我想不出它们的正确名称!) - 不是直接的依赖关系由依赖项 class 使用,而是传递给另一个依赖项。

这方面的一个例子是 addClientAction - 它需要一个请求和一个验证器,只是为了将它们传递给 clientRepostory

我们可以通过创建一个新的 class 来重构,专门用于根据请求创建客户端,从而减少我们的依赖性,并简化控制器和存储库:

//think of a better name!
Class ClientCreator 
{
    public function __construct(Request $request, validator $validator){}

    public function getClient(){}
    public function isValid(){}
    public function getErrors(){}
}

我们的方法现在变成:

public function addClientAction(ClientCreator $creator)
{ 
     if($creator->isValid()){
         $this->clientRepository->add($creator->getClient());
     }else{
         //handle errors
     }
}

对于多少依赖项太多没有硬性规定。 好消息是,如果您使用松散耦合构建应用程序,重构就相对简单。

我更愿意看到一个具有 6 或 7 个依赖项的构造函数,而不是一个无参数的构造函数和一堆隐藏在整个方法中的静态调用