四、结构设计模式

结构设计模式提供了创建类结构的不同方法;例如,我们可以使用封装从较小的对象创建较大的对象。它们的存在使我们能够识别实现实体之间关系的简单方法,从而简化设计。

在最后一章中,我们介绍了如何使用创造模式来确定应该如何创建对象;通过结构模式,我们可以确定类之间的结构和关系。

在简要介绍了敏捷软件体系结构之后,在本章中,我们将介绍以下主题:

  • 装饰者模式
  • 类适配器模式
  • 对象适配器模式
  • 飞锤图案
  • 复合图案
  • 桥型
  • 代理模式
  • 外观模式

敏捷软件架构

许多组织倾向于采用敏捷的项目管理形式。这给架构师的角色带来了新的关注;事实上,有些人认为敏捷和架构是冲突的。敏捷宣言的两个原始签署者,MartinFowler 和 RobertCecilMartin,一直在口头上反对这个想法。事实上,福勒清楚地阐明了一个事实,即尽管敏捷宣言反对大型前期设计(如您在 Prince2 中看到的类型),但敏捷并没有拒绝前期设计本身。

计算机科学家艾伦·霍卢布也有类似的观点。敏捷专注于做一些对交付对用户有用的软件非常重要的事情,而不是仅仅对销售人员有用的软件。为了使软件能够长期使用,它必须具有适应性、可扩展性和可维护性。

Fowler 还对软件开发团队中的架构师有一个愿景。引用一个事实,即不可逆的软件可能会在以后带来最头痛的问题,这就是架构决策的关键所在。除此之外,他声称架构师的角色应该是寻求使这些决策可逆,从而完全缓解问题。

在许多大型软件部署过程中,可能会使用短语我们正处于不归路的时刻。在停止返回点之后,无法将部署恢复到其原始状态。软件有它自己的不归路,当软件变得更难重写时,就要简单地重建。虽然软件可能无法达到这个不归路点的最坏情况,但在可维护性方面的困难会给业务带来困难。

Fowler 还指出,在许多情况下,软件架构师甚至不检查软件是否符合其原始设计。通过与架构师配对编程,以及架构师审查代码更改(即拉动请求),他们可以获得理解,以便向开发人员提供反馈,并减轻进一步的技术负担。

在本书中,您可能会注意到 UML 的缺乏;这是因为在这里我不认为 UML 是必要的。我的意思是,我们都在用 PHP 说话,对吗?不过,您可能会发现 UML 在您的团队中很有用。

架构(architecture)过程通常会产生可交付成果;我们将该可交付成果称为工件。在敏捷团队中,这些工件可能是以进化的方式开发的,而不是作为一个前期产品,但是在敏捷环境中进行架构设计是完全可能的。

事实上,我认为体系结构使在敏捷环境中工作更加容易。当编程到接口或抽象层时,替换类要容易得多;在敏捷环境中,需求可能会发生变化,这意味着可能需要替换类。软件只有在对最终客户机有用时才有用。敏捷可以帮助做到这一点,但为了敏捷,您的代码必须是自适应的。拥有优秀的架构对于这一目标至关重要。

当我们编写代码时,我们应该防御性地编写代码。然而,对手不是敌人,而是我们自己。降低可靠代码的最快方法之一是将其编辑为弱代码。

装饰师

装饰器只是向单个类添加附加功能而不影响同一类中其他对象的行为。

Robert C.Martin(我在本章开头介绍了他)简单地提出的单一责任原则是一个类应该只有一个改变的理由。

该原则指出,每个模块或类都应该有一个单独的职责,并且该职责应该完全由该类封装。该课程的所有服务都应与该职责保持一致。Martin 总结了这一点,将责任定义如下:

“分配给唯一参与者的费用,以表明其对唯一业务任务的责任”。

通过使用 Decorator 设计模式,我们能够确保在具有独特关注领域的类之间划分功能,从而遵守单一责任原则。

让我们首先声明我们的Book接口。这就是我们希望我们的书能够产生的:

<?php 

interface Book 
{ 
  public function __construct(string $title, string $author, string $contents); 

  public function getTitle(): string; 

  public function getAuthor(): string; 

  public function getContents(): string; 
} 

然后我们可以申报我们的EBook.php类。这是我们将用PrintBook课程装饰的课程:

<?php 

class EBook implements Book 
{ 

  public $title; 
  public $author; 
  public $contents; 

  public function __construct(string $title, string $author, string $contents) 
  { 
    $this->title = $title; 
    $this->author = $author; 
    $this->contents = $contents; 
  } 

  public function getTitle(): string 
  { 
    return $this->contents; 
  } 

  public function getAuthor(): string 
  { 
    return $this->author; 
  } 

  public function getContents(): string 
  { 
    return $this->contents; 
  } 
} 

现在我们可以申报我们的PrintBook类了。这是我们用来装饰EBook类的:

<?php 

class PrintBook implements Book 
{ 

  public $eBook; 

  public function __construct(string $title, string $author, string $contents) 
  { 
    $this->eBook = new EBook($title, $author, $contents); 
  } 

  public function getTitle(): string 
  { 
    return $this->eBook->getTitle(); 
  } 

  public function getAuthor(): string 
  { 
    return $this->eBook->getAuthor(); 
  } 

  public function getContents(): string 
  { 
    return $this->eBook->getContents(); 
  } 

  public function getText(): string 
  { 
    $contents = $this->eBook->getTitle() . " by " . $this->eBook->getAuthor(); 
    $contents .= "\n"; 
    $contents .= $this->eBook->getContents(); 

    return $contents; 
  } 
} 

现在让我们用我们的index.php文件来测试这一切:

<?php 

require_once('Book.php'); 
require_once('EBook.php'); 
$PHPBook = new EBook("Mastering PHP Design Patterns", "Junade Ali", "Some contents."); 

require_once('PrintBook.php'); 
$PHPBook = new PrintBook("Mastering PHP Design Patterns", "Junade Ali", "Some contents."); 
echo $PHPBook->getText(); 

输出如下所示:

Some contents. by Junade Ali 
Some contents. 

适配器

有两种类型的适配器模式。在可能的情况下,我更喜欢对象适配器而不是类适配器;稍后我会详细解释这一点。

适配器模式允许现有类与它不匹配的接口一起使用。它通常用于允许现有类与其他类一起工作,而无需修改它们的源代码。

这在多态设置中非常有用,因为您使用的是第三方库,每个库都有自己的接口。

从根本上说,适配器可以帮助两个不兼容的接口协同工作。否则,通过将一个类的接口转换为客户机期望的接口,可以使不兼容的类协同工作。

类适配器

在类适配器中,我们使用继承来创建适配器。一个类(适配器)可以继承另一个类(适配器);使用标准继承,我们可以向 adaptee 添加额外的功能。

假设我们在ATM.php文件中有一个ATM类:

<?php 

class ATM 
{ 
  private $balance; 

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

  public function withdraw(float $amount): float 
  { 
    if ($this->reduceBalance($amount) === true) { 
      return $amount; 
    } else { 
      throw new Exception("Couldn't withdraw money."); 
    } 
  } 

  protected function reduceBalance(float $amount): bool 
  { 
    if ($amount >= $this->balance) { 
      return false; 
    } 

    $this->balance = ($this->balance - $amount); 
    return true; 
  } 

  public function getBalance(): float 
  { 
    return $this->balance; 
  } 
} 

让我们创建我们的ATMWithPhoneTopUp.php来形成我们的适配器:

<?php 

class ATMWithPhoneTopUp extends ATM 
{ 
  public function getTopUp(float $amount, int $time): string 
  { 
    if ($this->reduceBalance($amount) === true) { 
      return $this->generateTopUpCode($amount, $time); 
    } else { 
      throw new Exception("Couldn't withdraw money."); 
    } 
  } 

  private function generateTopUpCode(float $amount, int $time): string 
  { 
    return $amount . $time . rand(0, 10000); 
  } 
} 

让我们把这些都打包成一个index.php文件:

<?php 

require_once('ATM.php'); 

$atm = new ATM(500.00); 
$atm->withdraw(50); 
echo $atm->getBalance(); 
echo "\n"; 

require_once('ATMWithPhoneTopUp.php'); 

$adaptedATM = new ATMWithPhoneTopUp(500.00); 
echo "Top-up code: " . $adaptedATM->getTopUp(50, time()); 
echo "\n"; 
echo $adaptedATM->getBalance(); 

既然我们已经调整了初始的ATM类以生成充值代码,我们现在就可以利用这个新的充值功能了。所有这些的输出如下:

450 
Top-up code: 5014606939121598 
450 

注意,如果我们想要适应多个适配器,这在 PHP 中是很困难的。

在 PHP 中,多重继承是不可能的,除非您使用的是 Traits。在这种情况下,我们只能调整一个类来匹配另一个类的接口。

我们不使用这种方法的另一个关键架构原因是,与继承相比,更喜欢组合通常是一种好的设计(如复合重用原则所述)。

为了更详细地探讨这一原理,我们需要了解对象适配器。

对象适配器

复合重用原则指出,类应该通过其组合实现多态行为和代码重用。

通过应用此原则,类在想要实现特定功能时应该包含其他类的实例,而不是从基类或父类继承功能。

为此,“四人帮”声明如下:

“喜欢‘对象组合’而不是‘类继承’”

为什么这个原则如此重要?考虑最后一个示例,在这里我们使用类继承;在这种情况下,没有正式的保证我们的适配器会匹配我们想要的接口。如果父类公开了我们不希望适配器使用的函数,该怎么办?构图给了我们更多的控制。

通过使用组合而不是继承,我们能够更好地支持在面向对象编程中非常重要的多态行为。

假设我们有一个类来生成保险费。它提供月度保险费和年度保险费,具体取决于客户支付保险费的方式。通过每年付款,客户可以节省相当于半个月的费用:

<?php 

class Insurance 
{ 
  private $limit; 
  private $excess; 

  public function __construct(float $limit, float $excess) 
  { 
    if ($excess >= $limit) { 
      throw New Exception('Excess must be less than premium.'); 
    } 

    $this->limit = $limit; 
    $this->excess = $excess; 
  } 

  public function monthlyPremium(): float 
  { 
    return ($this->limit-$this->excess)/200; 
  } 

  public function annualPremium(): float 
  { 
    return $this->monthlyPremium()*11.5; 
  } 
} 

让我们假设一个市场比较工具多态地使用前面提到的类来实际计算来自多个不同供应商的保险报价;他们使用此接口执行以下操作:

<?php 

interface MarketCompare 
{ 
  public function __construct(float $limit, float $excess); 
  public function getAnnualPremium(); 
  public function getMonthlyPremium(); 
} 

因此,我们可以使用此接口构建对象适配器,以确保我们的Insurance类,即我们的 premium generator,与市场比较工具期望的接口相匹配:

<?php 

class InsuranceMarketCompare implements MarketCompare 
{ 
  private $premium; 

  public function __construct(float $limit, float $excess) 
  { 
    $this->premium = new Insurance($limit, $excess); 
  } 

  public function getAnnualPremium(): float 
  { 
    return $this->premium->annualPremium(); 
  } 

  public function getMonthlyPremium(): float 
  { 
    return $this->premium->monthlyPremium(); 
  } 
} 

请注意,类实际上是如何进行的,并根据它试图适应的内容实例化它自己的类。

然后适配器将此类存储在一个private变量中。然后我们在private变量中使用这个对象来代理请求。

适配器(类适配器和对象适配器)应充当粘合代码。我的意思是适配器不应该执行任何计算,它们只是作为不兼容接口之间的代理。

标准做法是将逻辑排除在粘合代码之外,并将逻辑留给我们正在适应的代码。如果在这样做的过程中,我们遇到了单一责任原则,我们需要调整另一个类。

正如我前面提到的,在一个类适配器中调整多个类实际上是不可能的,因此您必须将这样的逻辑包装在一个 Trait 中,或者我们需要使用一个对象适配器,就像我们在这里讨论的那样。

让我们试试这个适配器。为此,我们将编写以下index.php文件,以查看我们的新类是否与预期的接口匹配:

<?php 

require_once('Insurance.php'); 

$quote = new Insurance(10000, 250); 
echo $quote->monthlyPremium(); 
echo "\n"; 

require_once('MarketCompare.php'); 
require_once('InsuranceMarketCompare.php'); 

$quote = new InsuranceMarketCompare(10000, 250); 
echo $quote->getMonthlyPremium(); 
echo "\n"; 
echo $quote->getAnnualPremium(); 

输出应如下所示:

48.75 
48.75 
560.625 

与类适配器方法相比,此方法的主要缺点是必须实现公共方法,即使这些方法只是转发方法。

飞锤

与现实生活一样,并非所有对象都易于创建,有些对象可能会占用过多的内存。FlyWeight 设计模式可以通过与类似对象共享尽可能多的数据来帮助我们最小化内存使用。

这种设计模式在大多数 PHP 应用程序中的使用是有限的,但在非常有用的奇怪情况下,仍然值得了解它。

假设我们有一个带有draw方法的Shape接口:

<?php 

interface Shape 
{ 
  public function draw(); 
} 

让我们创建一个实现这个接口的Circle类。在实现这一点时,我们可以使用 X 和 Y 坐标设置圆的位置。我们还可以设置圆的半径并绘制它(打印此信息)。请注意如何在类外设置颜色特征。

这其中有一个非常重要的原因。在我们的示例中,颜色与状态无关;这是循环的固有部分。然而,圆的位置和大小取决于状态,因此是外部的。当需要 FlyWeight 对象的功能时,将外部状态信息传递给该对象;但是,固有选项独立于 FlyWeight 的每个过程。当我们谈到这个工厂是如何建造的时,这将更有意义。

这是重要的信息:

  • 外在:状态属于对象的外部上下文,使用时输入到对象中。
  • 内在:自然属于对象的状态,因此应该是永久的、不可变的(内部的)或上下文无关的。

考虑到这一点,让我们把Shape接口的实现放在一起。这是我们的Circle课程:

<?php 

class Circle implements Shape 
{ 

  private $colour; 
  private $x; 
  private $y; 
  private $radius; 

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

  public function setX(int $x) 
  { 
    $this->x = $x; 
  } 

  public function setY(int $y) 
  { 
    $this->y = $y; 
  } 

  public function setRadius(int $radius) 
  { 
    $this->radius = $radius; 
  } 

  public function draw() 
  { 
    echo "Drawing circle which is " . $this->colour . " at [" . $this->x . ", " . $this->y . "] of radius " . $this->radius . "."; 
    echo "\n"; 
  } 
} 

有了这个,我们现在可以构建我们的ShapeFactory,它实际上实现了 FlyWeight 模式。具有我们选择的颜色的对象在需要时实例化,然后存储以供以后使用:

<?php 

class ShapeFactory 
{ 
  private $shapeMap = array(); 

  public function getCircle(string $colour) 
  { 
    $circle = 'Circle' . '_' . $colour; 

    if (!isset($this->shapeMap[$circle])) { 
      echo "Creating a ".$colour." circle."; 
      echo "\n"; 
      $this->shapeMap[$circle] = new Circle($colour); 
    } 

    return $this->shapeMap[$circle]; 
  } 
} 

让我们在index.php文件中演示这是如何工作的。

为了实现这一点,我们在随机位置创建具有随机颜色的100对象:

require_once('Shape.php'); 
require_once('Circle.php'); 
require_once('ShapeFactory.php'); 

$colours = array('red', 'blue', 'green', 'black', 'white', 'orange'); 

$factory = new ShapeFactory(); 

for ($i = 0; $i < 100; $i++) { 
  $randomColour = $colours[array_rand($colours)]; 

  $circle = $factory->getCircle($randomColour); 
  $circle->setX(rand(0, 100)); 
  $circle->setY(rand(0, 100)); 
  $circle->setRadius(100); 

  $circle->draw(); 
} 

现在,让我们来看看输出。您可以看到,我们已经绘制了 100 个圆,但我们只需要实例化一小部分圆,因为我们正在缓存相同颜色的对象以供以后使用:

Creating a green circle. 
Drawing circle which is green at [29, 26] of radius 100\. 
Creating a black circle. 
Drawing circle which is black at [17, 64] of radius 100\. 
Drawing circle which is black at [81, 86] of radius 100\. 
Drawing circle which is black at [0, 73] of radius 100\. 
Creating a red circle. 
Drawing circle which is red at [10, 15] of radius 100\. 
Drawing circle which is red at [70, 79] of radius 100\. 
Drawing circle which is red at [13, 78] of radius 100\. 
Drawing circle which is green at [78, 27] of radius 100\. 
Creating a blue circle. 
Drawing circle which is blue at [38, 11] of radius 100\. 
Creating a orange circle. 
Drawing circle which is orange at [43, 57] of radius 100\. 
Drawing circle which is blue at [58, 65] of radius 100\. 
Drawing circle which is orange at [75, 67] of radius 100\. 
Drawing circle which is green at [92, 59] of radius 100\. 
Drawing circle which is blue at [53, 3] of radius 100\. 
Drawing circle which is black at [14, 33] of radius 100\. 
Creating a white circle. 
Drawing circle which is white at [84, 46] of radius 100\. 
Drawing circle which is green at [49, 61] of radius 100\. 
Drawing circle which is orange at [57, 44] of radius 100\. 
Drawing circle which is orange at [64, 33] of radius 100\. 
Drawing circle which is white at [42, 74] of radius 100\. 
Drawing circle which is green at [5, 91] of radius 100\. 
Drawing circle which is white at [87, 36] of radius 100\. 
Drawing circle which is red at [74, 94] of radius 100\. 
Drawing circle which is black at [19, 6] of radius 100\. 
Drawing circle which is orange at [70, 83] of radius 100\. 
Drawing circle which is green at [74, 64] of radius 100\. 
Drawing circle which is white at [89, 21] of radius 100\. 
Drawing circle which is red at [25, 23] of radius 100\. 
Drawing circle which is blue at [68, 96] of radius 100\. 
Drawing circle which is green at [74, 6] of radius 100\. 

你可能注意到了一些事情。我存储我们正在重用的 FlyWeight 对象缓存的方式是通过连接和颜色,例如圆【绿色】。显然,这在这个用例中是有效的,但是有一种更好的方法可以做到这一点;在 PHP 中,实际上可以获得给定对象的唯一 ID。我们将在下一个模式中介绍这一点。

复合材料

想象一下,一个由单曲和歌曲播放列表组成的音频系统。是的,播放列表由歌曲组成,但我们希望两者都能单独处理。两者都是音乐类型,都可以播放。

复合设计模式在这方面有所帮助;它允许我们忽略对象和单个对象的组成之间的差异。它允许我们使用相同或几乎相同的代码来处理这两者。

让我们举一个小例子;一首歌是我们的叶子的例子,播放列表是复合Music是我们对播放列表和歌曲的抽象;因此,我们可以将其称为我们的组件。所有这些的客户是我们的index.php文件。

通过不区分叶节点和分支,我们的代码变得不那么复杂,因此更不容易出错。

让我们首先为我们的Music定义一个接口:

<?php 

interface Music 
{ 
  public function play(); 
} 

现在让我们从Song类开始,将一些实现放在一起:

<?php 

class Song implements Music 
{ 
  public $id; 
  public $name; 

  public function  __construct(string $name) 
  { 
    $this->id = uniqid(); 
    $this->name = $name; 
  } 

  public function play() 
  { 
    printf("Playing song #%s, %s.\n", $this->id, $this->name); 
  } 
} 

现在我们可以开始组织我们的Playlist课程了。在本例中,您可能会注意到我使用名为spl_object_hash的函数在歌曲数组中设置了键。在处理对象数组时,此函数绝对是一件好事。

这个函数的作用是为每个对象返回一个唯一的散列,只要该对象没有被破坏,该散列就保持一致,而不管类的属性发生了什么变化。它提供了一种稳定的寻址任意对象的方法。一旦对象被销毁,散列就可以重新用于其他对象。

该函数不会对对象的内容进行散列;它仅用于显示内部句柄和 hander 表指针。这意味着,如果更改对象的属性,哈希值将不会更改。也就是说,它不能保证唯一性。如果一个对象被销毁,并且随后立即创建了一个相同的类,那么您将得到相同的哈希值,因为在第一个类被取消引用和销毁之后,PHP 将重用相同的内部句柄。

这是正确的,因为 PHP 可以使用内部句柄:

var_dump(spl_object_hash(new stdClass()) === spl_object_hash(new stdClass())); 

但是,这将是错误的,因为 PHP 必须创建一个新的处理程序:

$object = new StdClass(); 
var_dump(spl_object_hash($object) === spl_object_hash(new stdClass())); 

现在让我们回到我们的Playlist课程。让我们用它来实现我们的Music接口;下面是课程:

<?php 

class Playlist implements Music 
{ 
  private $songs = array(); 

  public function addSong(Music $content): bool 
  { 
    $this->songs[spl_object_hash($content)] = $content; 
    return true; 
  } 

  public function removeItem(Music $content): bool 
  { 
    unset($this->songs[spl_object_hash($content)]); 
    return true; 
  } 

  public function play() 
  { 
    foreach ($this->songs as $content) { 
      $content->play(); 
    } 
  } 
} 

现在让我们把这些都放在我们的index.php文件中。我们在这里做的是创建一些歌曲对象,其中一些我们将使用它们的addSong功能分配给播放列表。

由于播放列表的实现方式与歌曲相同,我们甚至可以将addSong功能与其他播放列表一起使用(在这种情况下,我们最好将addSong功能重命名为addMusic

然后我们播放父播放列表。这将播放子播放列表,并依次播放这些播放列表中的所有歌曲:

<?php 

require_once('Music.php'); 
require_once('Playlist.php'); 
require_once('Song.php'); 

$songOne = new Song('Lost In Stereo'); 
$songTwo = new Song('Running From Lions'); 
$songThree = new Song('Guts'); 
$playlistOne = new Playlist(); 
$playlistTwo = new Playlist(); 
$playlistThree = new Playlist(); 
$playlistTwo->addSong($songOne); 
$playlistTwo->addSong($songTwo); 
$playlistThree->addSong($songThree); 
$playlistOne->addSong($playlistTwo); 
$playlistOne->addSong($playlistThree); 
$playlistOne->play(); 

运行此脚本时,我们可以看到预期的输出:

Playing song #57106d5adb364, Lost In Stereo. 
Playing song #57106d5adb63a, Running From Lions. 
Playing song #57106d5adb654, Guts. 

大桥

桥接模式可以非常简单;它有效地允许我们将抽象与实现解耦,以便两者可以独立变化。

当类频繁变化时,桥接接口和具体类可以让开发人员更轻松地改变他们的类。

让我们提出一个通用的 messenger 接口,它能够发送某种形式的消息,Messenger.php

<?php 

interface Messenger 
{ 
  public function send($body); 
} 

此接口的一个具体实现是一个InstantMessenger应用程序InstantMessenger.php

<?php 

class InstantMessenger implements Messenger 
{ 
  public function send($body) 
  { 
    echo "InstantMessenger: " . $body; 
  } 
} 

类似地,我们可以对SMS应用程序SMS.php执行相同的操作:

<?php 

class SMS implements Messenger 
{ 
  public function send($body) 
  { 
    echo "SMS: " . $body; 
  } 
} 

我们现在可以为物理设备(发射机)创建一个接口,如果您愿意,Transmitter.php

<?php 

interface Transmitter 
{ 
  public function setSender(Messenger $sender); 

  public function send($body); 
} 

我们可以使用Device类将发送器与实现其方法的设备解耦。Device类将Transmitter接口连接到物理设备Device.php

<?php 

abstract class Device implements Transmitter 
{ 
  protected $sender; 

  public function setSender(Messenger $sender) 
  { 
    $this->sender = $sender; 
  } 
} 

因此,让我们组合一个具体的类来表示电话,Phone.php

<?php 

class Phone extends Device 
{ 
  public function send($body) 
  { 
    $body .= "\n\n Sent from a phone."; 

    return $this->sender->send($body); 
  } 
} 

让我们为Tablet做同样的事情。Tablet.php是:

<?php 

class Tablet extends Device 
{ 
  public function send($body) 
  { 
    $body .= "\n\n Sent from a Tablet."; 

    return $this->sender->send($body); 
  } 
} 

最后,让我们把这些都打包成一个index.php文件:

<?php 

require_once('Transmitter.php'); 
require_once('Device.php'); 
require_once('Phone.php'); 
require_once('Tablet.php'); 

require_once('Messenger.php'); 
require_once('SMS.php'); 
require_once('InstantMessenger.php'); 

$phone = new Phone(); 
$phone->setSender(new SMS()); 

$phone->send("Hello there!"); 

其结果如下:

SMS: Hello there! 

 Sent from a phone. 

代理模式

Proxy 是一个类,它仅仅是其他对象的接口。它可能是任何东西的接口;从网络连接、文件、内存中的大型对象或任何其他难以复制的资源。

在这里的示例中,我们将创建一个简单的代理,根据代理的实例化方式转发到两个对象之一。

通过访问一个简单的代理类,客户端可以从一个对象访问猫和狗的两个馈线,具体取决于它是否被实例化。

让我们首先为我们的AnimalFeeder定义一个接口:

<?php 

namespace IcyApril\PetShop; 

interface AnimalFeeder 
{ 
  public function __construct(string $petName); 

  public function dropFood(int $hungerLevel, bool $water = false): string; 

  public function displayFood(int $hungerLevel): string; 
} 

然后,我们可以为猫和狗定义两种动物喂食器:

<?php 

namespace IcyApril\PetShop\AnimalFeeders; 

use IcyApril\PetShop\AnimalFeeder; 

class Cat implements AnimalFeeder 
{ 
  public function __construct(string $petName) 
  { 
    $this->petName = $petName; 
  } 

  public function dropFood(int $hungerLevel, bool $water = false): string 
  { 
    return $this->selectFood($hungerLevel) . ($water ? ' with water' : ''); 
  } 

  public function displayFood(int $hungerLevel): string 
  { 
    return $this->selectFood($hungerLevel); 
  } 

  protected function selectFood(int $hungerLevel): string 
  { 
    switch ($hungerLevel) { 
      case 0: 
        return 'lamb'; 
        break; 
      case 1: 
        return 'chicken'; 
        break; 
      case 3: 
        return 'tuna'; 
        break; 
    } 
  } 
} 

这是我们的狗AnimalFeeder

<?php 

namespace IcyApril\PetShop\AnimalFeeders; 

class Dog 
{ 

  public function __construct(string $petName) 
  { 
    if (strlen($petName) > 10) { 
      throw new \Exception('Name too long.'); 
    } 

    $this->petName = $petName; 
  } 

  public function dropFood(int $hungerLevel, bool $water = false): string 
  { 
    return $this->selectFood($hungerLevel) . ($water ? ' with water' : ''); 
  } 

  public function displayFood(int $hungerLevel): string 
  { 
    return $this->selectFood($hungerLevel); 
  } 

  protected function selectFood(int $hungerLevel): string 
  { 
    if ($hungerLevel == 3) { 
      return "chicken and vegetables"; 
    } elseif (date('H') < 10) { 
      return "turkey and beef"; 
    } else { 
      return "chicken and rice"; 
    } 
  } 
} 

有了这个定义,我们现在可以创建我们的代理类,这个类本质上使用构造函数来破译它需要实例化的类的类型,然后将所有函数调用重定向到这个类。为了重定向函数调用,使用了__call magic方法。

这看起来像这样:

<?php 

namespace IcyApril\PetShop; 

class AnimalFeederProxy 
{ 
  protected $instance; 

  public function __construct(string $feeder, string $name) 
  { 
    $class = __NAMESPACE__ . '\\AnimalFeeders' . $feeder; 
    $this->instance = new $class($name); 
  } 

  public function __call($name, $arguments) 
  { 
    return call_user_func_array([$this->instance, $name], $arguments); 
  } 
} 

您可能已经注意到,我们必须使用名称空间在构造函数中手动创建类。我们使用__NAMESPACE__ magic常量查找当前名称空间,然后将其连接到类所在的特定子名称空间。请注意,我们必须使用另一个\转义\,以允许我们指定名称空间,而无需 PHP 将\解释为转义字符。

让我们构建我们的index.php文件,并利用代理类来构建对象:

<?php 

require_once('AnimalFeeder.php'); 
require_once('AnimalFeederProxy.php'); 

require_once('AnimalFeeders/Cat.php'); 
$felix = new \IcyApril\PetShop\AnimalFeederProxy('Cat', 'Felix'); 
echo $felix->displayFood(1); 
echo "\n"; 
echo $felix->dropFood(1, true); 
echo "\n"; 

require_once('AnimalFeeders/Dog.php'); 
$brian = new \IcyApril\PetShop\AnimalFeederProxy('Dog', 'Brian'); 
echo $brian->displayFood(1); 
echo "\n"; 
echo $brian->dropFood(1, true); 

结果如下:

chicken 
chicken with water 
turkey and beef 
turkey and beef with water 

那么你如何在现实中使用它呢?假设您从数据库中获得一条记录,其中包含一个详细说明动物类型和名称的对象;您可以将此对象传递给代理类的构造函数,并将其用作创建类的机制。

在实践中,当涉及到支持资源匮乏的对象时,这有一个很好的用例,您不一定要实例化这些对象,除非客户机确实需要它们;对于资源匮乏的网络连接和其他类型的资源也是如此。

立面

立面(也称为立面)设计模式是一件奇怪的事情;它们本质上充当复杂系统的简单接口。Facade 设计模式提供一个类,该类本身实例化其他类,并提供一个使用这些函数的简单接口。

当使用这样的模式时,一个警告是,当类在 Facade 中实例化时,本质上是紧密耦合它所使用的类。有些情况下你想要这个,但有些情况下你不想要。如果不希望出现这种行为,则更适合使用依赖项注入。

我发现,当将一组糟糕的 API 包装到一个统一的 API 中时,这非常有用。它减少了外部依赖性,允许将复杂性内部化;此过程可以使代码更具可读性。

我将用一个粗略的例子来演示这个模式,但这将有效地使机制变得显而易见。

让我为一家玩具厂提议三个类别。

Manufacturer(制造玩具的工厂)是一个简单的类,可以实例化一次要制造多少玩具:

<?php 

class Manufacturer 
{ 
  private $capacity; 

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

  public function build(): string 
  { 
    return uniqid(); 
  } 
} 

Post 类(shipping courier)是一个简单的函数,用于从工厂发送玩具:

<?php 

class Post 
{ 
  private $sender; 

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

  public function dispatch(string $item, string $to): bool 
  { 
    if (strlen($item) !== 13) { 
      return false; 
    } 

    if (empty($to)) { 
      return false; 
    } 

    return true; 
  } 
} 

SMS类通知客户他们的玩具已从工厂发货:

<?php 

class SMS 
{ 
  private $from; 

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

  public function send(string $to, string $message): bool 
  { 
    if (empty($to)) { 
      return false; 
    } 

    if (strlen($message) === 0) { 
      return false; 
    } 

    echo $to . " received message: " . $message; 
    return true; 
  } 
} 

下面是我们的ToyFactory类,它充当一个门面,将所有这些类链接在一起,并允许操作按顺序进行:

<?php 

class ToyShop 
{ 
  private $courier; 
  private $manufacturer; 
  private $sms; 

  public function __construct(String $factoryAdress, String $contactNumber, int $capacity) 
  { 
    $this->courier = new Post($factoryAdress); 
    $this->sms = new SMS($contactNumber); 
    $this->manufacturer = new Manufacturer($capacity); 
  } 

  public function processOrder(string $address, $phone) 
  { 
    $item = $this->manufacturer->build(); 
    $this->courier->dispatch($item, $address); 
    $this->sms->send($phone, "Your order has been shipped."); 
  } 
} 

最后,我们可以将所有这些都打包到我们的index.php文件中:

<?php 

require_once('Manufacturer.php'); 
require_once('Post.php'); 
require_once('SMS.php'); 
require_once('ToyShop.php'); 

$childrensToyFactory = new ToyShop('1 Factory Lane, Oxfordshire', '07999999999', 5); 
$childrensToyFactory->processOrder('8 Midsummer Boulevard', '07123456789'); 

运行此代码后,我们会看到来自SMS类的消息,显示文本消息已发送:

Facade

在其他情况下,在不同的类松散耦合在一起的情况下,我们可能会发现使用依赖注入更好。通过将执行各种操作的对象注入到ToyFactory类中,我们可以通过注入ToyFactory类可以操纵的伪类来简化测试,从而从中获益。

就个人而言,我非常相信让代码尽可能容易测试;因此,我不喜欢这种方法。

总结

本章通过介绍结构设计模式扩展了我们在前一章中开始学习的设计模式。

为此,我们学习了一些关键模式,以简化软件设计过程;这些模式确定了实现不同实体之间关系的简单方法:

  • 我们了解了 Decorator,了解了如何包装类以向其添加额外的行为,并且更重要的是,我们了解了这如何帮助我们遵守单一责任原则。
  • 我们学习了类适配器和对象适配器,以及它们之间的区别。这里的关键要点是为什么我们可以选择组合而不是继承。
  • 我们回顾了 FlyWeight 设计模式,它可以帮助我们以高效的方式执行某些过程。
  • 我们学习了复合设计模式如何帮助我们将对象的组合与单个对象一样对待。
  • 我们讨论了桥接设计模式,它使我们能够将抽象与其实现分离,允许两者独立变化。
  • 我们介绍了代理设计模式如何作为另一个类的接口,以及如何将其用作转发代理。
  • 最后,我们学习了如何使用 Facade 设计模式为复杂系统提供简单的接口。

在下一章中,我们将通过讨论行为模式来结束我们的设计模式部分,准备好接触架构模式。