20

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

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

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


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

优雅的解决方案?

我的问题有几种可能的解决方案,但都不是优雅的

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

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


依赖注入框架

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

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

  • 将 DI 喷射器传递到这一层?
  • DI 框架作为单例?
  • 只为这一层放置隔离的 DI 框架配置并创建单独的 DI 注入器实例?
4

5 回答 5

3

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

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

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

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

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

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

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

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

于 2010-02-14T20:07:56.957 回答
1

当依赖注入可行时,单身人士不赞成(而且我还没有找到需要单身人士的案例)。

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

例如:

// 在控制器中定义应用程序——很可能在引导程序中
$应用程序=新应用程序();
控制器::setApplication($application);

// Controller 类定义中的某处
公共函数 setContentType($contentType)
{
    self::$application->setContentType($contentType);
}

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

于 2010-02-06T01:14:11.963 回答
1

重构怎么样?

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

于 2010-02-11T19:36:44.000 回答
1

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

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

我希望这是有道理的。

问候,尼克

于 2011-01-19T07:31:46.443 回答
1

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

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

您的应用程序不知道如何创建 Factory 的控制器。由于您可以将自己的 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/

于 2011-11-05T19:29:17.593 回答