48

我已经阅读了许多资料,暗示 laravel 外观最终存在是为了方便,并且应该注入这些类以允许松散耦合。甚至Taylor Otwell 也有一篇文章解释了如何做到这一点。似乎我不是唯一一个对此感到疑惑的人。

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的 ,然后是parameter其 url。一个服务功能还需要检查表单中的值,所以我需要Requestand Validator。就像那样,我有四个参数。

// 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 容器?其他一些答案似乎暗示 laravel 容器能够消除我的问题?

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

4

6 回答 6

26

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

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

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

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

于 2016-02-05T00:19:10.650 回答
3

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

至于解决方案:

1. 手动解决依赖

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

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

2.重构

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

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

3. 使用 Laravel 全局函数

在 Laravel 5 中,一些 Facades 可以作为函数调用使用。例如,你可以使用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 容器中检索依赖项,

于 2016-02-04T19:19:33.517 回答
1

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

于 2016-02-04T10:26:19.640 回答
1

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

  1. 如果 laravel 的内部结构在不同版本之间发生了变化(这显然在过去发生过),注入解析的外观类路径将破坏升级中的所有内容 - 而大部分(如果不是完全)使用默认外观和辅助方法可以避免这个问题.

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

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

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

于 2016-01-29T10:23:33.457 回答
1

好吧,您的想法和担忧是正确的,我也有。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";
    }
}

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

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

于 2016-02-03T07:35:01.410 回答
1

构成 Laravel 中路由机制一部分的类方法(中间件、控制器等)也有用于注入依赖项的类型提示——它们并不都需要在构造函数中注入。这可能有助于使您的构造函数保持苗条,即使我不熟悉任何四参数限制经验法则;PSR-2 允许将方法定义扩展到多行,大概是因为需要四个以上的参数并不罕见。

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

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

于 2016-01-27T00:45:03.830 回答