关于PHP:何时使用静态vs实例化类

When to use static vs instantiated classes

PHP是我的第一种编程语言。当使用静态类和实例化对象时,我无法完全理解。

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

我理解书中的概念,比如电子游戏角色课。复制car对象,并使新的一个红色,这一切都有意义,但不是它在php和web应用程序中的应用程序。

一个简单的例子。博客。博客的哪些对象最好实现为静态或实例化对象?DB类?为什么不在全局范围内实例化db对象呢?为什么不让每个对象都变成静态的呢?那表演呢?

这都是风格吗?有没有合适的方法来做这些事情?


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

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

  • 使用一个已声明的类,其中每个对象都有自己的数据(就像用户有名称一样)
  • 当静态类只是一个在其他东西上工作的工具时(例如,bb代码到html的语法转换器;它本身没有生命)

(是的,我承认,真的太过简单了…)

静态方法/类的一个特点是它们不便于单元测试(至少在PHP中是这样,但可能在其他语言中也是这样)。

关于静态数据的另一个问题是,在您的程序中只存在一个实例:如果您将myclass::$mydata设置为某个值,它就会有这个值,而且只有这个值,每个地方——说到用户,您只能有一个用户——这不是很好,是吗?

对于博客系统,我能说什么?实际上,我认为没有太多的东西是静态的;可能是db access类,但最终可能不是。^^


反对使用静态方法的主要两个原因是:

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

在其他方法中调用静态方法实际上比导入全局变量更糟糕。在PHP中,类是全局符号,因此每次调用静态方法时都依赖全局符号(类名)。这是一个当全球是邪恶的时候。我在使用Zend框架的某个组件时遇到了这种方法的问题。有些类使用静态方法调用(工厂)来构建对象。我不可能为那个实例提供另一个工厂,以便返回一个定制的对象。解决这个问题的方法是只使用实例和instace方法,在程序的开头执行singleton等。

惯性矩?KoHevery是谷歌的一名敏捷教练,他有一个有趣的理论,或者更确切地说,我们应该将对象创建时间与使用对象的时间分开。所以程序的生命周期被分成两部分。第一部分(比如main()方法),它负责处理应用程序中的所有对象连接,以及完成实际工作的部分。

因此,不必:

1
2
3
4
5
6
7
class HttpClient
{
    public function request()
    {
        return HttpResponse::build();
    }
}

我们宁愿这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HttpClient
{
    private $httpResponseFactory;

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

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

然后,在索引/主页中,我们会这样做(这是对象连接步骤,或者是创建程序要使用的实例图的时间):

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

主要的想法是将依赖项从类中分离出来。这样,代码的可扩展性就大大提高了,对我来说,这是最重要的部分,也是可测试的。为什么测试更重要?因为我并不总是编写库代码,所以可扩展性并不那么重要,但是在重构时可测试性很重要。不管怎样,可测试代码通常会生成可扩展代码,因此它不是真正的二者之一或情况。

惯性矩?Ko Hevery还明确区分了单件和单件(有或没有大写字母S)。区别很简单。索引/主目录中的接线强制使用小写"S"的单例。您实例化了一个不实现单例模式的类的对象,并注意只将该实例传递给任何其他需要它的实例。另一方面,带有大写"S"的singleton是经典(反)模式的实现。基本上是一个伪装的全局,在PHP世界中没有太多用途。到目前为止我还没见过。如果希望所有类都使用单个DB连接,最好这样做:

1
2
3
4
5
$db = new DbConnection;

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

通过上面的操作,很明显我们有一个单例,而且我们也有一个在我们的测试中注入模拟或存根的好方法。令人惊讶的是,单元测试如何带来更好的设计。但是当你认为测试迫使你思考你使用代码的方式时,这是很有意义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 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框架使用静态属性来指定Zend_Db_Table实例中使用的db adapter类的名称。我已经用了一段时间了,所以它可能不再相关,但我就是这样记得的。

不处理静态属性的静态方法应该是函数。PHP有函数,我们应该使用它们。


所以在PHP中,静态可以应用于函数或变量。非静态变量绑定到类的特定实例。非静态方法作用于类的实例。所以我们来组成一个名为BlogPost的类。

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

此类的外观如下:

1
2
3
4
5
6
7
8
class blog_post {
    public $title;
    public $my_dao;

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

另一方面,使用静态函数,可以编写这样的类:

1
2
3
4
5
class blog_post_helper {
    public static function find_related($blog_post) {
         // Do stuff.
    }
}

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

从根本上说,这是一个面向对象设计的问题。类是系统中的名词,作用于它们的函数是动词。静态函数是过程函数。将函数的对象作为参数传入。

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

在OO场景中,您在BlogPost类上编写方法,这些方法作用于单个日志,并且编写静态方法,这些方法作用于日志集合。


Is it all just style?

很长的路,是的。您可以编写非常好的面向对象程序,而不必使用静态成员。事实上,有些人会认为静态成员首先是不纯洁的。我建议,作为OOP的初学者,您应该尽量避免使用静态成员。它将迫使您以面向对象的方式而不是以过程的方式来写作。


在这里,我对大多数答案都有不同的处理方法,特别是在使用PHP时。我认为所有类都应该是静态的,除非您有充分的理由不这样做。一些"为什么不"的原因是:

  • 您需要该类的多个实例
  • 你的课需要延长
  • 代码的某些部分不能与任何其他部分共享类变量

我举个例子。因为每个PHP脚本都生成HTML代码,所以我的框架有一个HTML编写器类。这确保了其他类不会尝试编写HTML,因为它是一个专门的任务,应该集中到一个类中。

通常,您将使用如下HTML类:

1
2
3
html::set_attribute('class','myclass');
html::tag('div');
$str=html::get_buffer();

每次调用get_buffer()时,它都会重置所有内容,以便下一个使用HTML编写器的类以已知状态启动。

我所有的静态类都有一个init()函数,在第一次使用该类之前需要调用该函数。这是惯例而非必然。

在这种情况下,静态类的替代方法是混乱的。您不希望每个需要编写一点点HTML的类都必须管理HTML编写器的实例。

现在我给您举一个例子,说明何时不使用静态类。我的表单类管理表单元素列表,如文本输入、下拉列表等。它通常是这样使用的:

1
2
3
4
$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中,所有东西都是静态的,除非有一个原因不应该这样做。


"在其他方法中进行静态方法调用实际上比导入全局变量更糟。"(定义"更糟")…而"不处理静态属性的静态方法应该是函数"。

这两种说法都相当笼统。如果我有一组与主题相关的函数,但是实例数据完全不合适,那么我更愿意在类中定义它们,而不是在全局命名空间中定义它们中的每一个。我只是在使用PHP5中提供的机械装置

  • 给他们一个名称空间——避免任何名称冲突
  • 把它们放在一起,而不是分散在一个项目中——其他开发人员可以更容易地找到已经有的东西,并且不太可能重新发明轮子。
  • 让我使用类常量而不是全局定义来定义任何魔力值。

这完全是一种增强高内聚性和低耦合的方便方法。

fwiw——没有"静态类"这样的东西,至少在php5中是如此;方法和属性可以是静态的。为了防止类的实例化,还可以将其声明为抽象的。


首先问问你自己,这个物体代表什么?对象实例适合于对单独的动态数据集进行操作。

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

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

这两个连接现在可以独立运行:

1
2
$someRecordsFromDb1 = $db1->getRows($selectStatement);
$someRecordsFromDb2 = $db2->getRows($selectStatement);

现在在这个包/库中,可能还有其他类,如db_row等,来表示从select语句返回的特定行。如果这个db_row类是一个静态类,那么这将假设您在一个数据库中只有一行数据,并且不可能像对象实例那样做。通过一个实例,您现在可以在无限数量的数据库中无限数量的表中拥有无限数量的行。唯一的限制是服务器硬件;)。

例如,如果db对象上的getrows方法返回db_row对象的数组,则现在可以独立地对每一行进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

静态类的一个好例子是处理注册表变量或会话变量,因为每个用户只有一个注册表或一个会话。

在应用程序的一部分中:

1
Session::set('someVar', 'toThisValue');

在另一部分:

1
Session::get('someVar'); // returns 'toThisValue'

因为每次会话只有一个用户,所以没有必要为会话创建实例。

我希望这能有所帮助,以及其他帮助解决问题的答案。作为旁注,检查"内聚"和"耦合"。它们概述了在编写适用于所有编程语言的代码时要使用的一些非常非常好的实践。


如果您的类是静态的,这意味着您不能将它的对象传递给其他类(因为没有实例可能),所以这意味着您的所有类都将直接使用该静态类,这意味着您的代码现在与该类紧密耦合。

紧密耦合会降低代码的可重用性、脆弱性和易出错性。您希望避免静态类能够将类的实例传递给其他类。

是的,这只是许多其他原因中的一个,其中一些已经被提到。


我想说的是,在跨语言应用程序中,绝对有一种情况需要静态变量。您可以有一个将语言传递给的类(例如,$会话[‘语言’]),它反过来访问设计为这样的其他类:

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

使用strings::getstring("somestring")是从应用程序中抽象出语言用法的一种好方法。不过,您也可以这样做,但在这种情况下,每个字符串文件都有常量,其中字符串值由strings类访问,效果很好。


通常,您应该使用成员变量和成员函数,除非它必须在所有实例之间共享,或者除非您正在创建单例。使用成员数据和成员函数允许您对多个不同的数据块重复使用您的函数,而如果您使用静态数据和函数,则只能有一个操作数据的副本。另外,虽然不适用于PHP,但是静态函数和数据会导致代码不可重入,而类数据有助于重入。