在PHP项目中,存在哪些模式来存储,访问和组织帮助器对象?[关闭]


114

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

假设我有一个大型PHP CMS。CMS分为各种类别。一些例子:

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

等等

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

多年前,我的第一个方法是拥有一个$ application全局变量,其中包含这些类的初始化实例。

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

然后,我切换到Singleton模式和工厂功能:

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

但我也不满意。单元测试和封装对我来说变得越来越重要,而据我所知,全局/单数背后的逻辑破坏了OOP的基本思想。

然后当然可以给每个对象一些指向它所需的辅助对象的指针,这可能是最干净,节省资源和易于测试的方式,但是从长远来看,我对此表示怀疑。

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

我想扩大一下这里存在哪些常见模式的视野。我正在寻找从长期真实世界的角度讨论此问题的示例,其他想法和指向资源的指针。

另外,我很想听听有关此问题的专业,利基或简单怪异的方法。


1
我只是问了一个非常相似的问题,这个问题也有很多。:您可能有欣赏一些答案stackoverflow.com/questions/1967548/...
philfreo

3
抬起头来,通过引用返回一个新对象-这样$mh=&factory("messageHandler");是没有意义的,不会产生任何性能上的好处。另外,在5.3中不推荐使用。
ryeguy,2010年

Answers:


68

我会避免Flavius建议的Singleton方法。有许多理由避免使用这种方法。它违反了良好的OOP原则。Google测试博客在Singleton上有一些不错的文章,以及如何避免它:

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

备择方案

  1. 服务提供商

    http://java.sun.com/blueprints/corej2eepatterns/Patterns/ServiceLocator.html

  2. 依赖注入

    http://en.wikipedia.org/wiki/Dependency_injection

    和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框架(维基百科页面提供了指向PHP DI框架的链接)。

    重要的是,您只传递您实际使用的内容(调用操作),而不传递您仅传递给其他对象的内容,因为它们需要它们。这是来自“叔叔鲍勃”(罗伯特·马丁)的最新文章,内容涉及手动DI与使用框架

关于Flavius解决方案的更多想法。我不希望该文章成为反文章,但我认为重要的是要弄清为什么依赖注入至少在我看来比全局变量更好。

即使这不是“真正的” Singleton实现,我仍然认为Flavius弄错了。全球状态不好。请注意,此类解决方案还使用难以测试的静态方法

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

如果要能够测试应用程序,则需要采用其他方法来设计应用程序。当您进行测试优先编程时,您将遇到类似以下的问题:'接下来,我想在这段代码中实现日志记录;让我们先编写一个记录基本消息的测试”,然后提出一个强制您编写和使用无法替代的全局记录器的测试。

我仍然在努力地从博客中获取所有信息,并且实施起来并不总是那么容易,而且我有很多问题。但是当我掌握了Misko Hevery所说的话之后,就无法回到以前做过的事情(是的,全局状态和Singletons(大S))。


+1表示DI。尽管我没有像我想的那样使用它,但是无论我使用了多少少量它都非常有用。
阿努拉格2010年

1
@koen:关心举一个用PHP实现DI / SP实现的PHP示例吗?也许@Flavius代码是使用您建议的替代模式实现的?
Alix Axel

在我的答案中添加了指向DI实现和容器的链接。
Thomas

我想问的是,我现在正在阅读所有这些内容,但是我还没有阅读全部,请问依赖注入框架是否基本上是注册管理机构?
JasonDavis 2010年

不,不是。但是依赖注入容器也可以充当注册表。只需阅读我在答案中发布的链接即可。真正地解释了DI的概念。
托马斯(Thomas)2010年

16
class Application {
    protected static $_singletonFoo=NULL;

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

}

这就是我要做的方式。它按需创建对象:

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

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

注意:我所介绍的甚至不是真正的单例模式。通过将构造函数(Foo :: __ constructor())定义为私有,单例将仅允许其自身的一个实例。它只是对所有“应用程序”实例可用的“全局”变量。这就是为什么我认为它的使用是有效的,因为它不会忽略良好的OOP原则。当然,与世界上任何事物一样,此“模式”也不应过度使用!

我已经看到许多PHP框架,Zend框架和Yii中都使用了它。您应该使用一个框架。我不会告诉你哪一个。

附录 对于那些担心TDD的人,您仍然可以组成一些线路来进行依赖注入。它可能看起来像这样:

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;
        }
}

有足够的改进空间。只是一个PoC,请发挥您的想象力。

为什么会这样呢?好吧,在大多数情况下,该应用程序不会进行单元测试,而是可以在生产环境中运行。PHP的优势在于它的速度。PHP不是,永远不会像Java这样的“干净的OOP语言”。

在一个应用程序中,最多只有一个Application类,并且每个助手最多只有一个实例(按上述延迟加载)。当然,单身人士很糟糕,但只有在不遵守现实世界的情况下,这又是一次。在我的例子中,他们做到了。

刻板的“规则”(例如“坏人”)是邪恶的根源,它们是给那些不愿自己思考的懒惰人提供的。

是的,从技术上讲,我知道PHP宣言是BAD。但这是一种成功的语言,以其骇人听闻的方式表现出来。

附录

一种功能样式:

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();

2
我之所以投票否定答案,是因为我认为建议使用单例模式来解决该问题与可靠的OOP原则背道而驰。
koen

1
@koen:您的意思是对的,一般而言,但是据我所知,他是在为应用程序提供帮助,在一个应用程序中只有一个……嗯,一个应用程序。
Flavius

注意:我所介绍的甚至不是真正的单例模式。通过将构造函数定义为私有,单例将仅允许类的一个实例。它只是对所有“应用程序”实例可用的“全局”变量。这就是为什么我认为其有效并不忽略良好的OOP原则的原因。当然,与世界上任何事物一样,此“模式”也不应过度使用。
Flavius 2010年

我也是-1。它可能只是Singleton DP的一半,但很丑陋:“提供对其的全局访问”。
某人2010年

2
确实确实使他现有的方法更清洁。
Daniel Von Fange 2010年

15

我喜欢依赖注入的概念:

“依赖注入是通过组件的构造函数,方法或直接将它们赋予字段的组件。(来自Pico Container网站)”

Fabien Potencier写了一系列非常不错的文章,内容涉及依赖注入及其使用需求。他还提供了一个不错的小型依赖注入容器Pimple,我真的很想使用它(有关github的更多信息)。

如上所述,我不喜欢使用Singletons。在史蒂夫·耶格(Steve Yegge)的博客中,可以找到关于Singletons不好的设计的一个很好的总结。


我喜欢通过PHP中的Closures实现,非常有趣的阅读
Juraj Blahunka,2010年

我也是,他的网站上还有其他一些关于关闭的需求:fabien.potencier.org/article/17/…–
Thomas

2
让我们希望,主流网络公司将很快迁移到PHP 5.3,因为看到功能齐全的php 5.3服务器仍然不常见
Juraj Blahunka 2010年

他们将不得不,当越来越多的项目需要PHP 5.3一样的Zend Framework 2.0将framework.zend.com/wiki/display/ZFDEV2/...
托马斯

1
依赖注入也被接受了关于以下问题的答案decupling from GOD objectstackoverflow.com/questions/1580210/…一个很好的例子
Juraj Blahunka 2010年

9

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

辛格尔顿

不建议使用,因为它很难测试并且暗示全局状态。(单神经炎)

登记处

消除单身症,错误我也不推荐注册表,因为它也是一种单身人士。(难以进行单元测试)

遗产

可惜,PHP中没有多重继承,因此这将全部限制在链上。

依赖注入

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

传统的

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

构架

您可以滚动自己的依赖项注入器,或使用某些依赖项注入框架,例如。亚迪夫

申请资源

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

这是Zend Framework 1.x中实现的方法

资源加载器

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

注入特定层

在应用程序中的任何地方并不总是需要资源。有时您只需要它们,例如在控制器(MV C)中。然后,您可以仅在此处注入资源。

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

在这里查看我的处理方法:

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

最后,我想在这里添加关于非常重要的内容-缓存的注释。
通常,尽管选择了技术,但您仍应考虑如何缓存资源。缓存将是资源本身。

应用程序可能非常大,并且在每次请求时加载所有资源非常昂贵。有很多方法,包括此appserver-in-php-Google Code上的项目托管


6

如果要使对象全局可用,则注册表模式可能会让您感兴趣。要获取灵感,请查看Zend Registry

所以也 注册表与单例问题。


如果您不想使用Zend Framework,以下是PHP5注册表模式的不错实现:phpbar.de/w/Registry
Thomas

我喜欢类型化的注册表模式,例如Registry :: GetDatabase(“ master”); 注册表:: GetSession($ user-> SessionKey()); Registry :: GetConfig(“ local”); 并为每种类型定义一个接口。这样,您可以确保不会意外覆盖用于其他用途的密钥(即,您可能具有“主数据库”和“主配置”。通过使用接口,您可以确保仅使用有效的对象。也可以通过使用多个登记类,但恕我直言,一个单一的一个更简单,使用更方便执行,但仍具有优势。
Morfildur

或者当然是内置在PHP中的一个-$ _GLOBALS
Gnuffo1

4

正如您在单元测试中可能看到的那样,PHP中的对象占用了大量内存。因此,这是理想的销毁未使用的对象为尽快以节省内存为其他进程。考虑到这一点,我发现每个对象都适合两个模型之一。

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

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

2)对象仅在调用它的方法/函数的生命周期中存在,在这种情况下,简单的创建有利于防止挥之不去的对象引用使对象存活的时间过长。

$object = new Class();

存储临时对象ANYWHERE可能会导致内存泄漏,因为对它们的引用可能会忘记保持对象在内存中的脚本的其余部分。


3

我会选择返回初始化对象的函数:

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

在测试环境中,您可以定义它以返回模型。您甚至可以检测使用debug_backtrace()调用该函数的内部人员,并返回不同的对象。您可以在其中注册谁想要获取什么对象以了解程序内部实际发生的情况。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.