关于OOP:在PHP项目中,存在哪些模式来存储,访问和组织帮助程序对象?

In a PHP project, what patterns exist to store, access and organize helper objects?

在基于PHP、面向对象的项目中,如何组织和管理助手对象,如数据库引擎、用户通知、错误处理等?

假设我有一个大型的php-cms。CMS按不同的类别组织。举几个例子:

  • 数据库对象
  • 用户管理
  • 用于创建/修改/删除项的API
  • 向最终用户显示消息的消息对象
  • 将您带到右侧页面的上下文处理程序
  • 显示按钮的导航栏类
  • 日志记录对象
  • 可能,自定义错误处理

等。

我正在处理一个永恒的问题,如何最好地使这些对象能够被需要它的系统的每个部分访问。

多年前,我的第一个Apporach是拥有一个$applicationglobal,其中包含这些类的初始化实例。

1
2
global $application;
$application->messageHandler->addMessage("Item successfully inserted");

然后我换成了单件模式和工厂功能:

1
2
$mh =&factory("messageHandler");
$mh->addMessage("Item successfully inserted");

但我也不高兴。单元测试和封装对我来说越来越重要,在我的理解中,globals/singleton背后的逻辑破坏了OOP的基本思想。

当然,也有可能给每个对象一些指向它所需要的辅助对象的指针,可能是最干净、最节省资源和测试友好的方式,但我对这种方式的长期可维护性表示怀疑。

我研究过的大多数PHP框架要么使用单例模式,要么使用访问初始化对象的函数。两种方法都不错,但正如我所说,我对两者都不满意。

我想拓宽视野,看看这里有什么共同的模式。我正在寻找从长期、现实世界的角度讨论这一问题的例子、其他想法和资源指针。

另外,我很感兴趣听到关于这个问题的专门的、利基的或简单的奇怪的方法。


我会避免弗拉维乌斯建议的单子方法。有很多原因可以避免这种方法。这违反了良好的OOP原则。Google测试博客上有一些关于singleton的好文章,以及如何避免它:

http://googletesting.blogspot.com/2008/08/by-miko-hevery-so-you-join-new-project.htmlhttp://googletesting.blogspot.com/2008/05/tott-using-dependancy-injection-to.htmlhttp://googletesting.blogspot.com/2008/08/where-have-all-singletons-gone.html

选择

  • 服务提供商

    http://java.sun.com/blueprints/corej2eepatterns/patterns/servicelocator.html

  • 依赖注入

    http://en.wikipedia.org/wiki/dependency_注入

    以及一个PHP解释:

    http://components.symfony-project.org/dependency-injection/trunk/book/01-dependency-injection

  • 这是一篇关于这些备选方案的好文章:

    http://martinfowler.com/articles/injection.html

    实现依赖注入(DI):

    • 我相信您应该问构造函数中需要什么来使对象起作用:new YourObject($dependencyA, $dependencyB);

    • 您可以手动提供所需的对象(依赖项)($application = new Application(new MessageHandler())。但您也可以使用DI框架(维基百科页面提供了指向phpDI框架的链接)。

      重要的是你只传递你实际使用的东西(调用一个动作),而不是仅仅传递给其他对象的东西,因为它们需要它。这是"Bob叔叔"(Robert Martin)最近发表的一篇文章,讨论使用框架的手动DI。

    关于弗拉维乌斯的解决方案还有一些想法。我不希望这篇文章是一篇反文章,但我认为重要的是要明白为什么依赖注入(至少对我来说)比全局注入(globals)更好。

    尽管它不是一个"真正的"单例实现,但我仍然认为flavius搞错了。全局状态不好。请注意,此类解决方案也使用难以测试的静态方法。

    我知道很多人会这么做,批准并使用它。但是阅读MiskoHeverys的博客文章(一位谷歌测试性专家),重新阅读它,慢慢消化他所说的话,确实改变了我对设计的看法。

    如果您希望能够测试您的应用程序,您将需要采用不同的方法来设计您的应用程序。当您进行第一次测试编程时,您将很难做到这一点:"下一步,我要实现这段代码的日志记录;让我们先编写一个记录基本消息的测试",然后再编写一个强制您编写和使用无法替换的全局日志记录程序的测试。

    我仍然在努力处理我从那个博客中得到的所有信息,这并不总是容易实现的,我有很多问题。但在我理解了米斯科·赫韦里的话之后,我再也无法回到我以前的做法(是的,全球国家和单身汉(大S)):-)


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Application {
        protected static $_singletonFoo=NULL;

        public static function foo() {
            if(NULL === self::$_singletonFoo) {
                self::$_singletonFoo = new Foo;
            }
            return self::$_singletonFoo;
        }

    }

    我就是这样做的。它按需创建对象:

    1
    Application::foo()->bar();

    这是我做的方式,它尊重OOP原则,它的代码比你现在做的要少,对象只有在代码第一次需要它时才被创建。

    注意:我所展示的甚至不是真正的单例模式。通过将构造函数(foo::u constructor())定义为private,singleton将只允许自身的一个实例。它只是一个"全局"变量,可用于所有"应用程序"实例。这就是为什么我认为它的使用是有效的,因为它不忽视良好的OOP原则。当然,和世界上任何事物一样,这种"模式"也不应该被滥用!

    我在很多PHP框架中都看到过这种情况,其中包括Zend框架和YII。你应该使用一个框架。我不会告诉你是哪一个。

    补遗对于那些担心TDD的人来说,您仍然可以通过一些连接来依赖注入它。它可能看起来像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Application {
            protected static $_singletonFoo=NULL;
            protected static $_helperName = 'Foo';

            public static function setDefaultHelperName($helperName='Foo') {
                    if(is_string($helperName)) {
                            self::$_helperName = $helperName;
                    }
                    elseif(is_object($helperName)) {
                            self::$_singletonFoo = $helperName;
                    }
                    else {
                            return FALSE;
                    }
                    return TRUE;
            }
            public static function foo() {
                    if(NULL === self::$_singletonFoo) {
                            self::$_singletonFoo = new self::$_helperName;
                    }
                    return self::$_singletonFoo;
            }
    }

    有足够的改进空间。这只是一个概念验证,发挥你的想象力。

    为什么会这样?好吧,大多数时候应用程序不会进行单元测试,而是实际运行,希望在生产环境中。PHP的优势在于它的速度。PHP不是,也永远不会是一个"干净的OOP语言",像Java。

    在一个应用程序中,最多只有一个应用程序类和每个助手的一个实例(根据上面的延迟加载)。当然,单身是不好的,但再次强调,只有当他们不坚持现实世界。在我的例子中,是这样的。

    刻板的"规矩"就像"单身不好"一样是罪恶的根源,他们是为懒惰的人而不愿为自己着想的。

    是的,我知道,从技术上讲,PHP声明很糟糕。然而,它是一种成功的语言,以其黑客的方式。

    补遗

    一种功能样式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function app($class) {
        static $refs = array();

        //> Dependency injection in case of unit test
        if (is_object($class)) {
            $refs[get_class($class)] = $class;
            $class = get_class($class);
        }

        if (!isset($refs[$class]))
            $refs[$class] = new $class();

        return $refs[$class];
    }

    //> usage: app('Logger')->doWhatever();


    我喜欢依赖注入的概念:

    "Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields. (From Pico Container Website)"

    FabienPotencier写了一系列非常好的关于依赖注入和使用它们的必要性的文章。他还提供了一个很好的小依赖注入容器,名为Pimple,我非常喜欢使用它(更多关于Github的信息)。

    如上所述,我不喜欢单件的使用。在SteveYegge的博客中可以找到一个很好的总结,说明为什么单件设计不好。


    最好的方法是为这些资源提供某种容器。实现此容器的一些最常见方法:

    独生子女

    不推荐,因为它很难测试,并且意味着一个全局状态。(单鼻炎)

    登记处

    消除了singleton it is,我也不建议注册,因为它也是一种singleton。(难以进行单元测试)

    遗传

    遗憾的是,PHP中没有多个继承,因此这将所有继承限制在链上。

    依赖注入

    这是一个更好的方法,但主题更大。

    传统的

    最简单的方法是使用构造函数或setter注入(使用setter或类构造函数传递依赖对象)。

    框架

    您可以滚动自己的依赖注入器,或者使用一些依赖注入器框架,例如yadif

    应用程序资源

    您可以初始化应用程序引导(充当容器)中的每个资源,并在访问引导对象的应用程序中的任何位置访问它们。

    这是在Zend框架1.x中实现的方法

    资源加载器

    一种静态对象,只在需要时加载(创建)所需的资源。这是一个非常聪明的方法。您可能会看到它正在运行,例如,实现symfony的依赖注入组件

    注入特定层

    应用程序中的任何地方并不总是需要这些资源。有时您只需要它们,例如在控制器(mv c)中。然后您可以只在那里注入资源。

    常用的方法是使用docblock注释添加注入元数据。

    请看我的方法:

    如何在Zend框架中使用依赖注入?-堆栈溢出

    最后,我想在这里添加一个关于非常重要的事情的注释-缓存。一般来说,尽管您选择了这种技术,但是您应该考虑如何缓存资源。缓存将是资源本身。

    应用程序可能非常大,在每次请求时加载所有资源非常昂贵。有很多方法,包括在Google代码上托管PHP项目的AppServer。


    如果您想使对象全局可用,那么注册表模式对您来说可能很有趣。要获得灵感,请查看Zend注册表。

    所以注册表和singleton的问题。


    PHP中的对象占用了大量的内存,正如您可能从单元测试中看到的那样。因此,最好尽快销毁不需要的对象,以便为其他进程保存内存。考虑到这一点,我发现每个物体都适合两个模子中的一个。

    1)对象可能有许多有用的方法,或者需要多次调用,在这种情况下,我实现了一个singleton/registry:

    1
    2
    3
    $object = load::singleton('classname');
    //or
    $object = classname::instance(); // which sets self::$instance = $this

    2)对象只存在于调用它的方法/函数的生命周期中,在这种情况下,简单的创建有助于防止延迟的对象引用使对象保持太长的生命周期。

    1
    $object = new Class();

    将临时对象存储在任何地方都可能导致内存泄漏,因为对它们的引用可能会忘记将对象保存在内存中以供脚本的其余部分使用。


    我将使用返回初始化对象的函数:

    1
    A('Users')->getCurrentUser();

    在测试环境中,您可以定义它以返回实体模型。甚至可以在内部使用debug_backtrace()检测调用函数的人,并返回不同的对象。你可以在它里面注册谁想得到什么对象来获得一些洞察你的程序中到底发生了什么。


    为什么不读一下手册呢?

    http://php.net/manual/en/language.oop5.autoload.php