【问题标题】:Composing a Controller class with Dependency Injection in PHP在 PHP 中使用依赖注入组合控制器类
【发布时间】:2011-01-13 17:58:48
【问题描述】:

如何解决PHP中组成Controller类的问题,应该是:

  • 通过使用依赖注入轻松可测试
  • 为最终程序员提供共享对象
  • 提供一种加载新用户库的方法

往下看,使用依赖注入框架进行控制器实例化


问题是,派生的控制器可以使用程序员想要的任何资源(例如框架提供)。如何创建对共享资源(数据库、用户、存储、缓存、助手)、用户定义的类或其他库的统一访问?

优雅的解决方案?

我的问题有几种可能的解决方案,但没有一个看起来很优雅

  • 尝试通过构造函数传递所有共享对象? (即使有 10 个占位符也可以创建构造函数)
  • 创建getter、settter? (臃肿的代码)$controller->setApplication($app)
  • 在共享资源上应用单例User::getInstance()Database::getInstance()
  • 使用依赖注入容器作为控制器内部对象共享的单例?
  • 提供一个全局应用单例作为工厂? (这个在 php 框架中看起来很常用,但是它强烈违反了 DI 原则和 Demeter 定律)

我知道,不鼓励和禁止创建强耦合类:),但是我不知道这种范式如何应用于其他程序员(控制器类)的起点,他们应该能够访问提供给 MVC 架构的共享资源。我相信,将控制器类分解成更小的类会以某种方式破坏 MVC 的实际意义。


依赖注入框架

DI 框架看起来是一个可行的选择。然而问题仍然存在。像 Controller 这样的类并不位于 Application 层,而是位于 RequestHandler/Response 层。

这一层应该如何实例化控制器?

  • 将 DI 注入器传递到该层?
  • DI 框架作为单例?
  • 仅为该层放置隔离的 DI 框架配置并创建单独的 DI 注入器实例?

【问题讨论】:

  • 您在寻找特定于 PHP 的答案吗?
  • 是的,我忘了说,语言是 PHP,但我认为解决方案应该与语言无关
  • 我在询问之前已经阅读了它们。两者都建议使用依赖注入,但 DI 需要 parent 来请求 children,这是注入的。所谓的“父母”知道他要做什么,因此知道要问什么。我被困在控制器实现中。控制器应该是灵活的,所以最终程序员可以在应用程序逻辑中做他想做的一切。最好完全注入所有可能的对象吗?提供容器?但这会违反得墨忒耳定律……
  • 也许你可以看看这个小的 DI 库。非常简单易用和可测试gabordemooij.com/harbour

标签: php model-view-controller dependency-injection instantiation


【解决方案1】:

当依赖注入可行时,单例是不受欢迎的(我还没有找到需要单例的情况)。

您很可能可以控制控制器的实例化,因此您可以摆脱提到的$controller->setApplication($application),但如有必要,您可以使用静态方法和变量(它们对应用程序的正交性的危害远小于单例);即Controller::setApplication(),通过实例方法访问静态变量。

例如:

// 在控制器中定义应用程序——很可能在引导程序中 $应用程序=新应用程序(); 控制器::setApplication($application); // Controller 类定义中的某处 公共函数 setContentType($contentType) { self::$application->setContentType($contentType); }

我养成了将静态和实例属性和方法分开的习惯(必要时,仍然在类定义的顶部对属性进行分组)。我觉得这没有单例那么笨拙,因为类仍然非常紧凑。

【讨论】:

  • 这看起来不像是一个解决方案,测试编写会很容易。::setApplication 将为所有控制器创建一个全局状态,我不相信这是要走的路..
【解决方案2】:

重构怎么样?

当然,这不是您的选择之一,但您声明代码是一个很大程度上耦合的类。为什么不花时间和精力将其重构为更模块化、可测试的组件?

【讨论】:

  • 好点,但是我不知道这是否是个好主意,因为通过分解 Controller 类,MVC 模式会崩溃..
  • 您仍然有一个控制器,但控制器将被重构,以便更改该类的内部结构。控制器的公共接口应该保持不变,只是内部代码将被拆分。当然这是对你的代码是什么样子的猜测,但这就是我要做的。
  • 你能指出一些研究或博客,在哪里讨论这个问题?
  • 查看单一职责原则(SOLID 的一部分)。总而言之,一个班级应该做好一件事,做好一件事。不是多项工作,这就是您的控制器现在正在做的事情。
  • 我仍然回到使用 DI,将加载器对象作为参数传递,它可以为最终用户加载其他库(易于模拟和测试)
【解决方案3】:

您自己在开发框架吗?如果不是,则您的问题不适用,因为您必须从 already existing frameworks 及其现有解决方案中进行选择。在这种情况下,您的问题必须重新表述为“我如何在框架 X 中进行单元测试/依赖注入”。

如果您自己开发一个框架,您应该首先检查现有的框架是如何解决这个问题的。而且您还必须详细说明自己的要求,然后选择最简单的解决方案。如果没有要求,您的问题纯粹是审美和争论。

在我看来,最简单的解决方案是让公共属性初始化为你的框架提供的默认值,否则你可以在这里注入你的模拟。 (这等于您的 getter/setter 解决方案,但没有提到的膨胀。您并不总是需要 getter 和 setter。)或者,如果您真的需要它,您可以提供一个构造函数来在一次调用中初始化它们(如您所建议的) .

Singleton 是一个优雅的解决方案,但您必须再次问自己,它适用于您的情况吗?如果您的应用程序中必须有相同类型对象的不同实例,则不能使用它(例如,如果您希望仅在一半的应用程序中模拟一个类)。

当然,拥有所有选项真的很棒。您可以拥有 getter/setter、构造函数,当省略初始化时,默认取自单例工厂。但是在不需要的时候有太多的选项并不好,因为程序员必须弄清楚要使用哪种约定、选项和模式,这很令人不安。我绝对不想为了让一个简单的 CRUD 运行而做出几十个设计决策。

如果您查看其他框架,您会发现没有灵丹妙药。通常,单个框架会根据上下文使用不同的技术。在控制器中,DI 是一件非常简单的事情,看看 CakePHP 的 $helpers, $components 变量,它们指示将适当的变量注入控制器类。对于应用程序本身来说,单例仍然是一件好事,因为总是只有一个应用程序。使用公共属性注入较少更改/模拟的属性。 在 MVC 的情况下,子类化也是完全可行的选择:就像 CakePHP 中的 AppController、AppView、AppModel。它们被插入到框架和所有特定控制器、视图和模型类之间的类层次结构中。这样,您就可以为您的主要类类型声明全局变量。

在 Java 中,由于动态类加载器和反射,您可以选择更多选项。但另一方面,您还必须支持更多需求:并行请求、工作线程之间的共享对象和状态、分布式应用服务器等。

如果您首先知道自己需要什么,您只能回答适合您的问题。但实际上,你为什么还要编写另一个新框架呢?

【讨论】:

  • 是的,我正在构建,尝试一个简单的基于组件的框架,我需要确切的推送,你的答案给了我。另外,正如我之前所说,很多框架通过利用来解决这些问题单例,但我想尽可能地避开全局状态(组件应该易于替换)。将在第二天投票(用完票:))。感谢您的宝贵时间
  • “单身人士是一种优雅的解决方案”?!单例一点也不优雅,它们隐藏了类依赖关系,许多人认为它们是一种反模式。
【解决方案4】:

据我了解,您的 Application 类应该是调度程序。如果是这样,我宁愿使用控制器构造函数来传递应用程序的实例,这样控制器就会知道是谁在调用它。稍后,如果您想根据是否从 CLI 中调用代码来拥有不同的 Application 实例,您可以拥有一个 ApplicationInterface,Application\Http 和 Application\Cli 将实现它,并且一切都将易于维护。

您还可以实现一些工厂模式来很好地实现 DI。比如这里查看createThroughReflection方法:https://github.com/troelskn/bucket/blob/master/lib/bucket.inc.php

我希望这是有道理的。

问候, 尼克

【讨论】:

    【解决方案5】:

    您还可以使用 ControllerFatory,将其提供给您的应用程序或路由器/调度程序

    你可以调用 $controllerFactory->createController($name);

    您的应用程序将不知道如何创建工厂将要创建的控制器。由于您可以将自己的 ControllerFactory 注入到 DI 容器中,因此您可以根据控制器管理所需的所有依赖项。

    class ControllerFactory {
        public function __construct(EvenDispatcher $dispatcher,
                                    Request $request,
                                    ResponseFactory $responseFactory, 
                                    ModelFactory $modelFactory, 
                                    FormFactory $formFactory) {
            ...
        }
    
        public function createController($name = 'Default') {
            switch ($name) {
                case 'User':
                  return new UserController($dispatcher, 
                                            $request, 
                                            $responseFactory->createResponse('Html'), 
                                            $modelFactory->createModel('User'),
                                            $formFactory->createForm('User'),...);
    
                  break;
                case 'Ajax':
                  return new AjaxController($dispatcher, 
                                            $request, 
                                            $responseFactory->createResponse('Json'), 
                                            $modelFactory->createModel('User'));
                  break;
                default:
                     return new DefaultController($dispatcher, $request, $responseFactory->createResponse('Html'));
            }
        }
    
    } 
    

    因此,您只需将这个工厂添加到您的 DI 容器中并将其传递给您的应用程序。 每当您需要新的控制器时,将其添加到工厂中,如果需要新的依赖项,则通过 DI 容器将它们提供给工厂。

    class App {
        public function __construct(Router $router,Request $request, ControllerFactory $cf, ... ) {
          ...
        }
    
        public function execute() {
            $controllerName = $this->router->getMatchedController();
            $actionName $this->router->getMatchedAction();
    
            $controller = $cf->createController($controllerName);
    
            if(is_callable($controller, $actionName)) {
                $response = $controller->$action(request);
                $response->send();     
            }
        }
    }
    

    这不是生产代码,我还没有测试过,但这是您将控制器与应用程序分离的方式。请注意,这里有一个糟糕的耦合,因为我的控制器返回的是响应,而我在 App 中执行了响应。但就像我说的,这只是一个小例子。

    将模型、表单和控制器的工厂传递给它们各自的父级通常是一个好主意,因为您最终会在引导时加载所有对象图,这非常糟糕且消耗内存。

    我知道这个答案已经被批准,但这是我在这个问题上的 2 美分

    有一篇关于这个主题的好文章

    http://miller.limethinking.co.uk/2011/07/07/dependency-injection-moving-from-basics-to-container/

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-08
      相关资源
      最近更新 更多