何时使用静态类 vs 实例化类

174

PHP是我的第一种编程语言。我还不太清楚何时使用静态类和实例化对象。

我知道可以复制和克隆对象。但是在我使用PHP的所有时间里,任何对象或函数始终以单个返回值(数组、字符串、整数)或void结束。

我理解书中的概念,比如视频游戏角色类,“复制汽车对象并使新对象变红”,这些都是有意义的,但是在PHP和Web应用程序中使用它们却没有意义。

一个简单的例子是博客。博客的哪些对象最适合作为静态或实例化对象实现?DB类呢?为什么不只是在全局范围内实例化db对象?为什么不将每个对象都设为静态?性能方面呢?

这一切只是风格吗?这样做是否有正确的方法?

10个回答

128

这是一个非常有趣的问题——答案也可能很有趣 ^^

最简单的考虑方法可能是:

  • 当每个对象都有自己的数据时(例如用户有姓名)使用实例化类
  • 当它只是在其他东西上工作的工具时使用静态类(例如,BB代码转换为HTML的语法转换器;它本身没有生命)

(是的,我承认,真的真的过于简单了...)

关于静态方法/类的一件事是它们不方便单元测试(至少在PHP中,但可能在其他语言中也是如此)。

关于静态数据的另一件事是,在程序中仅存在一个实例:如果你在某个地方设置了 MyClass::$myData 的某个值,它将具有该值,并且每个地方都是如此——说到用户,你只能拥有一个用户——这并不太好,对吗?

对于博客系统,我能说些什么呢?实际上,我认为没有多少我会写成静态的内容;也许是DB访问类,但最终可能也不是 ^^


所以基本上在处理独特的事物时,如照片、用户、帖子等,使用对象;而在处理一般事物时,使用静态。 - Robert
1
说到用户,每个请求只有一个活跃用户。因此,将请求的活跃用户设置为静态是有意义的。 - My1

72

使用静态方法的两个主要缺点是:

  • 使用静态方法的代码很难进行测试
  • 使用静态方法的代码很难进行扩展

在某个方法内调用静态方法实际上比导入全局变量更糟糕。在PHP中,类是全局符号,因此每次调用静态方法都依赖于全局符号(即类名)。这是全局变量有害的情况之一。我曾经在Zend框架的某个组件中遇到过这种方法的问题。有些类使用静态方法调用(工厂)来构建对象。对我来说,不可能向该实例提供另一个工厂以获取自定义对象。解决这个问题的方法是只使用实例和实例方法,并在程序开始时强制使用单例等。

Miško Hevery是谷歌敏捷教练,他提出了一个有趣的理论,或者说建议,即我们应该将对象创建时间与使用对象的时间分开。因此,程序的生命周期分为两个部分。第一部分(例如main()方法)负责应用程序中所有对象的连接,第二部分则执行实际工作。

因此,不要使用:

class HttpClient
{
    public function request()
    {
        return HttpResponse::build();
    }
}

我们应该使用以下方法:

class HttpClient
{
    private $httpResponseFactory;

    public function __construct($httpResponseFactory)
    {
        $this->httpResponseFactory = $httpResponseFactory;
    }

    public function request()
    {
        return $this->httpResponseFactory->build();
    }
}

然后,在索引/主页中,我们将执行以下操作(这是对象连接步骤,或创建程序要使用的实例图形的时间):

$httpResponseFactory = new HttpResponseFactory;
$httpClient          = new HttpClient($httpResponseFactory);
$httpResponse        = $httpClient->request();

主要思想是将依赖关系解耦出您的类。这样,代码就变得更加可扩展,并且对我来说最重要的部分是可测试性。为什么可测试性更重要?因为我并不总是编写库代码,因此可扩展性并不那么重要,但是当我进行重构时,可测试性是很重要的。无论如何,可测试的代码通常会产生可扩展的代码,因此这并不是一个非此即彼的情况。

Miško Hevery也清楚地区分了单例和Singletons(大小写有或没有"S")之间的差异。区别非常简单。小写"s"的Singleton是由索引/主文件中的连接所强制执行的。您实例化一个不实现Singleton模式的类的对象,并确保只将该实例传递给需要它的任何其他实例。另一方面,具有大写"S"的Singleton是经典(反)模式的实现。基本上是一个伪装成全局变量的东西,在PHP世界中用处不大。到目前为止,我还没有见过这种情况。如果您希望所有类都使用单个DB连接,则最好按以下方式进行:

$db = new DbConnection;

$users    = new UserCollection($db);
$posts    = new PostCollection($db);
$comments = new CommentsCollection($db);

通过上述方法,我们明确了我们拥有一个单例,并且还有一种很好的方式在测试中注入模拟对象或存根。单元测试如何带来更好的设计令人惊讶,但当你考虑到测试强制你思考如何使用代码时,这是有很多道理的。

/**
 * An example of a test using PHPUnit. The point is to see how easy it is to
 * pass the UserCollection constructor an alternative implementation of
 * DbCollection.
 */
class UserCollection extends PHPUnit_Framework_TestCase
{
    public function testGetAllComments()
    {
        $mockedMethods = array('query');
        $dbMock = $this->getMock('DbConnection', $mockedMethods);
        $dbMock->expects($this->any())
               ->method('query')
               ->will($this->returnValue(array('John', 'George')));

        $userCollection = new UserCollection($dbMock);
        $allUsers       = $userCollection->getAll();

        $this->assertEquals(array('John', 'George'), $allUsers);
    }
}
唯一需要使用(我曾经使用它们来模仿PHP 5.3中的JavaScript原型对象)静态成员变量的情况是当知道该字段在所有实例中都具有相同值时。此时可以使用静态属性和可能的一对静态getter/setter方法。不过,别忘了添加覆盖静态成员变量的实例成员的可能性。例如,Zend Framework 使用静态属性来指定在 Zend_Db_Table 实例中使用的 DB 适配器类的名称。我已经很久没有使用过它们了,所以可能已经不再相关,但这是我记得的方式。
不涉及静态属性的静态方法应该是函数。PHP有函数,我们应该使用它们。

1
@Ionut,我一点也不怀疑。我意识到职位/角色也有其价值;然而,就像所有与XP/敏捷相关的事物一样,它有一个非常轻浮的名称。 - jason
2
“依赖注入”我认为是这个听起来过于复杂的术语。在SO和Google上有很多关于它的阅读资料。 至于“单例模式”,这是一些让我深思的内容:http://www.slideshare.net/go_oh/singletons-in-php-why-they-are-bad-and-how-you-can-eliminate-them-from-your-applications 这是一个有关为什么PHP单例模式真的没有任何意义的演讲。希望这能对旧问题做出一点贡献 :) - dudewad

23

在PHP中,static可以应用于函数或变量。非静态变量与类的特定实例绑定。非静态方法作用于类的一个实例。所以我们假设有一个叫做BlogPost的类。

title将成为非静态成员。它包含了该博客文章的标题。我们还可能有一个叫做find_related()的方法。它不是静态的,因为它需要来自博客文章类的特定实例的信息。

这个类看起来会像这样:

class blog_post {
    public $title;
    public $my_dao;

    public function find_related() {
        $this->my_dao->find_all_with_title_words($this->title);
    }
}

另一方面,如果使用静态函数,您可以编写如下类:

class blog_post_helper {
    public static function find_related($blog_post) {
         // Do stuff.
    }
}

在这种情况下,由于函数是静态的且不作用于任何特定的博客文章,因此您必须将博客文章作为参数传递。

从本质上讲,这是关于面向对象设计的问题。您的类是系统中的名词,而对它们进行操作的函数则是动词。 静态函数是过程式的。您需要将函数的对象作为参数传递。


更新:我还要补充一点,很少是在实例方法和静态方法之间进行选择,而更多的是在使用类和使用关联数组之间进行选择。例如,在博客应用程序中,您可以从数据库中读取博客文章并将其转换为对象,或者您可以将它们保留在结果集中并将它们视为关联数组。然后,你编写以关联数组或关联数组列表作为参数的函数。

在面向对象的情况下,您在BlogPost类上编写方法,以操作单个帖子,并编写静态方法来操作帖子集合。


4
这个回答非常好,特别是你所做的更新,因为这就是我来到这个问题的原因。我理解"::"和"$this"的功能区别,但当你考虑到从关联数组中提取博客文章的例子时,它增加了一个全新的维度,这也是这个问题的潜在含义。 - Gerben Jacobs
这是对这个经常被问到的PHP问题的一个最佳实用(而不是理论性)答案之一,附录非常有帮助。我认为这是许多PHP程序员正在寻找的答案,已点赞! - ajmedway

14

这是纯粹的风格问题吗?

在很长一段时间内,是的。您可以编写完全良好的面向对象程序而不使用静态成员。实际上,有些人认为静态成员本身就是一种不纯洁的元素。我建议作为oop入门者,尽量避免使用静态成员。这将促使您沿着“面向对象”的方向编写代码,而非“过程式”编程风格。


13

我对大多数回答方式有不同的看法,特别是在使用PHP时。除非你有很好的理由,否则我认为所有类都应该是静态的。一些“why not”的原因包括:

  • 您需要该类的多个实例
  • 您的类需要被扩展
  • 您的代码的某些部分无法与任何其他部分共享类变量

让我举个例子。由于每个PHP脚本都会生成HTML代码,我的框架有一个HTML编写器类。这确保了没有其他类会尝试编写HTML,因为这是一个专业任务,应该集中到一个单独的类中。

通常,您将像这样使用html类:

html::set_attribute('class','myclass');
html::tag('div');
$str=html::get_buffer();
每次调用get_buffer()函数时,它会重置所有内容,以便下一个使用html writer的类从已知状态开始。
我所有的静态类都有一个init()函数,在第一次使用类之前需要调用它。这更多是按约定而非必要的。
在这种情况下,静态类的替代方案很混乱。您不希望每个需要写入少量html的类都必须管理html writer的实例。
现在我将给您举一个不适用静态类的例子。我的表单类管理表单元素列表,例如文本输入、下拉列表等。它通常像这样使用:
$form = new form(stuff here);
$form->add(new text(stuff here));
$form->add(new submit(stuff here));
$form->render(); // Creates the entire form using the html class

使用静态类是不可能完成这个任务的,尤其是考虑到每个添加的类的某些构造函数会做很多工作,并且所有元素的继承链非常复杂。因此,这是一个清楚的例子,静态类不应该被使用。

大多数实用程序类,例如将字符串转换/格式化的类,都是成为静态类的好选择。我的规则很简单:在PHP中,除非有一个理由说明不应该使用静态类,否则一切都会变成静态的。


你能否提供一个以下要点的示例: "你的代码的某些部分不能与其他部分共享类变量" - khurshed alam

10
“在另一个方法内调用静态方法实际上比导入全局变量更糟糕。”(定义“更糟糕”)......和“不涉及静态属性的静态方法应该是函数”。
这两个语句都很笼统。如果我有一组关于特定主题的函数,但实例数据完全不合适,我宁愿将它们定义在一个类中而不是每个函数都定义在全局命名空间中。我只是利用PHP5中可用的机制
  • 为它们提供命名空间——避免任何名称冲突
  • 将它们物理地放在一起,而不是分散在整个项目中——其他开发人员可以更轻松地找到已经可用的内容,并且不太可能重新发明轮子
  • 使用类常量而不是全局定义来处理任何魔术值
这只是强化更高内聚性和更低耦合度的便捷方式。
此外,至于你需要知道的是,在PHP5中根本不存在“静态类”;方法和属性可以是静态的。为了防止类的实例化,可以将其声明为抽象类。

2
为了防止类的实例化,可以将其声明为抽象类。我非常喜欢这个方法。之前我曾经听说过有人将__construct()函数设为私有函数,但是我认为如果需要的话,我可能会使用抽象类。 - Kavi Siegel

7

首先要问自己,这个对象将代表什么?对象实例适用于操作不同的动态数据集。

一个很好的例子是ORM或数据库抽象层。您可能有多个数据库连接。

$db1 = new Db(array('host' => $host1, 'username' => $username1, 'password' => $password1));
$db2 = new Db(array('host' => $host2, 'username' => $username2, 'password' => $password2));

那两个连接现在可以独立运行:
$someRecordsFromDb1 = $db1->getRows($selectStatement);
$someRecordsFromDb2 = $db2->getRows($selectStatement);

现在,在这个包/库中,可能还有其他类,如Db_Row等,用于表示从SELECT语句返回的特定行。如果这个Db_Row类是一个静态类,那么就假设您只有一个数据库中的一行数据,无法做到对象实例可以做到的事情。使用实例,您现在可以在无限数量的表格和无限数量的数据库中拥有无限数量的行。唯一的限制是服务器硬件;)
例如,如果Db对象上的getRows方法返回一个Db_Row对象数组,则您现在可以独立地操作每一行:
foreach ($someRecordsFromDb1 as $row) {
    // change some values
    $row->someFieldValue = 'I am the value for someFieldValue';
    $row->anotherDbField = 1;

    // now save that record/row
    $row->save();
}

foreach ($someRecordsFromDb2 as $row) {
    // delete a row
    $row->delete();
}

一个很好的静态类示例是处理注册表变量或会话变量,因为每个用户只有一个注册表或一个会话。
在应用程序的某个部分中:
Session::set('someVar', 'toThisValue');

抱歉,我无法执行翻译任务。
Session::get('someVar'); // returns 'toThisValue'

由于每个会话一次只能有一个用户,因此创建会话实例没有意义。

希望这可以帮助您更好地理解。另外,请查看“内聚性”和“耦合性”,它们概述了编写代码时适用于所有编程语言的一些非常好的实践方法。


6
如果你的类是静态的,那就意味着你无法将其对象传递给其他类(因为不存在实例),这意味着所有的类都会直接使用这个静态类,这意味着你的代码现在与这个类紧密耦合。
紧密耦合使得你的代码具有较低的可重用性、易于破坏和容易出现 bug。你需要避免使用静态类以便能够将类的实例传递给其他类。
当然,这只是许多其他原因之一,其中一些已经被提到。

3

我想说,在跨语言应用程序中,静态变量确实有用的情况。

你可以创建一个类,将语言作为参数传递给它(例如 $_SESSION ['language']),然后它会访问其他设计为如下的类:

Srings.php //The main class to access
StringsENUS.php  //English/US 
StringsESAR.php  //Spanish/Argentina
//...etc

使用Strings::getString("somestring")是将语言使用抽象出应用程序的好方法。您可以按照自己的方式实现,但在这种情况下,每个字符串文件都具有带有字符串值的常量,并且由Strings类访问这些常量非常有效。


3

通常情况下,你应该使用成员变量和成员函数,除非它必须在所有实例之间共享,或者你正在创建一个单例。使用成员数据和成员函数使你可以重复使用函数来处理多个不同的数据,而如果你使用静态数据和函数,则只能有一个数据副本进行操作。此外,虽然这并不适用于PHP,但静态函数和数据会导致代码不可重入,而类数据则方便重入。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接