Skip to content

10. 分层应用程序

10.1. 引言

Image

接下来,我们将探讨如何将 PHP 应用程序按层进行结构化:

Image

在此图中,左侧层调用右侧层。各层的作用如下:

  • [1]:名为 [DAO](数据访问对象)的层负责处理与外部数据存储 [4](文件、数据库、Web 服务等)的交互。 该层有时也被称为 [DAL](数据访问层),这一术语更能准确描述该层的作用。该层可以读取数据 [3] 或写入数据 [2]。它由 [业务][6] 调用,并向其返回结果 [7]
  • [5]:名为[业务]的层,包含“业务”流程,这些流程会获得所需的所有数据。这通常是项目中最稳定的层,因为它不依赖于数据的获取方式。数据来自两个来源:
    • [9]:由 PHP 脚本提供的数据;
    • [6,7]:从[DAO]层请求的数据。
  • [8]:主脚本充当协调者。在控制台应用程序中,它将:
    • 创建 [business] [DAO] 层;
    • 执行应用程序的算法。该算法如同协调器:不包含任何“业务”逻辑或数据访问代码。主脚本仅调用[业务]层的程序[9]。它完全忽略[DAO]层和外部数据。它可以向[业务]层提供数据[9]。 在控制台应用程序中,这些数据可能来自配置文件或脚本用户。它从 [业务] 层接收结果 [10]。它可能需要存储某些结果:为此,它再次使用 [业务] 层的过程,而该层将调用 [DAO] 层来执行任务;
    • 在接收的结果中,主脚本可能会收到异常。其职责是处理从所有层级传播上来的异常;

这种分层架构旨在促进应用程序的演进。为此,每一层都实现了相应的接口。假设:

  • [DAO] 层由一个实现 [IDao] 接口的 [DAO] 类实现;
  • [业务]层由一个实现[IBusiness]接口的[Business]类实现;

因此,[业务]层中的过程将使用[IDao]接口,而非[Dao]类。这使得[Dao]层可以进行更新,而不会影响[业务]层。假设在版本1中,[Dao1]层使用来自数据库的数据。 经过更新,在版本 2 中,这些数据现在由 Web 服务 [Dao2] 提供。我们将确保 [Dao1][Dao2] 类都实现相同的 [IDao] 接口,并抛出相同的异常。如果验证通过,与 [IDao] 接口交互的 [Business] 层将保持不变;

同样的逻辑也适用于 [Business] 层。

让我们来看一个使用类和接口的实现:

Image

该应用程序将实现以下分层结构:

Image

10.2. 层之间交换的对象

通常,图层之间会交换各种对象。在此,它们将交换以下类 [Person.php]


<?php
 
// a person
class Personne {
  // identifier
  private $id;
 
  // manufacturer
  public function __construct(int $id) {
    $this->id = $id;
  }
 
  // toString
  public function __toString(): string {
    return "[Personne($this->id)]";
  }
 
}

评论

  • 第 6 行:Person 类仅有一个属性,即其标识符 [$id]。我们将假设该标识符在数据仓库中唯一标识某个人;
  • 第 9–11 行:构造函数,允许我们使用 ID 创建 [Person] 对象;
  • 第 14–16 行:[__toString] 方法,用于显示该人员的标识符;

10.3. [DAO] 层

[dao, 1] 层实现的 [IDao] 接口如下 [IDao.php]


<?php
 
 
// layer [dao] ------------------------
interface IDao {
 
  // recovery of a person from an external warehouse
  // pass the person's login
  public function get(int $id): Personne;
 
  // saving a person in an external warehouse
  public function save(Personne $p): void;
}

该接口由以下 [Dao1][Dao1.php] 实现:


<?php
 
class Dao1 implements IDao {
 
  // saving a person in an external warehouse
  public function save(Personne $p): void {
    print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
  }
 
  // recovery of a person from an external warehouse
  public function get(int $id): Personne {
    print "[Dao1] : Récupération de la personne d'identité ($id) en base de données [locale]\n";
    return new Personne($id);
  }
 
}

评论

  • 该类不与数据仓库交互。我们仅显示消息以跟踪代码的执行(第 7 行和第 12 行);
  • 第13行:我们返回一个ID与方法参数(第11行)相匹配的人员;

我们还通过以下 [Dao2][Dao2.php] 实现了 [IDao] 接口:


<?php
 
class Dao2 implements IDao {
 
  // saving a person in an external warehouse
  public function save(Personne $p): void {
    print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
  }
 
  // recovery of a person from an external warehouse
  public function get(int $id): Personne {
    print "[Dao2] : Récupération de la personne d'identité ($id) en base de données [distante]\n";
    return new Personne($id);
  }
 
}

[Dao2] 类与 [Dao1] 类类似,只是我们修改了显示的消息。

10.4. [业务] 层

[business, 2] 层提供了以下 [IMetier] 接口 [IMetier.php]


<?php
 
// business] layer ------------------------
interface IMétier extends IDao {
 
  // we use a person identified by his id
  public function doSomething(Personne $p): void;
}

注释

  • [IMétier] 接口继承自 [IDao] 接口。这完全不是强制要求的。我们在这里这样做是因为示例很简单;
  • 第 7 行:[doSomething] 方法是 [Business] 层特有的;

[IMetier] 接口将由以下 [Métier][Métier.php] 实现:


<?php
 
class Métier implements IMétier {
  // layer [dao]
  private $dao;
 
  // getter / setter
  public function setDao(IDao $dao): void {
    $this->dao = $dao;
  }
 
  public function getDao(): IDao {
    return $this->dao;
  }
 
  // a person is exploited
  public function doSomething(Personne $p): void {
    // treatment
    print "Métier : Traitement métier de la personne $p\n";
  }
 
  // safeguarding the individual
  public function save(Personne $p): void {
    print "[Métier] : Sauvegarde de la personne $p\n";
    // the [dao] layer is asked to make the backup
    $this->dao->save($p);
  }
 
  public function get(int $id): Personne {
    print "[Métier] : récupération de la personne d'identifiant $id\n";
    // we ask for the person in the diaper [dao]
    $personne = $this->dao->get($id);
    return $personne;
  }
 
}

评论

  • 第 5 行:[业务] 层必须持有对 [DAO] 层的引用,才能使用其方法;
  • 第 8–10 行:[setDao] 方法允许向 [业务] 层提供对 [DAO] 层的引用。 请注意,参数类型是 [IDao]。这意味着 [业务] 层将能够与任何实现 [IDao] 接口的类进行交互。如果我们将 [Dao1] 层替换为 [Dao2] 层,且两者都实现了 [IDao] 接口,则无需重写 [业务] 层;
  • 第 17–20 行:[IMetier::doSomething] 的实现;
  • 第 23–27 行:[IMetier::save] 的实现。该方法必须将人员数据保存至数据仓库。[业务] 层无法直接执行此操作,因此会调用 [DAO] 层来完成保存;
  • 第 29–34 行:实现 [IMetier::get]。该方法必须从数据仓库中检索 ID 作为参数传递给它的用户。[业务] 层不知道如何执行此操作。它调用 [DAO] 层来完成这项工作(第 32 行);

结论

每当[业务]层需要访问存储在数据仓库中的数据时,都必须通过[DAO]层,该层正是为访问这些数据而创建的。

10.5. 主脚本

我们将编写两个脚本,作为该应用程序的协调器。第一个 [main1.php] 将使用 [Dao1] 层,而第二个 [main2.php] 将使用 [Dao2] 层。我们希望以此证明,这不会对 [业务] 层的代码产生任何影响。

[main1.php]脚本如下:


<?php
 
// strict adherence to function parameters
declare (strict_types=1);
 
// inclusion classes and interfaces
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao1.php";
require_once __DIR__."/Métier.php";
 
// test ----------------
// layer creation
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// using the [business] layer
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);

注释

  • 让我们回顾几个要点:[main1.php] 脚本充当应用程序的协调者。它创建应用程序的分层结构(第 15–17 行),然后开始与 [business] 层进行通信(第 19–21 行)。分层结构如下:

Image

根据此图,[main1.php]脚本应仅与[business]层交互。即使理论上可行,它也不应与[DAO]层交互。

执行结果如下:


[Métier] : récupération de la personne d'identifiant 4
[Dao1] : Récupération de la personne d'identité (4) en base de données [locale]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao1] : Sauvegarde de la personne [Personne(4)] en base de données [locale]

注释

  • 代码第 10 行导致结果中的第 1 行和第 2 行被写入;
  • 代码第20行导致结果中的第3行被写入;
  • 代码的第 21 行导致结果的第 4 行和第 5 行被写入;

脚本 [main2.php] 如下:


<?php
 
// strict adherence to function parameters
declare (strict_types=1);
 
// inclusion classes and interfaces
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao2.php";
require_once __DIR__."/Métier.php";
 
// test ----------------
// layer creation
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// using the [business] layer
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);

注释

  • 第 36–38 行:分层架构现在使用 [Dao2] 层;

执行结果如下:


[Métier] : récupération de la personne d'identifiant 4
[Dao2] : Récupération de la personne d'identité (4) en base de données [distante]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao2] : Sauvegarde de la personne [Personne(4)] en base de données [distante]

[Dao1] 层模拟对本地数据库的访问,而 [Dao2] 层模拟对远程数据库的访问。只要这两个层都遵循 [IDao] 接口,我们可以看到 [Business] 层的代码无需修改。

我们将把刚刚学到的知识应用到作为本教程主线的税费计算练习中。