十、桥接
$> git checkout bridge
目的
将抽象与其实现解耦,这样两者可以独立变化。 1
应用
从实现中分离抽象意味着什么?为了展示一个这样的例子,让我们假设你想发送一条消息。A message
是一个抽象概念。然而,有许多不同的方式可以发送信息。你可以用电子邮件发送;把它作为一封信邮寄出去;在屋顶上大声说出来;把它贴到脸书、推特或其他网站上;把它放在瓶子里,扔进大海;或者在飞机后面挂横幅。见鬼,甚至还有一个向 外太空 2 提交短信的网站。这些都是Message
抽象的可能实现。
抽象结构
-
Abstraction
是使用来自Implementer
的methodA
的接口或类。见图 10-1 。图 10-1。
Bridge pattern
-
RefinedAbstraction
是methodB
的一个实际实现。有时当使用不同的实现者时,你需要稍微改变你的抽象。稍后您将在 MySQL 和 PostgreSQL 连接的例子中看到这一点。 -
Implementer
是真正的实现者 A 和 b 的接口。 -
ImplementerA
/B
满足实现者接口。他们都做methodA
,但方式略有不同。这个方法是在抽象层的保护下使用的。
例子
在这个例子中,您将看到 Laravel 的模式构建器的幕后,并演示它对桥模式的使用。Laravel 的 schema builder 允许您在数据库中创建新的表和列,并在以后回滚它们。我将在命令模式一章中详细介绍这一点,但现在让我们问这个问题:如何在 MySQL 中构建一个表?PostgreSQL 呢?微软 SQL 或者 SQLite 怎么样?这些都是您想要支持的数据库,但是如果您仔细想想,您会发现有以下几个部分:
-
抽象:构建一个表
-
实现:用于在 MySQL、SQLite、PostgreSQL 等中构建表的语法。
您可以为 MySQL、SQLite 和 PostgreSQL 创建一个构建器,但是您可能会以这种方式编写大量重复的代码。构建者可以关注如何创建表模式、删除表或检查表是否存在。从一个数据库到另一个数据库的解耦语法允许您获得细粒度的数据库 SQL 语句,并且还可以全面重用 builder 类。此外,您还将涵盖上述消息示例。
示例结构
图 10-2 为结构示意图。
图 10-2。
Concrete example
履行
这些都是 Laravel 中相当大的类,所以您将只关注每个类中的一小部分代码,这样您就可以看到运行中的构建器模式。我们来看一下hasTable
方法。这个方法告诉您$table
是否已经存在于您的数据库中。构建器抽象依赖于语法来编译 SQL,以确定表是否存在。
vendor/laravel/framework/src/Illuminate/Database/Schema/builder . PHP
49 public function hasTable($table)
50 {
51 $sql = $this->grammar->compileTableExists();
52
53 $table = $this->connection->getTablePrefix().$table;
54
55 return count($this->connection->select($sql, [$table])) > 0;
56 }
方法hasTable
首先编译特定于数据库的 SQL 文本,确定一个表是否存在,接下来它查找带有可选全局配置前缀的表名,最后它返回对 SQL 代码运行select
的结果。让我们来看看compileTableExists
的语法。您将使用 MySQL 版本,但是每个数据库都有自己的方法实现。
vendor/laravel/framework/src/Illuminate/Database/Schema/Grammars/MySQL grammar . PHP
33 public function compileTableExists()
34 {
35 return 'select * from information_schema.tables where table_schema \
36 = ? and table_name = ?';
37 }
这个简单的 SQL 字符串就是如何确定一个表在 MySQL 中是否存在。如果构建器是使用 MySQL 作为连接驱动程序配置的,那么在检查表是否存在时会使用这个语法。顺便说一下,如果你想知道 Laravel 是如何知道使用 MySQL 作为驱动程序的,这可以在config/database.php
中看到。我可以在构建器中检查其他方法,但它们或多或少是相同的。事实上,构建器还使用一个名为Blueprint
的类来帮助一些在表上创建列的常规方法和其他各种操作。如果你愿意的话,可以随意钻研那些代码,但是现在我将把注意力转移到另一个类上。
您可能没有注意到这一点,但是请注意在compileTableExists
方法中实际上有两个?
查询参数。然而,在hasTable
方法中,只有一个查询参数被传递给$this->connection->select($sql, [$table])
。这是如何工作的?不应该;它会出错。因此,MySQL 的Builder
抽象需要改进。输入MysqlBuilder
。
vendor/laravel/framework/src/Illuminate/Database/Schema/MySQL builder . PHP
13 public function hasTable($table)
14 {
15 $sql = $this->grammar->compileTableExists();
16
17 $database = $this->connection->getDatabaseName();
18
19 $table = $this->connection->getTablePrefix().$table;
20
21 return count($this->connection->select($sql, [$database, $table])) > 0;
22 }
显然,要检查 MySQL 中是否存在表,您需要数据库名和表名;因此需要对Builder
中的hasTable
方法进行一些改进,以处理这种微妙的差异。事实上,MysqlBuilder
只覆盖了 Builder 中的两个方法;它没有触及其他方法。
第二个示例(发送消息)
瓶子里的信息不仅仅是一首警察歌曲,它也是一种有效但低效的传递信息的方式。前面我谈到了将消息抽象从其实现中分离出来。所以让我们设计一个系统来发送消息。请注意下面的类名列表。看看本章开头的抽象 UML,看看你是否能把它们放在正确的地方。
-
Carrier
-
Email
-
OceanBottle
-
Messenger
-
PlainMessenger
现在看看下面的类。你会如何使用桥接模式?抽象是什么类,实现者是谁?桥在哪里?
-
Carrier (Implementer)
-
Email (ImplementerA)
-
OceanBottle (ImplementerB)
-
Messenger (Abstraction)
-
PlainMessenger (RefinedAbstraction)
你试图用各种载体抽象出信使。由于Carrier
是实现者,它将保存如何发送特定类型消息的逻辑。
app/Carriers/Carrier.php
namespace App\Carriers;
interface Carrier
{
public function sendMessage($message);
}
Email
和OceanBottle
都是Carrier
的具体实现。让我们来看看他们两个。请注意,他们只是吐出了消息。在现实世界的例子中,电子邮件运营商将连接到MailChimp
或一些服务来发送您的消息。OceanBottle
号航母会触发某种机器把你的信息打印在纸上,然后把折叠好的纸放进瓶子里,扔进太平洋。
app/Carriers/Email.php
namespace App\Carriers;
class Email implements Carrier
{
public function sendMessage($message)
{
echo 'EMAIL: '. $message . PHP_EOL;
}
}
app/Carriers/OceanBottle.php
namespace App\Carriers;
class OceanBottle implements Carrier
{
public function sendMessage($message)
{
echo 'OCEAN BOTTLE: ' . $message . PHP_EOL;
}
}
现在您已经知道了消息是如何发送的,让我们来看看Messenger
类。这种抽象是为了什么?记住,桥模式的目标是将抽象和实现分开。发送信息的想法与实际发送信息的细节是不同的。仔细想想,发送电子邮件的步骤与发送短信或邮寄信件的步骤非常相似。
-
把信息放到媒体上。
-
为媒介提供载体。
这并不是说媒介和载体不会改变,因为它们肯定会改变。电子邮件使用数字媒介和互联网作为载体。信件使用纸张作为媒介和邮件载体。当你说你的信息时,你的媒介是空气,你的载体是麦克风或者无线电广播。无论如何,这些步骤保持不变,因此当你抽象出Messenger
类时,它使用载体和媒介来处理实际细节的两步过程。在本例中,为简单起见,您将介质和载体结合在一起。
app/Messengers/Messenger.php
namespace App\Messengers;
class Messenger
{
protected $carrier;
public function __construct(use App\Carriers\Carrier; $carrier)
{
$this->carrier = $carrier;
}
public function send($message)
{
$message = $this->correctMisspellings($message);
$this->carrier->sendMessage($message);
}
// pretend like you are correcting mispellings
protected function correctMispellings($message)
{
return str_replace('Helo', 'Hello', $message);
}
}
请注意,messenger 会尝试纠正您的拼写错误。纠正拼写错误不是运营商的工作,但你可以看到,如果你没有将信使与运营商分开,这可能会被破坏在一起。但是,如果您不想担心拼写检查呢?很像前面的MysqlBuilder
是Builder
的精炼版本,您将精炼您的Messenger
抽象并创建一个PlainMessenger
。
{title="src/Messengers/PlainMessenger.php", lang=php}
类PlainMessenger
扩展了Messenger { public function send($message) { return $this->Carrier->sendMessage($message); } } ∼∼∼∼∼∼∼∼
当你处理文本消息类型时,你可能想使用PlainMessenger
。人们喜欢发表情符号和短词,所以拼写检查不太重要,实际上可能会让发短信的用户感到困惑。在模拟器中,让我们看看在处理短信载体时如何使用不同的信使。
app/Messengers/plain messenger . PHP
$message = "Helo world!";
$emailMessenger = new App\Messengers\Messenger(new App\Carriers\Email;
$snailMessenger = new Messengers\Messenger(new App\Carriers\SnailMail('PO Box 123, Somewhere, NY, 12345'));
$textMessenger = new App\Messengers\PlainMessenger(new App\Carriers\TextMessage ('123.456.7890'));
$emailMessenger->send($message);
$snailMessenger->send($message);
$textMessenger->send($message);
结论
您使用桥接模式将模式构建器与底层数据库分离。作为一个简单的例子,您还使用了桥模式来构建消息传递应用的主干。
那么什么时候应该使用这种模式呢?您可能已经注意到,将抽象和实现分成两个不同的类会有一些开销。在简单的情况下,使用桥模式可能会矫枉过正,并且可能会给本来简单的问题增加巨大的混乱。然而,如果你正在计划一个可扩展的和灵活的生态系统(例如框架),并且桥模式看起来很适合,它可能是。在上面的例子中,您可以轻松地添加新的信使和承运人。挺灵活的,每节课都很专注。另一方面,您可以构建一个大型的Emailer
类,它将发送消息和拼写检查合二为一。
因为您没有将抽象和实现分开,所以对于团队中的新成员来说,内聚且更大的Emailer
类可能更容易理解。假如你从来没有实现一个不同的消息载体,这种方法是没有错的。在某种程度上,避免抽象和实现之间的永久绑定可以给你带来很大的灵活性。
您的Messenger
抽象使用组合来调用来自Carrier
实现者的方法。虽然你还没有学会策略模式,但它有相似的结构,也使用了复合。事实上,您学习的许多模式都使用了组合(因为它很糟糕),表面上看,这些模式可能类似。请记住,这都是关于意图。桥的目的是保持抽象和实现非常松散的耦合。适配器模式的目的是在两个不兼容的类之间充当中间人。策略模式的目的是封装算法。因此,如果你发现自己在思考两种模式之间的差异,写下它们的意图,你可能会回答你自己的问题。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 171 页
2
3
www.youtube.com/watch?v=MbXWrmQW-OE
十一、组合
$> git checkout composite
目的
将对象组成树结构来表示部分-整体层次结构。Composite 允许客户端统一处理单个对象和对象的组合。 1
应用
生活中的很多东西都有等级结构:家谱、关系、组织、语言、自然、军事、政府、地址、工作场所、游戏、文件结构等等。有时候,事情在层级中更容易思考。想象一下,试着只用名字来解释你的家谱。等级制度允许我们把一个复杂的系统分解成相关的部分。然后,我们可以内省整棵树,或者只内省部分树,这对我们有利。
等级制度自然被认为是树。这是因为树木从底部开始,分支,并在某一特定的叶子处结束。叶子的父节点是分枝,它的父节点可能是另一个分枝。最后,事物在树的底部结束。但是,我不希望您认为,仅仅因为我们按照层次结构对事物进行排序,就意味着我们有一个组合模式。注意,在 intent 中还有第二句话:Composite 让客户机一致地对待单个对象和对象的组合。这意味着我们可以调用$cat->methodName()
和$cats->methodName()
,我们不必担心$cat
是一个单独的叶子对象,而$cats
是一个由许多对象的叶子组成的分支。
抽象结构
-
Component
:这可以是一个接口,也可以是一个抽象类,任何叶子或者复合都可以扩展(图 11-1 )。如果存在所有子类都能受益的方法,则使用抽象类;否则,简单的接口就足够了。图 11-1。
The composite pattern
-
Leaf
:叶子是原始的Component
对象。它没有孩子。 -
Composite
:复合是作为孩子的Components
的集合。在简单的情况下,你可以将孩子作为数组存储在Composite
类中。像Leaf
一样,它是一个Component
,因此必须实现method
。
例子
如果你已经建立了一些网站,那么你可能已经处理过下拉菜单。如果你运气不好,你有这样一个客户,他想要一个带有很多链接的层次菜单的大菜单。如果不管菜单有多少级,你都能一视同仁,那会怎么样?你只是把它们打印出来,对吗?所以如果你能说下面的话呢?
把菜单打印出来
$megaMenu->print();
// or
$simpleMenu->print();
// or
$someLink->print();
示例结构
图 11-2 为结构示意图。
图 11-2。
Composite pattern for menus
履行
您正在构建的是一种动态输出菜单的方式。不过,首先我要提醒您,将 HTML 放在 PHP 类中很难维护,有一种更好的方法:使用局部 Laravel 视图。然而,因为这个例子纯粹是为了说明组合模式,所以我在这些类中塞进了一些难看的 HTML。无论如何,这个示例的目标是构建一些菜单,以便您可以像这样轻松地输出它们:
app/simulator.php
$menulink1 = new App\MenuLink('google', 'http://google.com');
$menulink2 = new App\MenuLink('facebook', 'http://facebook.com');
$menulink3 = new App\MenuLink('kelt', 'http://keltdockins.com');
$menuitem1 = new App\MenuItem('some text');
$megaMenu = new App\MenuCollection;
$subMenu1 = new App\MenuCollection;
$subMenu2 = new App\MenuCollection;
$subMenu3 = new App\MenuCollection;
$megaMenu->add($subMenu1);
$megaMenu->add($subMenu2);
$subMenu1->add($menulink1);
$subMenu1->add($menulink2);
$subMenu2->add($menulink3);
$subMenu2->add($subMenu3);
$subMenu3->add($menuitem1);
print '<!-- printing entire mega menu -->' . PHP_EOL; $megaMenu->output();
print PHP_EOL . '<!-- printing submenu only -->' . PHP_EOL; $subMenu1->output();
print PHP_EOL . '<!-- printing menuitem1 only -->' . PHP_EOL; $menuitem1->output();
这个模拟的输出是什么?它输出 HTML,就像我之前说的。
模拟器输出
<!-- printing entire mega menu -->
<div class="sub-menu level0">
<div class="sub-menu level1">
<a title="google" href="http://google.com">google</a>
<a title="facebook" href="http://facebook.com">facebook</a>
</div>
<div class="sub-menu level1">
<a title="kelt" href="http://keltdockins.com">kelt</a>
<div class="sub-menu level2">
some text
</div>
</div>
</div>
<!-- printing submenu only -->
<div class="sub-menu level0">
<a title="google" href="http://google.com">google</a>
<a title="facebook" href="http://facebook.com">facebook</a>
</div>
<!-- printing menuitem1 only -->
some text
很漂亮,对吧?它让菜单的制作变得轻而易举。你也可以添加不同类型的菜单,比如一个MenuButton
或者MenuLinkWithImage
。不过,我有点言过其实了。你甚至还没有看到上面模拟中的类。它从Menu
开始。
app/Menu.php
namespace App;
interface Menu
{
public function output($level = 0);
}
其余的类扩展了Menu
,并且必须实现output
方法。接下来,让我们来考察一下叫做MenuLink
的叶子。
app/MenuLink.php
namespace App;
class MenuLink implements Menu
{
public function __construct($name, $url)
{
$this->name = $name;
$this->url = $url;
}
public function output($level = 0)
{
print str_repeat(' ', $level * 4);
print "<a title=\"{$this->name}\" href=\"{$this->url}\">{$this->name}</a>" . PHP_EOL;
}
}
正如您可能知道的,这个类只是处理打印带有 URL 和名称的锚 HTML 标签。很简单,对吧?但是,如果你没有这个MenuLink
的网址呢?您可以在这里放入一个if
语句,但这意味着您正在向一个方法添加逻辑。有没有另一种方法来处理这个问题,这样你就不必在你的output
方法中添加一个if
语句了?再来一片叶子怎么样?称它为MenuItem
。
app/MenuItem.php 类型
namespace App;
class MenuItem implements Menu
{
public function __construct($name)
{
$this->name = $name;
}
public function output($level = 0)
{
print str_repeat(' ', $level * 4);
print "{$this->name}" . PHP_EOL;
}
}
不要急于创建if
语句。记住:方法中的条件越多,方法的内部就越难理解。当然,在这里和那里添加一些if
/ else
不会让你感到困惑,但是我把条件句当成友敌。你必须利用条件,但他们一有机会就会在背后捅你一刀,所以你周围的条件越少越好。除非你喜欢被人在背后捅刀子,在这种情况下,if
该起来了!总之,您通过使用不同的叶类解决了上面的问题。现在让我们来看看你的组合,隐藏在阴影中,伪装成你的层次结构中的另一片叶子。
app/MenuCollection.php
namespace App;
class MenuCollection implements Menu
{
protected $children = [];
public function add(Menu $menu)
{
$this->children[] = $menu;
}
public function output($level = 0)
{
print str_repeat(' ', $level * 4);
print "<div class=\"sub-menu level{$level}\">". PHP_EOL;
foreach ($this->children as $child){
$child->output($level + 1);
}
print str_repeat(' ', $level * 4);
print "</div>" . PHP_EOL;
}
}
这个集合处理output
方法的方式略有不同。它调用它的直接子节点,当这些子节点也是MenuCollections
时,它们的子节点被调用。请注意,您可以向您的MenuCollection
添加实现Menu
的任何类型的类。最终,这意味着您可以在集合中嵌套链接、项目和集合。
在我结束这一章之前,考虑一下:如果你想向后(向上)遍历这棵树呢?所以如果你启动一个子菜单,你想知道它的父菜单,父菜单的父菜单,等等。,现在这是一条单行道,从顶级巨型菜单开始。您可以用跟踪子节点的类似方式来保存对每个子节点的父节点的引用。这完全取决于您的应用的业务需求。如果决定存储父引用,一定要决定是希望对象只有一个父引用,还是允许多个父引用。当你冒险进入这个领域时,你可能会遇到除了组合模式之外的其他模式,这完全没问题,勇敢的灵魂,继续冒险吧!
结论
在本章中,您使用了组合模式来输出菜单的层次结构。我想重申,简单地创建类的层次结构并不意味着它就是组合模式。当单个对象和对象集合以相同的方式处理时,可以看到组合模式。Composite 允许你命令一只猫去meow()
,同样也可以命令一群猫去meow()
。它让你output
一个菜单集合,就像一个菜单链接一样。老实说,如果你没有一个叶子,并且你正在使用一个单一的复合类,你也可以称它为组合模式。我不会向警方告发你的,我保证。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 183 页
十二、装饰器
$> git checkout decorator
目的
动态地将附加责任附加到对象上。Decorators 为扩展功能提供了子类化的灵活替代方案。 1
应用
装饰器扩展功能。装饰器和单纯的继承有什么不同?经典继承在运行时之前扩展了类的功能。装饰器在运行时扩展实时对象的功能。装饰器包装了一个真实的对象,让我们能够在运行时改变对象的行为。谈论运行时和 PHP 可能看起来很奇怪,因为 PHP 是一种中断式语言,而不是传统的编译语言(如 C++,Java ),但这个原则仍然适用。随着代码的执行,你在用 decorators 动态地改变行为。
作为改变行为的一个例子,假设你有一个Duck
类,它可以quack()
。Duck
在放射性池塘中游泳并获得超能力。你可以用一个SuperPower
装饰器把Duck
包起来,这将使它能够superQuack(),
击倒那些敢于挑战鸭子优势的坏蛋和火鸡。
抽象结构
-
Component
:作为ConcreteComponent
的父类的抽象类或接口。如果组件只有一种变体,它可能不需要抽象。见图 12-1 。图 12-1。
Who knew decorating was so much fun?
-
ConcreteComponent
:你可以有Component
的多种具体变化。它充当子类,并将成为被Decorator
包装的实际对象。 -
这个抽象类充当所有具体装饰器的基类。这个类很可能与
Component
共享同一个公共接口,并使用一个构造函数,该构造函数使用一个Component
来包装。被调用的Decorator
公共方法通常只是为Component
代理同一个公共方法。 -
ConcreteDecoratorA
/ConcreteDecoratorB
:这些具体的装饰器可以覆盖基类Decorator
中的方法,并利用通过构造函数注入的包装好的ConcreteComponent
对象。除了覆盖现有方法,还可以添加其他补充方法。
例子
你在构建一个有怪物的游戏。每个怪物都有固定的力量(STR),智力(INT),速度(SPE)。你将在怪物周围包裹装饰器,以在运行时修改它们的能力分数。虽然可能有更好的方法来处理大量的怪物(比如 flyweight 模式),但是您不必担心这里的内存使用;您希望严格关注在运行时添加职责。在战斗中,怪物可以变形并适应战斗,获得(和失去)能力。你不想为所有这些可能性构建不同的Monster
类;相反,您使用装饰器。
我将通过讨论演讲者来结束这一章,我称之为装饰器模式的一个微小变化。
示例结构
图 12-2 为结构示意图。
图 12-2。
Monster Mania@!#$#!
履行
怪物有智力,力量和速度分数。您构建了一个庞大的类来保存这些值。
app/Monster.php
namespace App;
abstract class Monster
{
public function intelligence() { return 1; }
public function strength() { return 1; }
public function speed() { return 1; }
}
你需要一个真正的怪物。我知道没有怪物、圣诞老人或者怪物圣诞老人 2 这样的东西,但怪物存在于电子游戏中。让我们选一个最笨的怪物:僵尸。经典僵尸没那么可怕。你可以从僵尸身边走开,它永远也抓不到你。流行文化电影和游戏,如《Z 世界大战》和《死路四条》,已经将这些原本可笑、缓慢、愚蠢的怪物变成了快速、强大、可怕的泰坦 3 。我看不出我们有什么理由不加入僵尸的行列!
app/Zombie.php
namesapce App;
class Zombie extends Monster
{
public function strength() { return 3; }
}
你所做的只是调整力量,让僵尸变得更强一点。接下来,您需要创建ModifiedMonster
,它是所有 decorators 的基类。为什么不简单地使用Monster
作为装饰器的基类呢?Monster
类不像装饰器那样包装另一个Monster
类。装饰器需要一个基类,将一个庞然大物传递给构造函数。你也可以在ModifiedMonster
抽象类中添加/删除额外的功能。
app/ModifiedMonster.php
namespace App;
abstract class ModifiedMonster
{
protected $monster;
public function __construct(Monster $monster)
{
$this->monster = $monster;
}
public function intelligence() { return $this->monster->intelligence(); }
public function strength() { return $this->monster->strength(); }
public function speed() { return $this->monster->speed(); }
}
现在你终于可以开始装修了!你想让你修改后的怪物更聪明,所以使用ExtremelySmart
装饰器来实现。你也可以用ExtremelyFast
让你的怪物跑得更快。我只展示了ExtremelySmart
类,因为它解释了装饰器的结构。请随意查看源代码中的其他装饰器。
app/ExtremelySmart.php
namespace App;
class ExtremelySmart extends ModifiedMonster
{
public function intelligence()
{
return parent::intelligence() * 2;
}
public function castSpell($spell)
{
return "casts the {$spell} spell";
}
}
最后,你需要一个客户来运行所有这些装饰。首先,你打印出僵尸的统计数据。
app/simulator.php
5 print 'Running Zombie Thing' . PHP_EOL;
6
7 $monster = new App\Zombie;
8
9 print 'This zombie stats are'
10 . ' STR ' . $monster->strength()
11 . ' INT ' . $monster->intelligence()
12 . ' SPE ' . $monster->speed() . PHP_EOL;
接下来,你想给僵尸增加一些速度。所以你在运行时修改他,用ExtremelyFast
来修饰。
app/simulator.php
18 $monster = new App\ExtremelyFast($monster);
19
20 print 'Decorated zombie stats are'
21 . ' STR ' . $monster->strength()
22 . ' INT ' . $monster->intelligence()
23 . ' SPE ' . $monster->speed()
24 . ' and it can now ' . $monster->jumpAttack() . PHP_EOL;
如果你愿意,现在你可以给僵尸增加更多的速度和智力。他现在是一个超级僵尸,我现在很害怕。我要去开灯了;这里太暗了。
app/simulator.php
30 $monster = new App\ExtremelyFast($monster);
31 $monster = new App\ExtremelyFast($monster);
32 $monster = new App\ExtremelySmart($monster);
33
34 print 'Decorated zombie stats are'
35 . ' STR ' . $monster->strength()
36 . ' INT ' . $monster->intelligence()
37 . ' SPE ' . $monster->speed()
38 . ' and ' . $monster->castSpell('fireball') . PHP_EOL;
现在你知道装饰模式是如何工作的了。它附加了额外的责任。但是,请注意,如果你要对你的怪物进行跳跃攻击,它会抛出一个错误。
app/simulator.php
41 $monster->jumpAttack(); // no such method - errors
这个错误是因为ExtremelyFast
装饰器提供了jumpAttack
,但是第 32 行的最后一个装饰是ExtremelySmart
,这意味着这个方法不再可用。这是装饰模式的一个缺点。如果一个修饰提供了一些基类ModifiedMonster
不知道的新职责,那么你只能得到最新的修饰的方法。令人高兴的是,一种叫做 presenters 的装饰模式可以解决这个问题。
提出者
主持人利用 PHP 中的魔法方法 4 ,比如 __call() 5 和 __get() 6 ,来绕过你在修改后的怪物装饰器中发现的消失责任问题。那么你什么时候使用演示者呢?如果满足以下任一条件,请使用它们:
-
您在模型中添加代码完全是为了视图逻辑:尽可能保持模型的整洁。模型在你的应用中随处可见。尽可能地保留逻辑,以避免到处制造恶魔。
-
您正在向自己的 Laravel 视图添加逻辑:如果您发现自己在做大量的条件语句,可以考虑将其抽象成一个 presenter 方法。
在本例中,您将使用一个名为robclanc y/presenter7的演示者。我已经冒昧地将它添加到了composer.json
中,所以请确保您运行了composer update
,然后您就可以使用php artisan serve
运行 web 服务器了。
一旦完成,看一看http://localhost:8000
8 和http://localhost:8000/presenter
9 。这是你的路线。
routes/web.php
Route::get('/', function () {
$user = new App\UserPresenter(new User);
$user->favoriteColor = rand(0, 1) ? 'blue' : null;
return view('hello', compact('user'));
});
注意,在这个例子中,我修改了 home 路由。您为该用户随机设置了一种喜爱的颜色。您还用一个UserPresenter
来“装饰”这个$user
对象。一会儿你会看到那堂课。首先,我们来看看 hello 视图。演示者的目标是将第一位代码转换成第二位代码。
以前
resources/views/hello . blade . PHP
23 @if ($user->favoriteColor)
24 <span style="background-color: {{ $user->favoriteColor }}">Hello
25 there!</span>
26 @else
27 <span>Hello there!</span>
28 @endif
在…之后
resources/views/hello . blade . PHP
17 <span {{ $user->favoriteColorStyle }}>Hello there!</span>
您可以看到第二段代码看起来更简单,并且不需要您思考太多。信不信由你,去除你的观点中的大部分逻辑实际上是可能的。无逻辑视图是你应该努力争取的,因为它们使你的代码工作更容易。阅读包含if/else/else
和if/foreach
语句的数千行 HTML 代码是一件非常麻烦的事情。如果您从不添加条件,那么您将永远不会发现自己在视图中调试逻辑,因为它将在您的 PHP presenter 类中。说到这里,我们来看看UserPresenter
级。
app/user reseller . PHP
namespace App;
class UserPresenter extends Presenter
{
public function presentFavoriteColorStyle()
{
return $this->favoriteColor
? "style=\"background-color: {$this->favoriteColor};\""
: '';
}
}
正如我在开始时所说的,演示者是神奇的,不同的包之间实现略有不同。还有其他一些软件包,但我随机选择了robclancy/presenter
。你可以自己写,但我不想为这一章写。如果你愿意,你可以和我分享。这种特殊风格的 presenter 希望您在 Camel 封装的函数名前面加上前缀present
,这将在您的类上创建一个动态属性。您也可以只创建一个函数,但是将favoriteColorStyle
作为另一个属性而不是某个函数似乎更好。也许我只是懒?无论如何,这涵盖了称为 presenter 的装饰器模式的变体。
结论
装饰模式提供了一种灵活的方式来为对象添加职责;职责可以在运行时附加和分离。这也意味着您不必编写完美的类。如果你第一次没有做对,没关系。当价格合适的时候,你可以在以后增加新的职责。
使用 decorators 的一个缺点是,如果你使用类型提示,那么你必须记住一个Decorator
不实现它的Component
。它把它包起来。因此,如果你使用类型提示,比如someMethod(Component $obj)
,这将抛出一个错误,因为从技术上讲Decorator
不是Component
的类型。装饰模式的另一个缺点是,如果有很多装饰,调试或故障排除会变得越来越困难。但是,不要让这阻止你使用表示器或装饰器,特别是如果你需要扩展类的职责而不需要子类化或修改基类的时候。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 196 页
2
http://en.wikipedia.org/wiki/Santa's_Slay
3
http://en.wikipedia.org/wiki/Attack_on_Titan
4
http://php.net/manual/en/language.oop5.magic.php
5
http://php.net/manual/en/language.oop5.overloading.php#object.call
6
http://php.net/manual/en/language.oop5.overloading.php#object.get
7
https://github.com/robclancy/presenter
8
http://localhost:8000
9
http://localhost:8000/presenter
十三、外观
$> git checkout facade
目的
为子系统中的一组接口提供统一的接口。Facade 定义了一个更高级的接口,使得子系统更容易使用。 1
应用
生活是艰难的。有时候事情就是很复杂。也许你写的代码是为了解决一个非常棘手的问题。当然,你通常试图把事情分解成最简单的形式(模型),但是你的建模并不总是正确的。有时候代码并不需要复杂。当你第一次写东西时,你并不总是第一次就写对。有多少次你看着你的旧代码想要重构它?你学习新的东西。你可以尝试重构;然而,在某些情况下,这可能是非常昂贵的(时间和金钱)。重构的另一种方法是在现有代码的基础上创建另一层,使界面更容易理解和使用。在某种程度上,这种模式可以被认为是第二次机会模式。
然而,facade 模式并不完全是为了掩盖糟糕的代码。它可以用来创建一个简单的公共接口,该接口包含了以某种方式协同工作的各种类。也许您正在处理一个拥有数百个公共方法的大型子系统,但是您只需要其中的十个。您希望 facade 正确地构造子系统中的各种类,并向您提供这十个接口,以便您可以轻松地使用它们。使用这种模式可以让您分层考虑代码。facade 将包装一层较低级别的代码,这样当您想要使用该系统时,就不必处理子系统的复杂细节。
Laravel 的脸
fa-cade (fah-saud)
建筑物的正面。一种行为或表现方式,让别人对你的真实感受或处境产生错误的想法。
Laravel 有个东西叫外观 2 ,不要和外观图案混淆。Laravel 使用 facades 作为在服务容器中包装类或绑定的方式,然后静态地调用它。因此,如果你有一个名为AwesomeImpl
的类,它有一个名为someMethod
的非静态方法,你可以为AwesomeImpl
创建一个名为Awesome
的门面,然后静态地调用Awesome::someMethod()
。不过,真的是这样。它只是为类提供一个静态接口。您不希望在AwesomeImpl
中使用静态方法,因为静态方法很难在 PHPUnit 中测试。
如果你关注拉勒韦尔的戏剧,那么你可能知道每隔几个月就会有人在 reddit 3 上抱怨拉勒韦尔的门面,声称它们不是真正的门面。每个人都喜欢好的戏剧。
- 谁在乎他们是否不同?门面的定义是一种行为方式,给人一种错误的情况。Laravel facades 确实给人一种拥有静态方法的类的感觉。-引人注目的猫科设计
当我们谈到 Laravel facade 辩论的主题时,有人提到 Laravel 也许应该将 facade 的名称改为 proxies。您将很快了解代理模式。“四人帮”最初打算用代理来解决性能、保护和并发性问题。拉勒维尔外观的意图是什么?
早些时候,当我说 Laravel facades 只是用来将方法转化为静态方法时,我有点撒谎了。不好意思。实际上我忽略了一个重要的细节。Laravel facade 的真正意图是让客户不必知道如何构造一个类。静态调用方法只是一个副产品。您静态调用这些方法的真正原因是因为这样做更容易。所以在上面的例子中,客户端从不构造一个new AwesomeImpl
;客户使用 Laravel 外观,为他们处理施工。对某些人来说,静态调用方法比非静态调用方法更性感。使用 Laravel facade 允许您绕过构造类的细节。说出做到这一点的模式!(我会说简单工厂模式。)
在这本书的开始,我承诺了一些戏剧,所以你有它!除了戏剧性之外,这里的主要收获是您应该意识到 Laravel 中的术语 facade 与 facade 模式有着不同的含义。
抽象结构
图 13-1 为结构示意图。
-
Facade
通常是一个单独的类。它深入到一个复杂的子系统中,构造不同的类并调用方法。在这个抽象的例子中,method()
正在创建ClassA1, ClassB1
和ClassC1
,并从每个调用方法。这些类在同一个子系统中,所以它们可能以某种方式一起工作。facade 的创建者知道它们是如何协同工作的,并为您创建了一个名为method()
的调用方法。谢谢,代笔! 4 -
包含许多逻辑上属于一起的类。这些类可能在同一个域中。这些类可能有些耦合。它们可能彼此没有任何关系,但属于同一个名称空间,因为开发人员喜欢这样做。理解子系统对于创建外观抽象层以简化其他人的工作至关重要。
图 13-1。
The facade pattern
例子
我没有一个类的子系统供你使用。我们需要一些现有的代码,我们不太喜欢的界面。为了发明一个例子,我可以从 WordPress 或者其他一些头脑装瓶商那里获取一些代码。唉,不过!我们在这个蓝色的星球上只有一百多岁。如果我跳过 WordPress 的想法,我真的希望你不会太失望。我们有更重要的事情要做。
因此,在本例中,您将采用所有现有的模式模拟,并将它们合并到一个单独的类中。这样做是为了有一个地方可以运行所有的模式。你将称这个地方为PatternExecutor
。您正在创建一个新层,允许您触发到目前为止您已经覆盖的任何模式的模拟。作为额外的奖励,有一个random()
方法,它的职责是随机执行一个模式模拟。结果应该类似于下面的代码:
app/simulator.php
$patternFacade = new App\PatternExectutor;
$patternFacade->random();
每次运行使用模式外观的模拟器时,您都会得到一个随机输出,因为一个模式是随机选择运行的。假设您运行它两次,那么您看到的输出可能(随机地)如下所示:
php 应用/模拟器
ABSTRACTFACTORY PATTERN
======================================
Your merchant made $20
======================================
php 应用/模拟器
BRIDGE PATTERN
=====================================================================
EMAIL: Hello world!
SNAIL MAIL: Hello world! sending to: PO Box 123, Somewhere, NY, 12345
TEXT: Helo world!
=====================================================================
示例结构
图 13-2 为结构示意图。
图 13-2。
The concrete example
履行
目前,每个模式模拟都在它自己的分支中。这意味着您需要检查每个 git 分支,并将代码复制到/app
目录中。我指出这一点只是为了清楚起见,在现实生活中你不会用你的 facade 模式这样做,因为你的代码不会分散在你的存储库中不同的 git 分支。现在,来自各个分支的所有代码都已经被复制了,假设这些代码是您的子系统。图 13-3 是目录截图。正如您所看到的,对于不知道这段代码背后的原因的人来说,文件名并不能提供太多的信息。
图 13-3。
What is the purpose of the code in this directory?
你能通过查看目录中的文件名告诉我这个子系统的用途吗?结构和文件名可能会令人混淆。这里发生了很多事情,仅仅通过查看这些文件名并不清楚所有这些代码应该如何一起工作。因此,您将为新手开发人员创建一个名为PickerExecutor
的门面,统一代码子系统。
创建代码的人通常知道代码背后的原因。他们知道为什么一个叫做doSomethin
g
的方法存在。他们创造了它。当然,总有时间和记忆丧失的因素,所以这些原因变得不那么清楚了。这就是为什么创建一个 facade 可能有助于减轻对整个子系统存在原因的任何混淆。
facade 模式用一个单一的目的统一了所有这些代码。那目的是什么?我碰巧知道所有这些类都是为了说明 GoF 模式,但那是因为我是整本书中创建这些类的工程师。外观是复杂子系统的简化,这里的复杂子系统有传播不同 GoF 模式的目的。因此,您的外观将为新用户提供一个有希望有明确目的和/或更易于使用的界面。你不希望人们仅仅为了利用子系统的目的而必须挖掘子系统的所有类。
app/PatternExecutor.php
5 public function random($params = [])
6 {
7 $methods = ['abstractFactory', 'adapter', 'bridge', 'builder'];
8 $method = $methods[array_rand($methods)];
9
10 print PHP_EOL . strtoupper("$method pattern") . PHP_EOL;
11 print "======================================" . PHP_EOL;
12 $this->$method($params);
13 print "======================================" . PHP_EOL;
14 }
这个random
方法选择一个随机的方法名并调用它。因此,选择一些方法来看看,其中任何一个都可以随机选择。
app/PatternExecutor.php
16 public function abstractFactory()
17 {
18 $ratings = array(
19 'PG-13' => new App\RatedPG13\RiceFarmer,
20 'R' => new App\RatedR\DrugDealer
21 );
22
23 $merchant = $ratings[array_rand($ratings)];
24 $client = new App\Client($merchant);
25
26 $client->run();
27 }
对于abstractFactory
方法,我从抽象工厂模式分支复制了simulator.php
代码。我没有将模拟器代码组织成类,所以我只是将所有代码复制到方法中。不过,这提出了一个很好的观点。为什么我复制了所有的代码?如果代码已经用simulator.php
写好了,为什么我不需要那个文件?我可以将每个分支的simulator.php
重命名为模式名,然后需要 PHP 脚本。我没有这样做,因为我认为在这种情况下,在 facade 的每个方法中复制代码会更干净。我说这些是为了把我们带到下一点。为了利用子系统,外观有时需要实现自己的代码。其他时候,它可能像调用现有方法的序列一样简单。在这种情况下,您必须在最终调用PatternExecutor
之前设置一些东西。
app/PatternExecutor.php
30 public function adapter()
31 {
32 $crmAddress = with(new App\CRM\AddressLookup)->findByTelephone('555 867-5309');
33 $address = new App\CRM\AddressAdapter('Jenny Call', $crmAddress);
34 $mailClient = new App\MailClient;
35 $mailClient->sendLetter($address, 'Hello there, this is the body of
36 the letter');
37 }
这里没有列出其余的方法。我想你现在大概明白这个门面是怎么实现的了。请随意查看 git 存储库,了解 facade 中的其他方法。如果你真的感到勇敢,试着自己实现一些吧!
外观和适配器一样吗?
那么,外观和适配器有什么不同呢?它们的结构相似。根本区别在于意图。facade 接口不像适配器接口那样是预先确定的。在适配器中,您不能随意命名您的方法,因为您有一个需要特定接口的客户端。facade 可以命名最有意义的方法,也可以命名对 facade 的客户来说最简单的方法。
此外,适配器并不意味着采用复杂的层和简化。它们是关于把一层转化成客户已经期望的另一层。适配器模式类似于 HDMI 转 DVI 适配器,外观模式更像是将 HDMI 输出连接到主板及其子电路。你不需要知道电路是如何一起工作的;您只需将电缆插入 HDMI 接口,让工程师们去担心底层部件。简而言之,适配器转换层,外观隐藏层。
结论
在本章中,你看到了当你想简化某层代码时,你可以使用 facades。这种模式的缺点是 facade 依赖于各种各样的类。如果底层系统发生变化,可能会给你的门面带来麻烦。每当对子系统进行底层更改时,您可能都必须重构外观。这也意味着你需要理解底层的类是如何一起工作的。必须有人来创建和管理门面。拥有 facade 的好处在于,团队中的所有开发人员都可以利用更简单、更易于使用的 API,而不必处理底层子系统的复杂性。
一个 facade 必须是一个单独的类吗?一般来说是的,一个 facade 通常是一个单独的类。你希望你的外观尽可能简单易用。如果你创建了多个类,你需要知道这些类是如何一起工作的。一个 facade 已经与子系统中的许多模块耦合在一起,但是您对使用 facade 的客户端开发人员隐藏了所有这些。这并不意味着你必须创建一个巨大的超级门面类。如果有必要的话,你可以创建多个 facade 类,然后在这些 facade 之上创建另一个层。建在其他外观上的外观。请记住,您希望尽可能保持简单,外观是您必须管理的更多代码。因此,当建立一个门面时,要确保收益大于建立和管理门面所花费的时间。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 208 页
2
https://laravel.com/docs/5.3/facades
3
www.reddit.com/r/PHP/comments/1zpm0y/laravel_lets_talk_about_facades
4
www.youtube.com/watch?v=HZJSccRDKZc
十四、享元
$> git checkout flyweight
目的
使用共享来有效地支持大量细粒度的对象。 1
应用
flyweight 模式用于减少内存占用。当您处理大量共享相同属性的对象或共享相同大数据(例如图像)的少数对象时,您会发现这种模式非常有用。举例来说,假设您有一个具有类似于name, gender,
和phone number
属性的Person
对象。假设每个 Person 对象的平均内存开销是 1000 字节。当您分配 100,000 个Person
对象时,您最终会使用 1 亿字节(95MB)的内存。现在让我们也假设所有的Person
对象都有设置为"Female"
的gender
属性,并假设字符串"Female"
占用大约 300 字节的内存。这意味着大约有 300 x 100,000 字节(28MB)的内存被复制。(OMG,让它停下来!不再有数学 PLZ!)
一句话:利用共享数据可以减少总的内存消耗。更少的内存可能意味着更好的性能。
四人组把可以在对象之间共享的数据称为内在数据,把不能共享的数据称为外在数据。我发现自己不得不回去回忆哪个是哪个。我将省去您的麻烦,而是说数据要么是可共享的,要么是不可共享的。
你如何知道哪些数据是可共享的?对于Person
类,你有三个属性:name, gender
和phone number
。你可能会说gender
是可共享的,而name
和phone number
不是。电话号码和姓名通常有些独特;但是,如果在这个应用中,您处理的数据集有很多重复的名称,该怎么办呢?在这种情况下,您可以通过共享name
和gender
来节省大量内存。由您决定在 flyweight 中共享哪些数据。
然而,如果你停下来想一想,在 PHP 的传统工作流程中,你可能不需要 flyweights。我说的 PHP 传统工作流程到底是什么意思?
-
某个最终用户在浏览器中输入一个 URL 来导航到某个路由。
-
该路由调用 web 服务器(Apache/Nginx ),而 web 服务器又调用 PHP。
-
PHP 调用数据库调用/业务逻辑等操作。
-
最终,一些 HTML 会通过 HTTP 协议返回给最终用户。
所有这些都是在说:PHP 脚本的生命可能非常短暂。构建 100,000 个对象需要时间(在我的机器上大约 15 秒)。在这段时间里,你的终端用户不耐烦地敲着他的脚,等待网页的回应。更不用说其他用户也试图请求相同的页面。因此,flyweight 模式在无状态请求-响应情况下没有意义,因为每次请求路由时构造大量对象会花费很长时间。
在一个小的对象集合上使用 flyweight 可能有些过头了。那么你什么时候使用 flyweight 呢?并非所有 PHP 都属于请求-响应无状态 HTTP 范式。您可以在命令行上运行 PHP 脚本。您可以将 PHP 作为 web 套接字服务运行。你甚至可以利用 Laravel 的队列 2 与 Beanstalk 和亚马逊 SQS 的集成来运行 PHP 作为后台进程。不是每一个 PHP 脚本都必须在几毫秒内运行,因此有时您可能希望使用 flyweight 模式来节省内存。既然我已经展示了为什么你可能使用 flyweight 模式,让我们学习这个模式。
抽象结构
图 14-1 为结构示意图。
-
是所有 flyweight 对象继承的基础抽象类。如果不需要基础方法,那么这可能只是具体 flyweights 实现的一个接口。
-
减轻客户创建新 flyweight 对象的负担。它处理决定何时创建一个新的具体 flyweight 对象或指向一个已经创建的对象的逻辑。这个工厂包含一个关联数组,跟踪所有创建的 flyweights。
-
ConcreteFlyweight
是一个Flyweight
的实例。它的属性是共享的。重要的是要记住,如果这个 flyweight 的一个属性被改变,那么指向这个 flyweight 的每个人的属性都会被改变。共享的 flyweights 应该被视为不可变的,否则你会危及你的理智。 -
UnsharedConcreteFlyweight
是非共享 flyweight 的实例。有时,您需要创建一个 flyweight 的唯一的、非共享的实例。使用 flyweight 工厂,您可以创建一个特殊的 flyweight 实例。这个对象可以随意更改。一种方法是在你的基类Flyweight
中创建一个clone
方法。这样,flyweight 工厂可以从现有对象中克隆对象,然后将它们转换成非共享的 flyweight。此时,当您更改 flyweight 的任何属性时,您并没有破坏共享数据。有时你可能不需要这种类型的轻量级。这里提到它只是为了让您意识到数据共享问题。我将在例子中说明这个问题。
图 14-1。
The flyweight pattern
例子
经过几天对其他开发人员使用 flyweight 模式的不同例子的研究,我感到沮丧。我看到的很多例子都是针对游戏引擎的。在游戏中,你经常会处理共享数据的庞大对象。充满坦克和僵尸的世界。一片长满树木的森林。激光枪的粒子爆炸。在拉勒维尔你不会真的做那种事。
“四人帮”在文本编辑器中使用了 flyweight 模式。在文档中重复出现的图像节点共享相同的图像内存,但在页面中包含非共享的图像坐标。当只有位置不同时,不需要复制图像数据。网络浏览器也做同样的事情。再说一次,在拉勒维尔你也不会真的做这种事情。当然,你确实使用了 WYSIWYG 编辑器,但这更多的是与 JavaScript 有关,而不是 Laravel。
有什么好的例子来说明 flyweight 模式?这对我来说很难。想了一会这个模式还是被难住了,决定看一些动漫。我从风之谷的娜乌西卡 3 开始。还是什么都没有;接下来是4。做完那个,我又看了英姿飒爽 远去 5 。在这一点上我绝望了。爆发大炮的时间:Ponyo6。这些事情需要时间。我想,时间就是一个很好的例子。例如,在组织约会时,可以使用日历上的日期。你写下同一天的多个约会。你买五本日历并不是为了在同一天写下五个不同的约会。你只要在盒子里写小就行了。让我们进一步探讨这个概念。
在这个例子中,您将创建一个计时器 flyweight,它为给定的一天记录一个日期。您将在脚本中加载 100,000 个用户,每个用户都有一个最后登录的日期。更有趣的是,您的大多数用户都是在过去几个月内登录的。您可以创建 100,000 个日期时间对象,每个用户一个。然而,您实际上只需要大约 60 个用户共享的日期时间对象。为了保持这个例子的简洁,您实际上不会对大量的用户对象做任何事情。实际上,您实际上会对这 100,000 个用户对象做一些事情。您还应该假设这个脚本作为一些 cron 任务、队列作业或一次性命令行 PHP 脚本运行。如前所述,您不可能在一个网络请求中加载这么多对象。
示例结构
图 14-2 为结构示意图。
图 14-2。
The concrete example
履行
咳咳…首先是关于 PHP 和内存的一课。
当我谈到原型模式(和克隆)时,我简要地谈到了内存和指针,但是让我们更详细地再讨论一下。你问为什么?如果您理解 PHP 如何为变量分配内存,您应该更好地理解如何实现 flyweight 模式。几个实验将更好地解释 PHP 如何为变量分配内存。首先,我想定义一些东西。指针是内存中的一个小空间,它只是指向另一个内存地址的开始。这类似于将一个电子邮件地址(pointer@stuff.com
)转发到另一个电子邮件地址(real@stuff.com
)。数据都保存在real@stuff.com
的收件箱里,但你仍然可以使用两个电子邮件地址。比我聪明的人说过以下的话:
- PHP 是一种动态的、松散类型的语言,使用写时复制和引用计数。 7
什么是参考计数 8 ?它是引用特定内存空间的变量的数量。当引用计数达到零时,PHP 内部知道他们可以丢弃这个内存空间中的数据,并将其重新用于其他用途。没有引用计数,你就不知道什么时候释放内存。最终你会耗尽内存。使用 Xdebug,您可以查看变量的引用计数。如果没有 Xdebug,可以使用 Ubuntu 的 apt-get,Mac 的 brew,或者在 Windows 上下载安装程序。
app/memory/experience 1 . PHP
$a = "hello there";
xdebug_debug_zval('a');
// a: (refcount=1, is_ref=0)='hello there'
$b = $a;
xdebug_debug_zval('a');
// a: (refcount=2, is_ref=0)='hello there'
$b = 'something else';
xdebug_debug_zval('a');
// a: (refcount=1, is_ref=0)='hello there'
注意当你把$b
赋给变量$a
时refcount
是如何增加的?在内部,PHP 并没有为$b
创建新的内存空间。这样更快(比我聪明的人说)。稍后,当$b
变为something else,
时,你必须分配新的内存,在此期间$a
的 refcount 返回到 1。
接下来要解决的是写时复制的确切含义。当两个或多个变量相互赋值时,它们共享同一个内存地址。只有当其中一个变量改变时,PHP 才会复制内存中的实际值。您可以在下一个实验中使用memory _ get _ usage9函数来查看这一点。
app/memory/experience 2 . PHP
function print_member($step) {
print "Step #{$step} - " . memory_get_usage() . PHP_EOL;
}
print_memory(1); // Step #1 - 226536 bytes
$a = array_fill(0, 200000, 0); // new memory allocated in Address#1
print_memory(2); // Step #2 - 19924088 bytes
$b = $a; // address of $b equ\
als address $a
print_memory(3); // Step #3 - 19924176 bytes
$b[4] = 4; // new memory allocat\
ed Address#2
print_memory(4); // Step #4 - 39621528 bytes
当你第一次启动你的程序时,你使用了 226,536 字节。好的,很好。谁在乎…请下一步!在第 2 步中,分配一个巨大的数组,其中填充了 200,000 个零值元素。在这一步中,您现在使用了 19,924,088 字节的内存。涨幅很大啊!在步骤 3 中,当$b
被分配给$a
时,你可能期望看到内存使用翻倍,但这并没有发生。实际上只增加了 88 个字节。这是为$b
创建一个新指针并更新地址#1 中的引用计数所需的空间量。最后,当您在步骤 4 中更改$b
时,您会看到内存使用的巨大差异,因为此时当$b
被更改时,您将内存从地址#1 复制到一个新的位置,我们称之为地址#2,然后您更改了$b
的第四个元素。这就是写入时复制的工作原理。直到最后需要的时候,你才复制内存值。这就是为什么您可以在不耗尽内存的情况下执行类似以下代码的操作。
app/memory/experience 3 . PHP
$storage = [];
$a = array_fill(0, 200000, 'abc');
for ($i = 0; $i < 1000; $i++) {
$storage[$i] = $a;
}
print $storage[999][3] . PHP_EOL; // abc
请注意,您在数组$a
中分配了 200,000 个块。根据您上一次的实验,100,000 个数据块大约是 19,924,176 个字节,因此如果您将它翻倍,它将占用将近 38MB 的内存。如果你这样循环 1000 次,那将会有将近 4GB 的内存,这将会使大多数系统陷入瘫痪。这里不会崩溃和烧毁的原因是 PHP 的写时复制方法。
到目前为止,您只处理了字符串、数字和数组。对象是如何表现的?对象仍然有引用计数和写时复制,所以它们的行为方式基本相同。下面的代码说明了对象和基元之间的区别。
app/memory/experience 4 . PHP
class SomeObject { public $answer; }
function change1($obj) { $obj->answer = 42; }
function change2($obj) { $obj = 'Douglas'; }
$x = new SomeObject();
$x->answer = 0;
change1($x);
change2($x);
var_dump($x); // what is the output?
你在这里的产量是多少?
-
你应该看到你的对象的回答是 0 吗?
-
你应该去看看道格拉斯吗?
-
你应该看到你的答案已被改为 42?
事实证明第三种选择是正确的,但是为什么呢?为什么在change1
方法中answer
属性变成了 42,而change2
方法却没有任何效果?答案在于你的对象的属性是引用相同内存地址的指针。当你将$x
传递给change1
和change2
函数时,你为$x
本身创建了一个新的内存指针,但是$x
的所有属性仍然指向同一个内存位置。因此,在change1
中更新$obj->answer = 42
,也就是改变$x->answer
的相同内存地址。在change2
中,当您更新$obj = 'Douglas'
时,您正在更改与$x
不同的内存地址。你将在下一个实验中利用这一点。
app/memory/experience 5 . PHP
$faker = Faker\Factory::create();
$faker->seed(42);
$storage = [];
$checkOne = memory_get_usage();
for ($i = 0; $i < 100000; $i++) {
$storage[] = new Person($faker->firstName, $faker->boolean() ? 'Male' : 'Female');
}
$checkTwo = memory_get_usage();
print round(abs($checkTwo - $checkOne) / (1024*1024)) . 'MB memory'
. PHP_EOL;
// 44MB memory
Faker 10 库为您提供了伪随机的名字以及随机的是或否布尔值来确定性别。您可能需要运行composer update
来获取 Faker。在这个实验中,您将创建一组随机的Person
对象,存储所有 100,000 个实例的总内存消耗是 44MB。接下来,你要利用你在实验 4 中的发现。通过在对象间共享gender
属性,可以减少使用的内存量。
app/memory/experience 6 . PHP
$checkOne = memory_get_usage();
$male = new Gender('Male');
$female = new Gender('Female');
for ($i = 0; $i < 100000; $i++) {
$storage[] = new Person($faker->firstName, $faker->boolean() ? $male : $female);
}
$checkTwo = memory_get_usage();
print round(abs($checkTwo - $checkOne) / (1024*1024)) . 'MB memory'
. PHP_EOL; // 39MB
memory
所以你节省了 5MB 的内存。这看起来并不多。在这种情况下,您没有保存那么多,因为大部分内存使用的是这个人的名字。共享name
属性可能会为您节省更多空间。我把它作为一个练习留给你去尝试。希望我已经用一种可以理解的方式解释了 PHP 中跨内存共享数据的概念。但是您还没有实现 flyweight 模式。到目前为止你所做的叫做串实习 11 。让我们继续前进,实现你的轻量级!
Flyweight 实现
当您使用 flyweight 模式时,您需要一种基准测试方法。这种模式的要点是节省内存和提高性能。如果你不知道你的程序使用了多少内存,那么你怎么知道你为做一个轻量级应用所做的工作是否有回报呢?你已经在这一章的内存基准上花了很多时间,所以你应该很熟悉memory_get_usage
,这是你将用来确定内存使用的。这里是你将要运行的模拟器来演示轻量级。您将首先看到在没有 flyweight 的情况下使用了多少内存。接下来,您将看到在保持 flyweight 不变的情况下节省了多少内存。
app/simulator.php
$memory1 = memory_get_usage();
$time1 = microtime(true);
$people1 = [];
for ($i = 0; $i < 100000; $i++) {
$person = new App\Person;
$person->last_login = App\RandomDate::between('2014-11-01', '2014-12-01');
$people1[] = $person;
}
$memory = round((memory_get_usage() - $memory1) / (1024 * 1024), 2);
$time = round(microtime(true) - $time1, 2);
print "Without flyweight, {$memory}MB of memory and {$time}s" . PHP_EOL;
下一步是使用一个名为DateKeeper
的 flyweight 来存储日期。与前面的代码不同,来自DateKeeper
的日期由创建的 100,000 个Person
对象的last_login
属性共享。本质上,你是在重复代码;唯一的主要区别是在给Person
对象分配last_login
日期时使用了 Flyweight 工厂。
src/simulator.php
$person->last_login = $dateKeeper->fetch(RandomDate::between('2014-11-01',
'2014-12-01'));
那么这个例子中的DateKeeper
flyweight 工厂是什么样子的呢?让我们来看看。
app/datekeeper.php
class DateKeeper
{
static private $dates = [];
static public function fetch($dateAsString)
{
$datetime = is_a($dateAsString, 'DateTime') ? $dateAsString : new \DateTime($dateAsString);
$index = $datetime->format('Y-m-d');
if (! array_key_exists($index, static::$dates)) {
static::$dates[$index] = $datetime;
}
return static::$dates[$index];
}
}
你可能会说,“哦,你在用静力学;太恶心了!Kelt!”是啊,你可能是对的。不过,在这种情况下,这给了您不必初始化新的DateKeeper
的优势。你可以在任何地方使用这个DateKeeper
作为简单的单例全局变量!如果您想在整个应用的不同地方使用DateKeeper
,而不必依赖服务容器来创建单例,这可能会非常好。真的是这样。我想指出这一点,因为你也可以创建无静电的轻量级。当不使用静态时,只需要确保根据需要在应用中传递 flyweight 对象,否则就会失去所有缓存的对象和 flyweight 模式的好处。
正如你所看到的,这并不是让你的轻量级跑起来的全部工作。轻量级不应该有很多工作。对待它的本质:内存共享。
结论
总的来说,flyweight 模式不太可能在 Laravel 中使用。不过,不用担心;这一章并不完全是浪费时间。您学到了很多关于 PHP 内存管理的知识。了解内存在 PHP 中是如何分配的似乎太低级了,但是考虑一下这个。假设您使用 Laravel 的雄辩 ORM 从数据库中提取了数千条记录。您可以花一点时间来检查这个操作的内存使用和性能。接下来,您可以跳过雄辩的 ORM,直接使用DB::table('table_name')
。这种方法将返回stdClass
对象,而不是水合雄辩的模型。它可能会更快,使用更少的内存。我把这作为一个练习留给你去尝试。
flyweight 模式的一个缺点是它会导致更复杂的代码。这可能不值得增加额外的复杂性。你应该经常检查使用 flyweight 节省了多少内存。有时获得的内存自由根本不值得使用 flyweight 的额外复杂性。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 218 页
2
https://laravel.com/docs/master/queues
3
http://en.wikipedia.org/wiki/Nausica%C3%A4_of_the_Valley_of_the_Wind_%28film%29
4
http://en.wikipedia.org/wiki/My_Neighbor_Totoro
5
http://en.wikipedia.org/wiki/Spirited_Away
6
http://en.wikipedia.org/wiki/Ponyo
7
http://PHP.net/manual/en/internals2.variables.intro.PHP
8
http://PHP.net/manual/en/features.gc.refcounting-basics.PHP
9
http://PHP.net/manual/en/function.memory-get-usage.PHP
10
https://github.com/fzaninotto/Faker
11
http://en.wikipedia.org/wiki/String_interning
十五、代理
$> git checkout proxy
目的
为另一个对象提供代理或占位符,以控制对它的访问。 1
应用
代理是中间人模式。传入调用首先与代理通信,而不是客户端直接调用某个对象方法。类似于装饰器,代理将自己包装在对象周围。与装饰器不同,代理不会为包装的对象添加新功能。那么你为什么使用代理呢?为什么中间有个男人?有几个很好的理由可以解释为什么你会这样做,并且从这些理由中产生了不同类型的代理。我将在下面列出这些不同的类型,以及你为什么想要使用那种类型的代理的原因。
-
虚拟代理用于延迟底层对象的构造或简化对象。当一个对象需要一段时间来加载时,您可以仅在绝对需要时使用这个代理来构造对象。它还可以用来降低复杂性。例如,通过使用虚拟代理,名为
doStuff
的方法可以更恰当地重命名为maximizeProfits
。 -
当您希望将远程资源视为本地资源时,远程代理非常有用。使用肥皂 WSDL 就是一个例子,但是我已经很久没有使用了。许多 restful APIs,如 Stripe,都附带了一个库,它充当 web 服务的代理。
-
保护代理阻止对方法访问。一个
UnauthenticatedProxy
可能不允许某些方法给它的底层委托。 -
智能代理为包装的对象添加了额外的功能。调用对象方法时触发事件或写入日志文件不会干扰原始对象源代码。这类似于装饰器,但是您没有向对象添加可见的功能。添加的功能对客户端是透明的。
还有其他代理,但你必须自己去发现它们。现在你将把注意力集中在这些主要的问题上。
抽象结构
-
Subject
是Proxy
和RealSubject
扩展而来的抽象类或接口。它定义了使用的基本共享方法。这是您在键入提示时使用的。见图 15-1 。图 15-1。
The proxy pattern
-
Real Subject
是Subject
的具体实现。它将被Proxy
使用。Proxy
一般会通过构图来包裹Real Subject
。 -
Proxy
用于代替Real Subject
。它是一个Real Subject
的替代品。如果你的词汇像我一样糟糕,那么你可能会听到“代理人”这个词,并想起布鲁斯·威利斯的那部电影。因此,请允许我定义代理人。代孕妈妈是替代品。类似于代课教师如何代替另一名教师,proxy
将表现得更像一个真实的主题,只在保护、性能或简化Real Subject
的功能方面进行替代。
例子
在本例中,您将创建四种不同类型的代理。
-
智能代理:大多数矿工都不与人交往。他们不想分享任何关于他们珍贵战利品的信息。其他矿工都管不住自己的脏嘴。这些矿工不得不吹嘘和广播他们开采的黄金数量。您将使用智能代理来模拟这个场景。您将使用内置的 Laravel 事件在挖掘发生时进行广播。见图 15-5 。
图 15-5。
Smart proxy - loudmouth miners
-
保护代理:你有一座你想要开采的金矿。有些矿你想开采多少就开采多少。你想要限制在任何给定时间可以开采多少的其他矿。您将为此使用保护代理。见图 15-4 。
图 15-4。
Protection proxy - lawful mining
-
远程代理:您有一个想要使用的远程 restful API。您将使用 Guzzle 客户端来处理这个问题。您的代理会让它看起来好像根本没有 restful API。这个代理对您的客户机隐藏了使用 JSON 和 HTTP 的细节。见图 15-3 。
图 15-3。
Remote proxy - fetching people
-
虚拟代理:您将为文件阅读器创建一个虚拟代理。文件读取器被设计成读取文件并从文件中提取信息。当它构造时,它将文件加载到内存中。这里的问题是,您可能同时使用许多这样的文件读取器,并且除非绝对需要,否则您不想将文件加载到内存中。见图 15-2 。
图 15-2。
Virtual proxy - file reader
履行
虚拟代理(实验 1 和 2)
虚拟代理仅在需要时用于构造对象。在这个例子中,您有一个名为FileReader
的类。文件读取器在首次构造文件时将文件加载到内存中。如果你有许多不同的FileReader
对象,你将消耗大量的内存。有时你不需要打开文件,直到后来。例如,如果您有 100 个文件读取器对象,您可能只能选择操作其中的 10 个。因此,在内存中保存其他 90 个文件是对资源的浪费。你的虚拟代理将推迟FileReader
的建造。
在第一个实验中,您将看到内存猪FileReader
对象使用了多少内存。这样,您可以比较在第二个实验中使用虚拟代理节省了多少内存。
app/experience 1 . PHP
$benchmark1 = memory_get_usage();
$baseDir = base_path();
$files = ['files/file1.txt', 'files/file2.txt', 'files/file3.txt'];
foreach ($files as $index => $file) {
$files[$index] = new App\File\FileReader($baseDir . $file);
}
$benchmark2 = memory_get_usage();
$difference = $benchmark2 - $benchmark1;
print "Memory used: {$difference}" . PHP_EOL;
当你运行这个的时候,你可能会得到一个和我不同的数字。我的实验#1 输出Memory used: 4373840
。这大约是 4.2MB。需要注意的是,此时您甚至没有使用FileReader
对象。一个数组中只存储三个文件读取器。为了理解为什么会消耗这么多内存,让我们来看看实际的FileReader
类。
app/文件/文件管理器. php
namespace App\File;
class FileReader implements ReaderInterface
{
// somebody wrote this class so that it
// loads a damn file when you construct... geesh
public function __construct($path)
{
$this->file = file_get_contents($path);
$this->path = $path;
}
public function countOccurancesOfWord($word)
{
return substr_count($this->file, $word);
}
}
为什么不重构这个文件阅读器?为什么不简单地将file_get_contents
移出构造函数?这将解决问题。这个类非常简单,所以很容易重构。然而,有两件事。一是不是所有的类都这么简单。第二,我想演示一个虚拟代理。所以发挥一下你的想象力,看看这个例子。求你了。谢谢你。我知道如果我礼貌地请求你,你会同意的。为了创建一个虚拟代理,你创建了一个被巧妙地称为FileReader
Proxy
的类。
app/文件/文件管理器 xy.php
namespace App\File;
class FileReaderProxy implements ReaderInterface
{
public function __construct($path)
{
$this->path = $path;
}
public function countOccurancesOfWord($word)
{
return $this->fileReader()->countOccurancesOfWord();
}
protected function fileReader()
{
if (! $this->fileReader) {
$this->fileReader = new FileReader($this->path);
}
return $this->fileReader;
}
}
这只是延迟了FileReader
的创建,直到countOccurancesOfWord
最终被调用。虽然没有在这里实践,但我可以初始化一个新的FileReader
,只是为了在 count occurrences 方法完成后立即销毁它。一遍又一遍地从一个文件中读取可能没有将文件存储在内存中有效,直到您完成它。在这种情况下,您将新的文件读取器存储在类的实例变量中,以便以后可以再次使用它。我向你挑战,换个方式试试。不要存储文件阅读器,看看这样节省了多少内存。说到内存使用,我们来运行实验 2。除了一个关键的区别,大部分代码都是相同的。
app/experience 2 . PHP
foreach ($files as $index => $file) {
$files[$index] = new \App\File\FileReaderProxy($baseDir . $file);
}
运行这个实验会产生输出Memory used: 10552
(10KB)。使用 10KB 的空间比使用 4000KB 的空间有效得多。当然,在所有三个文件上调用countOccurancesOfWord
方法将使用相同数量的内存。为什么要经历这些麻烦?在这个实验中,假设不是每个文件都会被加载。您还保留在每次调用完countOccurancesOfWord
方法时修改代理并从内存中删除文件的权利。这有助于保持空间空闲。不过,这不是这个练习的重点。真正的要点是理解虚拟代理如何允许你改变另一个类的性能。
远程代理(实验 3)
当您有远程运行的代码,但您希望透明地将其视为在本地运行时,远程代理非常有用。在本例中,您为一个 HTTP RESTful JSON 服务创建了一个代理。您可以直接调用 web 服务。您也可以直接处理 JSON。代理可以为您消除一些复杂性。您调用的这个 API 会找到一个人员列表。返回的人将有一个你需要的有偿或无偿标志。您将循环查找所有尚未付账的人。
app/experience 3 . PHP
$api = new \App\Api\ApiProxy;
$people = $api->findPeople();
foreach ($people as $person) {
if (! $person->paid) {
print "{$person->name} has not paid yet!" . PHP_EOL;
}
}
实验 3 中没有任何地方提到 HTTP 协议或 JSON。查看这段代码,似乎这里的一切都是在本地运行的。ApiProxy 处理 HTTP 客户机的美味佳肴。对于您的 HTTP 客户端,您使用 Guzzle,因为它非常适合使用。你可以建立一个真正的服务器进行通信,但这需要大量的工作。相反,你利用 Guzzle 的嘲笑能力来返回嘲笑的响应。
app/Api/MockedWebCalls.php
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
$json = json_encode([
['id' => 1234, 'name' => 'John', 'paid' => false ],
['id' => 2345, 'name' => 'Joe', 'paid' => true ],
]);
$stream = Stream::factory($json);
$response = new Response(200);
$response->setBody($stream);
\App\Api\HttpClient::$mocks = [$response];
不过,这种嘲笑与您的远程代理模式没有任何关系。这只是在嘲笑人们的反应。当您第一次调用 HttpClient 时,它将返回编码的 JSON 字符串作为响应,状态代码为 200。对 HttpClient 的任何额外调用都将导致抛出异常。到目前为止,这是一个巨大的代码。让我们看看您实际的远程代理。
app/Api/ApiProxy.php
namespace App\Api;
class ApiProxy
{
public function findPeople()
{
$client = new \App\Api\HttpClient;
$response = $client->get('http://some.api.com/find/people');
$peopleAsJson = $response->json();
$people = [];
foreach ($peopleAsJson as $personAsJson) {
$person = new App/Person;
$person->id = $personAsJson['id'];
$person->name = $personAsJson['name'];
$person->paid = $personAsJson['paid'];
$people[] = $person;
}
return $people;
}
}
ApiProxy
从 API 服务器获取 JSON 数据,并将其转换成一个由ApiPerson
对象组成的数组。在只传递数据的情况下,创建Person
对象可能不值得。现在,让我们假设您想要的不仅仅是 JSON。你应该选择那些可能有方法的类。在 PHP 中使用 JSON 对象确实给了你一些数据结构,但是它没有给你任何类方法。此外,您可以键入提示并扩展一个类;仅仅用 JSON 是无法做到的。这个例子的目的是展示如何通过为远程 API 创建一个代理来简化它的工作。
保护代理(实验 4)
有时候你需要保护一些东西。这是保护代理的工作。在本例中,您将模拟一个金矿和矿工。每个矿都有一定数量的黄金。矿工可以开采金矿,并继续这样做,直到金矿耗尽黄金。镇上来了一位新警长,他说没有一个矿工可以每天开采超过 500 盎司的黄金。让我们看看这个法则如何作为你的保护代理来应用。
app/experience 4 . PHP
$miner = new \App\Mining\Miner('Big Bad John');
$goldmine = new \App\Mining\MiningLaws(new \App\Mining\Goldmine(10000));
// it is okay to mine a little bit at a time
$amount1 = $miner->mine($goldmine, 10); // mined 10
print "{$miner->name} attempts to mine 10 ounces and got $amount1" .
PHP_EOL;
$amount2 = $miner->mine($goldmine, 50); // mined 50
print "{$miner->name} attempts to mine 50 ounces and got $amount2" .
PHP_EOL;
$amount3 = $miner->mine($goldmine, 500); // only 100 due to mining l
aws proxy
print "{$miner->name} attempts to mine 500 ounces and got $amount3"
. PHP_EOL;
在本例中,您有矿井和矿工。矿工可以采矿,从矿井中提取资源。
app/Mining/Miner.php
namespace App\Mining;
class Miner
{
public function __construct($name)
{
$this->name = $name;
}
public function mine(Mine $mine, $amount)
{
return $mine->extract($amount);
}
}
接下来,我们来考察金矿类。它记录了可供开采的黄金数量。金矿是用一定数量的可开采的黄金来初始化的。他们不能提取比现有的更多的黄金。
app/Mining/Goldmine.php
namespace App\Mining;
class Goldmine implements Mine
{
const TYPE = 'gold mine';
protected $amountAvailable;
public function __construct($amountAvailable)
{
$this->amountAvailable = $amountAvailable;
}
public function extract($amount)
{
if ($amount > $this->amountAvailable) {
$amount = $this->amountAvailable;
}
$this->amountAvailable -= $amount;
return $amount;
}
}
这一类中没有任何东西阻止矿工一次开采超过 100 盎司的黄金。这就是你的保护代理的用武之地。在实验 4 中,一名矿工试图开采 10、50 和 500 盎司的黄金。你可以在你的Goldmine
中放置条件逻辑,不接受超过 100 盎司的黄金,但是将采矿法与实际的矿分开会给你更多的灵活性。它是灵活的,因为它允许您保护其他类型的矿山,并将采矿法与实际的金矿脱钩。另一个好处是,挖掘规则可以在运行时修改。所以让我们看看这个采矿法保护代理是如何工作的。
app/Mining/MiningLaws.php
namespace App\Mining;
class MiningLaws implements Mine
{
public function __construct(Mine $mine)
{
$this->Mine = $Mine;
}
public function extract($amount)
{
// limit to only 100 units at a time
if ($amount > 100) {
$amount = 100;
}
return $this->mine->extract($amount);
}
}
保护代理保护底层对象。你在利用组合而不是继承。你可以让MiningLaws
延长Goldmine
,但是在那种情况下,那些法律将会与金矿联系在一起。当你想使用其他类型的矿山,如铜矿或煤矿,你不能重复使用这些相同的法律。在某些情况下这可能没问题。然而,一般来说,当您耦合到接口而不是具体化时,更容易适应变化。
说到你本可以做得不同的事情,为什么不在这里抛出一个异常呢?简单的回答是因为我想引起你的注意。如果您抛出了一个CallThePoliceException?
,而不是将数量更改为 100,这是处理权限和保护代理时的一个好习惯。抛出异常是一个很好的练习阶段。如果你愿意的话,我把它作为一个练习留给你去尝试。
智能代理
有时你想给一个对象增加额外的功能。这类似于装饰工。它们的不同之处在于,智能代理通常将其附加功能隐藏在公共接口下。装饰器可以添加额外的新公共方法,智能代理可能不会这样做。让我们向 miner 应用添加一个事件 fire/broadcast。这里唯一改变的是,当一个矿工开采一些东西时,你希望一个事件被触发。您的名为LoudMouthMiner
的智能代理将为您处理此事。
app/Mining/loudhouthminer . PHP
namespace App\Mining;
class LoudMouthMining extends Miner
{
public function __construct($name, Illuminate\Contracts\Events\Dispatcher = null)
{
parent::__construct($name);
$this->event = $event ?: app('Illuminate\Contracts\Events\Dispatcher');
}
public function mine(Mine $mine, $amount)
{
$amount = parent::mine($mine, $amount);
$this->event->fire('loud.mouth.mined', [$this, $mine, $amount]);
return $amount;
}
}
在关于保护代理的部分,我谈到了使用复合。这里你把它混合起来,使用继承。你把Miner
当作一个抽象基类。虽然它没有被命名为 abstract,但它足够基本,所有其他的 miner 类型都可以从它扩展。如果您的父类Miner
中有其他方法,您就不必覆盖它们。这个代理做的唯一一件事是添加一个事件处理程序对象,该对象在 mine 方法内部被触发。因此,你不需要使用全合成,因为你不需要那么多的灵活性。做最容易的事。如果你不确定Miner
类以及它会带来多大的变化,你可能会坚持使用合成,因为就像我之前说的,它更容易适应未来的变化。
假设您不想将激发的事件添加到您可能创建的不同类型的Miner
中。从代码的角度来看,这是很困难的。您最终将不得不扩展一个LoudMouthMiner
或者编写重复的代码。更好的方法是重构并使用组合。你可以在构造函数中注入一个Miner
类型,这样LoudMouthMiner
将不再从基类Miner
中继承。够了。我们来看实验 5。
app/experience 5 . PHP
Event::listen('loud.mouth.mined', function (\App\Mining\Miner $miner, \App\Mining\Mine $mine, $amount){
print "{$miner->name} gone done mined {$amount} from the ol' " .
$mine::TYPE . PHP_EOL;
});
$miner = new \App\Mining\LoudMouthMiner('Big Bad John');
$goldmine = new \App\Mining\Goldmine(10000);
$miner->mine($goldmine, 10);
在 Laravel 的应用上注册事件处理程序将在触发loud.mouth.mined
事件的任何时候触发这个闭包。匿名功能打印出矿工的姓名、开采量和矿井类型。当您在$miner
上调用mine
方法时,事件将被触发并处理。除了在内部触发一个事件之外,大嘴矿工的行为与普通矿工非常相似。这是一个智能代理。
结论
代理模式有哪些缺点?一个缺点是有时重构会更容易。在您的FileReader
的例子中,您创建了另一个完整的类,您必须管理它来解决内存使用问题。代理是您必须维护的另一个类。你应该确信这是值得的。
另一个缺点是,当你使用 composition 时,你可能会编写许多类似包装器的方法来调用底层的真实主题。代理为您提供了两个地方来维护真正的主题代码。你可以通过继承来解决这个问题,但是这意味着你的基类会变得更加复杂。
除了缺点之外,当您想要用一个对象替换另一个占位符时,代理是一个有用的模式。代理的好处包括性能的提高、更简单的界面和增加额外的功能。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 233 页
十六、责任链
$> git checkout chain_of_responsibility
目的
通过给多个对象一个处理请求的机会,避免将请求的发送方耦合到接收方。链接接收对象,并沿着链传递请求,直到有对象处理它。 1
应用
生活中的许多事情都遵循责任链模式:军队、企业,甚至赌场老虎机。举个例子,打电话给你手机公司的客户支持。通常从自动语音系统开始。如果这还不能让你满意,那么你会被转到基本的第 1 级支持,从那里你可以一步步往上爬,直到你发现自己在和第 4 级工程师争论。在一场关于冰棍的激烈争论后,你要求和他的经理通话。
希望在这个指挥链的某一点上,你的要求得到满足。如果在这个过程中的任何一点,你的电话断线了,你必须用自动语音系统从零开始。
抽象结构
-
Client
会在某个Handler
的具体实例上调用handleRequest
。在某个时刻,客户端的请求被满足,客户端接收到某个请求。见图 16-1 。图 16-1。
Chain of responsibility pattern
-
Handler
是一个抽象类或接口。所有具体的处理程序都是从处理程序扩展而来的。所有的处理程序可能有也可能没有后继者。如果具体的处理程序不能处理这个请求,那么这个请求就被传递给它的继任者。 -
ConcreteHandler1/ConcreteHandler2
实际实现handleRequest
方法。但是请记住,处理程序不一定处理请求。请求可以交给实现Handler
接口的后继者。
例子
跳街 37 号的生活很艰难。大坏鸟垄断了方圆 20 个街区的毒品市场,得分就像拜访坏脾气的奥斯卡一样简单。我相信你熟悉杂草的不同测量方法;然而,为了清楚起见,让我们在这里列出它们。
-
克:基本单位
-
第八:3.5 克
-
四重:7 克
-
盎司:28 克
-
千克:1000 克
一个客户会要求一定数量的大麻。所有的请求都以不高兴的奥斯卡开始。如果客户的要求太高,爱发牢骚的奥斯卡会让他的老板帮你牵线搭桥。如果他的老板不能满足你的毒瘾,他会把你送上毒品交易链。这样一直持续到一个人都不剩。大坏鸟是头儿,所以如果你去见他,那么你要么是在开世界上最大的派对,要么你就有大麻烦了。为了到达大鸟,你必须通过一个关卡。这家伙是个直截了当的数字和商人,他也不怕开枪。所以要小心。
坏脾气的奥斯卡会和各种各样的客户打交道。一个客户甚至向他要一块饼干(猜猜是谁?).因为奥斯卡只是一个普通的街头流浪儿,他从来不给任何客户超过 3 克的毒品。为了得到 3 克以上,你必须对付奥斯卡的老板。他的老板嗤之以鼻,是个EighthDealer
只会在你的要求小于 7 克的情况下为你服务;否则,你会见到斯尼菲的老板,他是一个QuadDealer
。你的要求会不断上升,直到得到满足。
以下是您将为每个类定义的规则:
-
GramDealers
:份量不超过 3 克 -
EighthDealers
:份量不超过 7 克 -
QuadDealers
:份量不超过 28 克 -
OunceDealers
:不超过 1000 克,有麻醉保护 -
KiloDealers
:缉毒保护
示例结构
图 16-2 为结构示意图。
图 16-2。
Lots of dealing going on this street
履行
Dealer
类中有一些基本的助手方法。它是你的基础抽象类,所有的具体处理程序都将继承它。帮手的方法包括为客户服务,让老板处理,把计量换算成克,甚至拍客户。
app/Dealer.php
namespace App;
abstract class Dealer;
{
protected $boss;
protected $name;
abstract public function dealWith(Client $client);
public function __construct($name)
{
$this->name = $name;
}
public function boss(Dealer $dealer)
{
$this->boss = $dealer;
}
protected function shoot(Client $client)
{
print "{$client->name} got shot" . PHP_EOL;
}
protected function serve(Client $client)
{
print "{$client->name} got {$client->request} from {$this->name}" . PHP_EOL;
}
protected function letTheBossDealWith(Client $client)
{
if ($this->boss) return $this->boss->dealWith($client);
}
protected function convertRequestToGrams(Client $client)
{
// returns requested amount in your basic grams unit
}
}
当从Dealer
类继承时,有一个抽象方法你必须实现,那就是dealWith
。首先,你要弄清楚客户想要多少。接下来,你来处理。对于一个克经销商来说,当要求的数量在 1 到 3 克之间时,你就为客户服务了。你给客户的服务不能少于 1 克。任何超过 3 克的东西都需要你的老板来处理。
app/GramDealer.php
namespace App;
class GramDealer extends Dealer
{
public function dealWith(Client $client)
{
$amount = $this->convertRequestToGrams($client);
if ($amount < 1) return;
if ($amount > 3) return $this->letTheBossDealWith($client);
return $this->serve($client);
}
}
GramDealer, EighthDealer, QuadDealer, OunceDealer
和KiloDealer
都共享同一个接口dealWith
,非常相似。当他们无法让客户满意时,他们也会依赖老板。在您的模拟中,红眼 Mos 将从 Oscar 获得 2 克,EarnEz 从 Kabby 获得 1 盎司,以此类推。
app/simulator.php
// create the dealers
$grouchyOscar = new \App\GramDealer('Grouchy Oscar');
$dealer2 = new \App\EighthDealer('Sniffy');
$dealer3 = new \App\QuadDealer('Kabby');
$dealer4 = new \App\OunceDealer('AC Countant');
$dealer5 = new \App\KiloDealer('The Big Bad Bird');
// setup the chain of responsibility
$grouchyOscar->boss($dealer2);
$dealer2->boss($dealer3);
$dealer3->boss($dealer4);
$dealer4->boss($dealer5);
// all deals start with Grouchy
$grouchyOscar->dealWith(new \App\Client('Red Eye Mos', '2 grams'));
$grouchyOscar->dealWith(new \App\Client('EarnEz', 'ounce'));
$grouchyOscar->dealWith(new \App\Client('Tellme Fatz', 'quad'));
$grouchyOscar->dealWith(new \App\Client('Cookie Hipster', 'cookie'));
$grouchyOscar->dealWith(new \App\Client('Zo 2 Easy', '99 grams'));
$grouchyOscar->dealWith(new \App\Client('Bertie', '4 eighths', $narc = tr\
ue));
$grouchyOscar->dealWith(new \App\Client('Seth Rogen', '2 kilos'));
// Sniffy and Kabby are taken out of play
// because Bertie busted them
$grouchyOscar->boss($dealer4);
// Bertie the Narc gets greedy
// and gets shot
$grouchyOscar->dealWith(new Client('Bertie', 'kilo', $narc = true));
看到这有多灵活了吗?格鲁希甚至在执行过程中把他的老板换成了会计师。在这个特定的模拟中,您以线性方式组织了每个经销商,但是责任链允许您非常灵活地更换继任者,而无需更改子类。这个链条让斯尼菲的老板卡比与格鲁希脱钩。这很重要,因为在第 37 跳街,毒贩经常被逮捕或枪杀。能够在运行时动态替换经销商可以让您的程序继续运行。假设您已经将每个类耦合在一起。你会有一些非常不灵活的东西。见图 16-3 。
图 16-3。
Way too much inheritance, homie!
这种类型的链式继承将您锁定在一个固定的流程中。你不需要这种锁定,尤其是当你想让一个GramDealer
跳过一个EighthDealer
而一个QuadDealer
直接到达一个OunceDealer
的时候。
结论
我喜欢把责任链模式理解为“当你的客户令人讨厌时,给你的老板打电话”模式。你的老板比你赚得多是有原因的,所以让他去处理更大的问题。
使用这种模式时,应该警惕循环引用。除非您的应用处理它们,否则您可能会走向无限循环。基本上,在下面的代码中,你是在说奥斯卡的老板是 Sniffy,Sniffy 的老板是奥斯卡,这没有意义。下属不能当老板。这就是你陷入无限循环的原因:循环引用。这是这种模式的一个缺点。
危险的循环引用
$grouchyOscar->boss($dealer2);
$dealer2->boss($grouchyOscar); // WAT?
$grouchyOscar->dealWith(new Client('Infinite Loop Man!', '2 grams');
// Oscar handles this
$grouchyOscar->dealWith(new Client('Infinite Loop Man!', 'kilo');
// loopty loop forever!
这种模式的好处是它允许您解耦请求链。在这方面,它非常灵活。对于那些有计算机科学头脑的人来说,你可能会看到责任链模式和面向对象的有限状态机的相似之处,其中每个状态都是终结的。然而,从技术上讲,一个有限状态机可以有多个后继。责任链模式只有一个继任者。有多个怎么知道选哪个接班人?只有一个继任者意味着你不用担心挑选一个。这个限制使得链式模式比有限状态机更容易使用。如果你发现自己需要一个有限状态机,那么就去看看关于状态模式的那一章。
您已经看到了将请求解耦到单独的类如何帮助您灵活地连接和完成请求。这是一个可以使用的强大模式。不过要记住,伴随着巨大力量而来的是邪恶的外星人 2 和大地精 3 。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 251 页
2
http://en.wikipedia.org/wiki/Venom_%28comics%29
3
http://en.wikipedia.org/wiki/Hobgoblin_%28comics%29
十七、命令
$> git checkout command
目的
将请求封装为一个对象,从而允许您用不同的请求参数化客户端,对请求进行排队或记录,并支持可撤销的操作。 1
应用
当您需要将执行动作的对象与调用动作的对象分离时,这是一个很好的使用模式。然而,最后一部分到底是什么意思呢?假设您想要将一系列做各种不同事情的事件排队。稍后您将清除队列,这将实际调用所有这些操作。你为什么不立即调用/执行该操作?将一些动作排在后面有一些好处。一个是您可以保持调用动作的顺序。这允许您拥有撤消功能。另一个好处是您将请求具体化为一个对象。这使得定制新的请求就像创建新的命令类一样简单。
许多现实世界的例子都遵循命令模式。举个例子,一个顾客(也叫客户)点了他的食物。你可以把命令想象成命令。服务员是点菜的人。厨师收到订单,为顾客准备美味的晚餐。见图 17-1 。
图 17-1。
Too many cooks (Clipart provided by openclipart.org.2 3 4 5)
抽象结构
-
Client
创建具体的命令实例供Invoker
使用。见图 17-2 。 -
Receiver
是命令将要操作的对象。这可能是一个文档、数据库或任何数量的类,它们保存您正在执行命令的实际数据。这些命令都在这个 receiver 对象上操作。 -
Command
是一个抽象类,定义了所有具体命令的结构。 -
ConcreteCommand
是一个特定的命令类。虽然不是必需的,但它通常具有回滚操作的能力。命令作用于接收器。 -
Invoker
是实际调用命令的内容。假设你是客户,电视是接收器;Invoker
将是遥控器。一些可能被调用的命令是音量增大和减小。
图 17-2。
Command pattern UML
例子
您将探索在 Laravel 6 中迁移 是如何工作的,而不是创建一个虚构的示例。Laravel 中的迁移受到了 Rails 框架的启发,并提供了一种一致的方式来为数据库创建表、列和索引。除了创建之外,迁移还提供回滚功能,以防您需要撤消数据库更改。
等一下。我以为这一章是关于命令模式的?是的。Laravel 迁移是在野外发现的命令模式的一个例子。我已经冒昧地将 Laravel 框架类映射到抽象 UML 图(图 17-2 )。因此,您将了解更多关于迁移和新模式的知识!谈两只死鸟。这样你就不会觉得被欺骗了,我将用一个使用电视的命令模式的简化例子来结束这一章。
示例结构
图 17-3 为结构示意图。
图 17-3。
Laravel migrations and the command pattern
履行
您要做的第一件事是创建一个迁移,在您的数据库中创建一个users
表。您没有设置数据库,所以对于这个例子,您将配置 Laravel 使用 SQLite。
DB_CONNECTION=sqlite
如果使用 SQLite,Laravel 将尝试使用默认名称为 database/database.sqlite 的数据库。用户需要手动创建该文件(即在命令行上使用 touch database/database.sqlite)。
确保为 PHP 启用了 SQLite 驱动程序。在 Ubuntu 上,这和sudo apt-get install php7.0-sqlite sqlite
一样简单。如果不想用 SQLite,可以随便用 PgSQL 或者 MySQL。您使用 SQLite 是因为它设置起来很容易。
如果您查看database/migrations
文件夹,您应该会注意到用户的迁移文件。它将附有时间戳,文件名的最后一部分将是create_users_table
。作为一种常见的良好做法,您应该根据您正在执行的操作来命名您的迁移。create_users_table
这个名字清楚地解释了它的目的和作用。如果你不创建一个表呢?如果要向现有的表中添加一个新字段,该怎么办?然后您可以将您的迁移命名为add_field1_to_users_table
。
- 命名迁移时,尽可能具体。
这有什么关系?这很重要,因为您不希望有两个名为do_stuff
的迁移;太令人困惑了,伙计。每次迁移都有一个目的。请尝试在文件名中清楚地说明这一目的。
这是生成的文件。
数据库/迁移/2014 _ 07 _ 11 _ 185334 _ create _ users _ table . PHP
class CreateUsersTable extends Migration
{
public function up()
{
// do command action
}
public function down()
{
// undo command action
}
}
CreateUsersTable
是从抽象的Migration
类扩展而来的具体命令。填充up()
和down()
部分是你的工作,所以现在让我们使用 Laravel 令人敬畏的模式构建器 7 来完成这项工作。
数据库/迁移/2014 _ 07 _ 11 _ 185334 _ create _ users _ table . PHP
public function up()
{
Schema::create('users', function ($table) {
$table->increments('id');
$table->string('first_name');
$table->string('last_name');
$table->string('email')->unique();
$table->string('password');
$table->timestamp('last_login_at')->nullable();
$table->timestamps(); // gives you created_at and updated_
});
}
public function down()
{
Schema::drop('users');
}
给你。您创建了一个新的users
表,其中包含一些字段:first_name
、last_name
、email
等等。让我们抓住这个坏男孩!
php artisan migrate
如果幸运的话,您应该会看到一条如下所示的消息,让您知道您创建的迁移命令已成功运行。如果没有,请确保您的数据库设置和配置正确。
Migrated: 2014_07_11_185334_create_users_table
如果您决定撤销这个迁移,您可以运行php artisan migrate:rollback
,它从您的CreateUsersTable
类运行down()
方法,然后从模式中删除users
表。
注意,如果您连续多次运行php artisan migrate
,脚本不会每次都尝试创建users
表。您的模式的调用者称为Migrator
,它将之前运行的迁移存储在一个名为migrations
的数据库表中。一会儿你会看一看Migrator
;现在让我们检查一下你的模式的client
类:??。
vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/migrate command . PHP
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
public function fire()
{
if (! $this->confirmToProceed()) {
return;
}
$this->prepareDatabase();
$this->migrator->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
foreach ($this->migrator->getNotes() as $note) {
$this->output->writeln($note);
}
if ($this->option('seed')) {
$this->call('db:seed', ['--force' => true);
}
}
vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator . PHP
public function run($paths = [], array $options = [])
{
$this->notes = [];
$files = $this->getMigrationFiles($paths);
$ran = $this->repository->getRan();
$migrations = Collection::make($files)
->reject(function ($file) use ($ran) {
return in_array($this->getMigrationName($file), $ran);
})->values()->all();
$this->requireFiles($migrations);
$this->runMigrationList($migrations, $options);
return $migrations;
}
public function runMigrationList($migrations, array $options = [])
{
if (count($migrations) == 0) {
$this->note('<info>Nothing to migrate.</info>');
return;
}
$batch = $this->repository->getNextBatchNumber();
$pretend = Arr::get($options, 'pretend', false);
$step = Arr::get($options, 'step', false);
foreach ($migrations as $file) {
$this->runUp($file, $batch, $pretend);
if ($step) {
$batch++;
}
}
}
protected function runUp($file, $batch, $pretend)
{
$file = $this->getMigrationName($file);
$migration = $this->resolve($file);
if ($pretend) {
return $this->pretendToRun($migration, 'up');
}
$this->runMigration($migration, 'up');
$this->repository->log($file, $batch);
$this->note("<info>Migrated:</info> {$file}");
}
客户机利用构造函数中传递的Migrator
并最终运行迁移器。上面要注意的重点是$this->migrator->run($paths = [], array $options = [])
法。这就是Invoker
的开始。如果您想了解迁移是如何回滚的,您可以查看另一个名为RollbackCommand
的命令。它像MigrateCommand
一样使用Migrator
来调用rollback
方法,而不是run
。
Migrator
作为Invoker
运行每个迁移命令。但是等等。为什么Client
知道Invoker
?我以为客户端依赖于Command
和Receiver
?
上面的 UML 图有错吗?这真的不是命令模式吗?当你在野外发现模式时,它们并不总是与“我不知道泰勒·奥特威尔在想什么”相匹配。我怀疑当他写这些东西的时候,命令模式是否在他的脑海中。更有可能的是,他在做他觉得正确的事情,于是这种命令模式出现了。没关系。你不是警察。
事实上,泰勒写这个的方式,他几乎已经排除了一个Receiver
类。不过,不要让这欺骗了你,因为在 Laravel 中,使用所谓的 facade 8 (不要与 facade 模式混淆)允许你从任何地方全局访问框架的许多不同部分。在您的CreateUsersTable
中,Schema
构建者承担Receiver
的角色。
多棒啊。想象一下,在运行任何类型的迁移之前,您必须检查它以前是否运行过。那不是很糟糕吗?幸运的是,您所要担心的只是填写每次迁移的up()
和down()
部分;剩下的在拉勒维尔手上。迁移通常用于改变模式,但不限于此。您可以使用 up()将照片从互联网下载到 public/awesome-cat-photos 目录中。down()
命令可以简单地删除 public/awesome-cat-photos 目录。
需要指出的是,迁移是按顺序调用的。这就是为什么每次迁移的文件名中都有一个时间戳:以确保所有迁移都按照特定的时间顺序运行。命令模式也包含时间顺序。当您在文本编辑器中按 Ctrl+Z 时,它应该会撤消您刚刚做的最后一件事。其他任何事情都会令人沮丧。
命令模式的出现正是为了解决这个问题:文本编辑器的撤销按钮!作者不想将命令请求与文档本身联系起来。他们将文档用作一个Receiver
,通过您刚才看到的命令模式,可以很容易地撤销在文档对象上调用的命令。
电视命令模式示例
在这个简短的例子中,我将使用电视、遥控器和手来说明命令模式是如何工作的。他们说一张照片胜过一千句话。不过,谁在数呢,阿米赖特?任何人,这里有一张图描述了你想要编码出来的情况(见图 17-4 )。
图 17-4。
Using the command pattern (clip art provided by openclipart.org)
您将制作一个电视遥控器,它可以执行命令,但也可以存储可以撤消的命令的历史记录。大多数电视遥控器不存储命令的历史。不是很好的用户体验。如果你按下音量增大按钮,最好按下音量减小按钮,而不是撤销按钮。在本例中,您正在构建一个间谍遥控器。它将保存所有发送命令的历史记录。为什么呢?因为你是个间谍。这是你的遥控器,它充当调用者。
app/TV/remote control . PHP
namespace App\Television;
class RemoteControl
{
private $history;
public function __construct()
{
$this->history = new \SplStack;
}
public function invoke(Command $command)
{
$this->history->push($command);
$command->fire();
}
您在命令对象的堆栈中跟踪历史。但是,请注意调用者并不知道实际命令在做什么。调用程序只触发命令并保存历史记录。您可以添加更多功能。您将提供一种撤消命令的方法。
app/TV/remote control . PHP
public function undo($amount = 1)
{
while ($amount-- > 0 && ! $this->history->isEmpty())
{
$command = $this->history->pop();
$command->undo();
}
}
这个undo
函数从堆栈中弹出命令,并对命令调用undo
方法。那么命令看起来像什么呢?这是一个处理不断变化的卷的命令。
app/TV/change volume . PHP
namespace App\Television;
class ChangeVolume implements Command
{
protected $tv;
public function __construct(Television $tv, $delta = 1)
{
$this->tv = $tv;
$this->delta = $delta;
}
public function fire()
{
$volume = $this->tv->getVolume();
$this->tv->setVolume($volume + $this->delta);
}
public function undo()
{
$volume = $this->tv->getVolume();
$this->tv->setVolume($volume - $this->delta);
}
}
该命令用一个电视接收机对象和一个增量初始化。Delta 是一个整数,用于知道每次执行该命令时电视音量应该改变多少。大多数命令都耦合到接收器。如果您希望能够与实现电视接口的其他具体类一起工作,您可以将接收器作为一个接口。不过,在这个例子中,您直接将命令耦合到接收器。你的接收器,电视,负责存储音量。它可以存储其他属性,如频道号、输入源、视频/音频设置。为了简化这个例子,电视只存储音量。
app/TV/Television . PHP
namespace App\Television;
class Television
{
protected $volume;
public function getVolume()
{
return $this->volume;
}
public function setVolume($volume)
{
if ($volume < 0) $volume = 0;
if ($volume > 50) $volume = 50;
$this->volume = $volume;
}
}
除了充当模型和存储卷之外,Television
类还有一些业务逻辑来确保没有卷是负数或超过 50。任何超过 50 的声音都会弄坏扬声器。你不想要坏了的扬声器。电视课对命令一无所知。最后,您将客户端代码付诸实施。客户端的第一步是创建invoker
、receiver
和command
对象。
app/simulator.php
$tv = new \App\Television\Television;
$control = new \App\Television\RemoteControl;
$volumeUp = new \App\Television\ChangeVolume($tv, 1);
$volumeUpFour = new \App\Television\ChangeVolume($tv, 4);
$volumeDown = new \App\Television\ChangeVolume($tv, -1);
接下来,客户端调用命令。我在每个调用方法旁边都注释了音量的变化。这将在你撤销命令时处理。
app/simulator.php
$control->invoke($volumeUp); // 1
$control->invoke($volumeUp); // 2
$control->invoke($volumeDown); // 1
$control->invoke($volumeUp); // 2
$control->invoke($volumeUp); // 3 <-- 6 more
$control->invoke($volumeDown); // 2
$control->invoke($volumeUpFour); // 6
$control->invoke($volumeUpFour); // 10
$control->invoke($volumeUp); // 11
$control->invoke($volumeUp); // 12
$control->invoke($volumeUp); // 13 <-- 4 ago
$control->invoke($volumeUp); // 14
$control->invoke($volumeUp); // 15
$control->invoke($volumeUp); // 16
$control->invoke($volumeDown); // 15 <-- current
您检查当前状态下的音量。应该是 15。接下来,回滚四个命令,然后再回滚六个,每次都确保输出音量。这些水平应该是 13 和 3。
app/simulator.php
print $tv->getVolume() . PHP_EOL; // 15
$control->undo(4);
print $tv->getVolume() . PHP_EOL; // 13
$control->undo(6);
print $tv->getVolume() . PHP_EOL; // 3
所以你有它。命令模式如何工作的另一个例子。构建和处理调用者、接收者和命令的任务留给了客户端。建造工作可以委托给名为CreateRemoteControl
的工厂。
结论
命令模式在处理请求时非常有用,如果将它们作为对象处理,会更容易处理。在这些例子中,你可以写一个大类来处理电视机的所有命令。您可以编写一个大型 SQL 文件来处理数据库。这样做使得处理代码的特定部分变得困难,因为您有一大块代表所有命令的代码。您将每个命令封装到它自己的对象中。这意味着重用命令和提供可撤销的动作要容易得多。
命令模式的主要缺点是增加了处理命令的类。班级越多,尤其是班级差异很大的时候,就越容易失去凝聚力。使用迁移时,假设您创建的所有类都与数据库结构的变化有关。然而,情况并不一定如此。你可以使用迁移将海蒂·克鲁姆的图片下载到/public
目录中。这是对迁移的严重滥用,但在技术上仍然是可能的。只要保持命令的目的一致,就不需要太担心整体的内聚性。
接下来,你将学习解释器模式(见图 17-5 )。
图 17-5。
You’ve been beckoned. Footnotes 1
Design Patterns: Elements of Reusable Object-Oriented Software
,第 263 页
2
https://openclipart.org/detail/154837/people-cook-by-yyycatch
3
https://openclipart.org/detail/77077/waiter-by-shokunin
4
https://openclipart.org/detail/182377/notepadr-by-crisg-182377
5
https://openclipart.org/detail/77077/waiter-by-shokunin
6
http://laravel.com/docs/migrations
7
http://laravel.com/docs/schema
8