设计模式

早在互联网出现之前,开发人员就已经开始创建代码,他们的工作涉及多个不同领域,而不仅仅是网络应用程序。正因为如此,很多人已经遇到过类似的情况,并积累了以前尝试解决相同问题的经验。简而言之,这意味着几乎可以肯定,已经有人设计出了解决你现在所面临问题的好方法。

已经有很多书籍试图将常见问题的解决方案归类,这也被称为 设计模式。设计模式并不是你复制粘贴到程序中的算法,它告诉你如何一步一步地解决某些问题,而是以启发式的方式告诉你如何寻找答案的秘诀。

如果你想成为一名专业的开发人员,学习它们是必不可少的,这不仅是为了解决问题,也是为了与其他开发人员交流。在讨论程序设计时,得到类似 "你可以在这里使用一个工厂" 的答案是很常见的。了解工厂是什么,比每次别人提到它时再解释这个模式要节省很多时间。

正如我们所说,有很多书籍都在谈论设计模式,我们强烈建议你去看看其中的一些。本节的目的是向你介绍什么是设计模式以及如何使用它。此外,我们还将向你展示一些在编写 Web 应用程序时最常用的 PHP 设计模式,但不包括 MVC 模式,我们将在第 6 章 "适应 MVC "中学习 MVC 模式。

除书籍外,您还可以访问 http://designpatternsphp.readthedocs.org/en/latest/README.html 上的开源项目 DesignPatternsPHP。这里收集了大量的设计模式,而且它们都是用 PHP 实现的,所以你会更容易适应。

工厂

工厂是创建组的一种设计模式,这意味着它允许你创建对象。你可能会认为我们不需要这样的东西,因为创建对象就像使用 new 关键字、类及其参数一样简单。但出于不同的原因,让用户这样做是很危险的。在对代码进行单元测试时,使用 new 会增加难度(在第 7 章 "测试 Web 应用程序" 中将学习单元测试),除此之外,还会在代码中添加大量耦合。

在讨论封装时,我们了解到最好隐藏类的内部实现,可以将构造函数视为类的一部分。原因是用户需要随时知道如何创建对象,包括构造函数的参数是什么。如果我们想改变构造函数以接受不同的参数呢?我们需要逐一访问所有创建过对象的地方并更新它们。

使用工厂的另一个原因是管理继承超类或实现相同接口的不同类。众所周知,由于多态性的存在,只要知道实现的接口,就可以使用一个对象,而无需知道它实例化的具体类。也许你的代码需要实例化一个实现了接口的对象并使用它,但对象的具体类可能根本不重要。

想想我们的书店例子。我们有两类顾客:基本顾客和高级顾客。但在大部分代码中,我们并不关心具体实例是哪种类型的顾客。事实上,我们的代码应该使用实现了客户接口的对象,而不考虑具体类型。因此,如果我们将来决定添加一种新类型,只要它实现了正确的接口,我们的代码就能顺利运行。但是,如果是这样的话,当我们需要创建一个新客户时该怎么办呢?我们不能实例化接口,所以还是使用工厂模式吧。在 src/Domain/Customer/ CustomerFactory.php 中添加以下代码:

<?php

namespace Bookstore\Domain\Customer;

use Bookstore\Domain\Customer;

class CustomerFactory {
    public static function factory(
        string $type,
        int $id,
        string $firstname,
        string $surname,
        string $email
    ): Customer {
        switch ($type) {
            case 'basic':
                return new Basic($id, $firstname, $surname, $email);
            case 'premium':
                return new Premium($id, $firstname, $surname, $email);
        }
    }
}
php

由于各种原因,前面代码中的工厂并不理想。在第一段中,我们使用了一个 switch,并为所有现有的客户类型添加了一个案例。两种类型没有太大区别,但如果我们有 19 种类型呢?让我们试着让这个工厂方法更有活力一些。

public static function factory(
        string $type,
        int $id,
        string $firstname,
        string $surname,
        string $email
    ): Customer {
    $classname = __NAMESPACE__ . '\\' . ucfirst($type);
    if (!class_exists($classname)) {
        throw new \InvalidArgumentException('Wrong type.');
    }
    return new $classname($id, $firstname, $surname, $email);
}
php

是的,你可以在 PHP 中实现前面的代码。动态实例化类,即使用变量的内容作为类的名称,是使 PHP 如此灵活的原因之一…​…​同时也是危险的。如果使用不当,会使你的代码变得非常难以阅读和维护,所以一定要小心。还要注意常量 __NAMESPACE__,它包含了当前文件的命名空间。

现在,这个工厂看起来更整洁了,而且非常动态。你可以添加更多的客户类型,只要它们在正确的命名空间内并实现了接口,工厂方面和工厂的使用都无需改变。

为了使用它,让我们修改 init.php 文件。可以删除所有测试,只保留自动加载器代码。然后添加以下内容:

CustomerFactory::factory('basic', 2, 'mary', 'poppins', 'mary@poppins.com');
CustomerFactory::factory('premium', null, 'james', 'bond', 'james@bond.com');
php

工厂设计模式的复杂程度可以随心所欲。它有不同的变体,每种变体都有其存在的理由和时间,但总体思路始终如一。

单例

如果对设计模式或一般网络开发稍有经验的人看到本节的标题,他们可能会开始撕扯自己的头发,并声称单例是设计模式中最糟糕的例子。不过,请听我慢慢道来。

在解释接口时,我补充说明了开发人员如何倾向于将代码复杂化,以便使用他们知道的所有工具。使用设计模式就是其中一种情况。设计模式已经非常有名,人们声称善于使用设计模式与优秀的开发人员直接相关,以至于每个学习设计模式的人都试图在任何地方使用它们。

单例模式可能是 PHP 用于 Web 开发的设计模式中最臭名昭著的一种。这种模式有一个非常特殊的目的,在这种情况下,这种模式被证明是非常有用的。但这种模式非常容易实现,以至于开发人员不断尝试在各处添加单子,导致代码变得无法维护。正因为如此,人们称这种模式为反模式,应该避免而不是使用。

我同意这种观点,但我仍然认为你应该非常熟悉这种设计模式。尽管你应该避免过度使用它,但人们还是会到处使用它,而且会无数次地提到它,所以你应该同意他们的观点,或者有足够的理由阻止他们使用它。说了这么多,让我们来看看单例模式的目的是什么。

单例的概念很简单:当你想让一个类始终有一个唯一的实例时,就会使用单例。每次在任何地方使用该类时,都必须使用同一个实例。这样做的原因是为了避免某些重型资源拥有过多实例,或者是为了在任何地方都保持相同的状态—​即全局性。数据库连接或配置处理程序就是这样的例子。

想象一下,我们的应用程序需要一些配置才能运行,例如数据库的凭据、特殊端点的 URL、查找库或重要文件的目录路径等等。收到请求后,首先要做的就是从文件系统中加载这些配置,然后将其存储为数组或其他数据结构。将以下代码保存为 src/Utils/Config. php 文件:

<?php

namespace Bookstore\Utils;

use Bookstore\Exceptions\NotFoundException;

class Config {
    private $data;

    public function __construct() {
        $json = file_get_contents(__DIR__ . '/../../config/app.json');
        $this->data = json_decode($json, true);
    }

    public function get($key) {
        if (!isset($this->data[$key])) {
            throw new NotFoundException("Key $key not in config.");
        }
        return $this->data[$key];
    }
}
php

正如您所看到的,此类使用了一个新的异常。在 src/Utils/NotFoundException.php 下创建它:

<?php

namespace Bookstore\Exceptions;

use Exception;

class NotFoundException extends Exception {
}
php

此外,该类还会读取一个文件 config/app.json。您可以在其中添加以下 JSON 映射:

{
    "db": {
        "user": "Luke",
        "password": "Skywalker"
    }
}
php

为了使用此配置,我们将以下代码添加到 init.php 文件中。

$config = new Config();
$dbConfig = $config->get('db');
var_dump($dbConfig);
php

这似乎是阅读配置的好方法,对吗?但请注意高亮显示的一行。我们实例化了 Config 对象,因此读取了一个文件,将其内容从 JSON 转换为数组并存储起来。如果文件包含数百行而不是六行呢?你应该注意到,实例化这个类是非常昂贵的。

您不希望每次从配置中获取数据时都要读取文件并将其转换为数组。那样成本太高了!但是,可以肯定的是,你会在代码的不同地方需要配置数组,你不可能走到哪里都带着这个数组。如果你了解静态属性和方法,你就会认为在对象内部实现一个静态数组应该可以解决这个问题。你只需实例化一次,然后调用一个静态方法,访问已经填充好的静态属性即可。理论上,我们可以跳过实例化,对吗?

<?php

namespace Bookstore\Utils;

use Bookstore\Exceptions\NotFoundException;

class Config {
    private static $data;

    public function __construct() {
        $json = file_get_contents(__DIR__ . '/../config/app.json');
        self::$data = json_decode($json, true);
    }

    public static function get($key) {
        if (!isset(self::$data[$key])) {
            throw new NotFoundException("Key $key not in config.");
        }
        return self::$data[$key];
    }
}
php

这似乎是个好主意,但却非常危险。你怎么能绝对确定数组已经填充完毕?又如何确保即使使用静态上下文,用户也不会一次又一次地实例化这个类?这就是单子的用武之地。

实现单例意味着以下几点:

  1. 将类的构造函数设置为私有,这样类之外的任何人都无法实例化该类。

  2. 创建一个名为 $instance 的静态属性,该属性将包含其自身的一个实例—​也就是说,在我们的 Config 类中,$instance 属性将包含 Config 类的一个实例。

  3. 创建静态方法 getInstance,该方法将检查 $instance 是否为空,如果为空,它将使用私有构造函数创建一个新实例。无论如何,该方法都将返回 $instance 属性。

让我们看看单例类是什么样子的:

<?php
namespace Bookstore\Utils;

use Bookstore\Exceptions\NotFoundException;

class Config {
    private $data;
    private static $instance;

    private function __construct() {
        $json = file_get_contents(__DIR__ . '/../config/app.json');
        $this->data = json_decode($json, true);
    }

    public static function getInstance(){
        if (self::$instance == null) {
            self::$instance = new Config();
        }
        return self::$instance;
    }

    public function get($key) {
        if (!isset($this->data[$key])) {
            throw new NotFoundException("Key $key not in config.");
        }
        return $this->data[$key];
    }
}
php

如果现在运行这段代码,会出现错误,因为该类的构造函数是私有的。第一个成就已解锁!让我们正确使用这个类:

$config = Config::getInstance();
$dbConfig = $config->get('db');
var_dump($dbConfig);
php

它能说服你吗?事实证明,它确实非常方便。但我必须强调的是:在使用这种设计模式时一定要小心,因为它有非常、非常、特殊的用例。避免掉入到处实施它的陷阱!