【问题标题】:Using dependency injection over laravel facades在 laravel 外观上使用依赖注入
【发布时间】:2016-01-26 10:10:38
【问题描述】:

我阅读了许多资料,暗示 laravel 外观最终存在是为了方便,并且这些类应该改为 injected 以允许松散耦合。甚至Taylor Otwell has a post 解释如何做到这一点。看来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似乎只注入到类构造函数和某些方法(控制器)中,即使我有相当精简的函数和类,我发现类的构造函数正在被打包将所需的类注入所需的方法中。

现在我发现,如果我继续这种方法,我将需要我自己的 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]);
    }
}

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

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

我是不是把这个想法搞错了?我的课程/功能不够精简吗?我是否错过了 laravel 容器的要点,还是真的需要考虑创建自己的 IoC 容器? Some 其他 answers 似乎暗示 laravel 容器能够消除我的问题?

也就是说,在这个问题上似乎没有一个明确的共识......

【问题讨论】:

  • 最佳实践始终是指导方针,而不是一成不变的规则。有些模式完全冲突,因此您必须权衡每种模式的利弊并做出判断。就个人而言,当涉及到构造函数时,许多注入的依赖项可能是不可避免的。但它也不是你在 Laravel 中测试之外一直使用的东西。 IoC 会为你处理它们,所以不要太害怕加载你的构造函数。
  • 纠正我自己,当我说“不是你一直使用的东西”时,我的意思是你不会为你的构造函数手动提供依赖项。由于 IoC 将处理提供它们,因此方法过多导致的问题并不完全相同。不过,我个人会将其保留给构造函数,而不是特定的方法。
  • 所以你不认为注入大量类的构造函数(在中心位置)是个问题?
  • 为什么不直接使用redirect() 辅助函数呢? return redirect('something');
  • @myol - 不,我没有。我认为这可能是问题的证据,正如另一个答案中所说,但有时可能存在原则不适合您的用例的情况。您还想避免过度设计您的软件。编写可读、可管理的代码与被锁定在一个设计原则中以致于你花费数小时试图在没有意义的情况下强行将代码写入其中之间有一条很好的界限。

标签: php laravel dependency-injection laravel-facade


【解决方案1】:

这是构造函数注入的好处之一 - 当你的类做了很多事情时,它变得很明显,因为构造函数参数变得太大。

要做的第一件事是将责任过多的控制器分开。

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

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 的主要候选者。

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

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

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

//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 个依赖项的构造函数,而不是无参数的构造函数和隐藏在方法中的一堆静态调用

【讨论】:

  • 我已经重构了我的类,以便现在将“间接”laravel 依赖项注入这些 SRP 类(验证、格式化、请求)等,但我仍然有一个主要的业务逻辑“服务”我将所有这些类注入然后使用。我认为您的回答暗示了我缺少的模式问题..
  • 如果您可以更新您的问题以显示您如何使用此服务,我很乐意看看。猜测一下,您可能已经从一个极端走向了另一个极端——将所有逻辑从控制器中取出,而是将您的单一“业务服务”变成了神级。可能业务服务需要通过类似的流程进行分解
  • 经过多次重构,确实是类“责任过多”的情况。我学到了宝贵的一课,现在我的项目更能适应变化。
  • @myol 太好了,很高兴它为您解决了问题,感谢您回来报告结果
【解决方案2】:

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

至于解决方案:

1.手动解决依赖关系

解决依赖关系的一种方法,如果你不想这样做的话。构造函数或者方法注入,就是直接调用app():

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

2。重构

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

示例:我有一项服务可以生成推荐产品并通过电子邮件将其发送给用户。我将服务称为WeeklyRecommendationServices,它接受另外两个服务作为依赖项——一个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);
    }

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

【讨论】:

  • 为什么你会推荐函数调用而不是显式注入所需的类?这不会导致难以发现依赖关系吗?
  • 1.如果依赖项是标准依赖项(例如重定向、分派等),我认为这是可以接受的。 2. 这些全局函数实际上是对 DI 解析器的调用。
【解决方案3】:

构成 Laravel 中路由机制一部分的类方法(中间件、控制器等)also have their type-hints used to inject dependencies - 它们并不都需要注入到构造函数中。这可能有助于使您的构造函数保持苗条,即使我不熟悉任何四参数限制经验法则; PSR-2 allows for the method definition to be stretched over multiple lines 大概是因为需要超过四个参数的情况并不少见。

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

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

【讨论】:

    【解决方案4】:

    与其说是一个答案,而是在与我的同事交谈后思考一些问题,他们提出了一些非常有效的观点;

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

    2. 开发速度减慢,可测试性并没有真正提高,因为外观已经可以测试。快速开发是首先使用 laravel 的一个强项。时间永远是一种约束。

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

    【讨论】:

      【解决方案5】:

      你的想法和顾虑是正确的,我也有。 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";
          }
      }
      

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

      最后一点,如果您正在构建一个小型应用程序,甚至是大型应用程序中的一个页面,例如“联系页面”和“联系页面提交”,您肯定可以在控制器中使用外观做所有事情,它只是取决于项目的复杂性。

      【讨论】:

      • 我明白你在说什么,但如果你有一个单点做业务逻辑(如服务),即使你为验证、格式化等创建单独的服务,注入的 laravel 类也是只需替换为您自己的注入类(同时将 laravel 类注入这些类)
      • 就在我发布的示例中,您可以看到依赖关系被划分...只是不要过度使用它来将每个操作重构为一个类。确保你的代码是“SOLID”,在 90% 的情况下你不会有这么多的依赖
      • 你一定可以在帖子中给我一个例子,我会尝试重构它。
      【解决方案6】:

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

      【讨论】:

        猜你喜欢
        • 2013-09-26
        • 2013-05-10
        • 2015-03-02
        • 1970-01-01
        • 2022-01-16
        • 2016-07-19
        • 2023-03-23
        • 2014-02-02
        • 1970-01-01
        相关资源
        最近更新 更多