PHP Laravel 设计模式(一)
一、Laravel 基础
如果你打算做 PHP 开发,你应该下载 Laravel。Symfony 和 Laravel 是世界上最流行和最好的 PHP 框架。要在您的机器上安装 Laravel,您可以遵循快速启动页面 1 上的说明。你将需要 PHP7 和 OpenSSL、PDO、Mbstring、Tokenizer 和 XML PHP。它还要求启用 PHP 版本 5.6.4 或更高版本。你知道怎么安装,对吧?太棒了。但是为了以防万一,在 Ubuntu 上,使用下面的代码。
安装 PHP 和一些依赖项
> sudo add-apt-repository ppa:ondrej/php
> sudo apt-get update
> sudo apt-get install php7.0 php7.0-curl php7.0-mcrypt
现在,要创建一个新的 laravel 应用,我们只需按照名为designpatterns的文件夹中的 https://laravel.com/docs/5.3/installation#installing-laravel 上提供给我们的说明进行操作。当您构建本书中的各种应用时,请查阅每一章的 git 分支,以便 git Jedis 可以跟踪。
接下来你要做的是看看作曲家。Laravel 是基于大约 20 个 Composer 包构建的;作曲是猫的喵(重要信息见图 1-1 )。
图 1-1。
Meow
什么是作曲家?
Composer 是 PHP 的依赖管理工具。它允许您列出应用正常运行所依赖的包。项目根目录中的一个名为composer.json的文件允许大量的配置选项,所以让我们先浏览一下其中的一些。
Composer 做了几件巧妙的事情:
-
包的依赖性管理
-
PSR 和基于自定义文件的自动加载
-
编译器优化有助于代码运行更快
-
定制挂钩到生命周期事件,例如应用的安装、更新或首次创建。
-
稳定性检查
使用您最喜欢的文本编辑器,打开项目根目录中的composer.json文件。请注意,在本书中,文件名都是相对于项目根的。这里要明确的是,当我说项目根时,那意味着直接在你创建的designpatterns文件夹中,所以app/MODELS/User.php实际上是我机器上的路径/home/kelt/book/designpatterns/app/models/User.php。
元信息
在 Composer 清单的第一部分,您可以看到基本的元信息。
composer.json
"name": "laravel/laravel",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"license": "MIT",
所有这些信息都被一个名为包装商 2 的网站所使用,该网站对外面的包裹进行分类。作为一个标准实践,如果你创建包来托管在 Packagist 上,你可能希望name与那个包的 GitHub 库相同。
依赖性管理
接下来你会看到一个require块。这就是包依赖管理发挥作用的地方。目前你只需要 Laravel 框架,它由许多其他的包组成;然而,随着时间的推移,你会添加额外的软件包。
composer.json
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.3.*"
},
看起来很简单,对吧?不过这里有一个问题,你可能会看到一个∾4.1或>=1.0,<1.1 | >=1.2.访问 https://getcomposer.org/doc/01-basic-usage.md#package-versions 解释不同版本的规则,阅读表 1-1 也是如此。
表 1-1。
Version Rules
| 名字 | 例子 | 描述 | | --- | --- | --- | | 精确版本 | `1.0.2` | 您可以指定软件包的确切版本。 | | 范围 | `>=1.0` | 通过使用比较运算符,您可以指定有效版本的范围。 | | | `>=1.0,<2.0` | 有效的运算符是`>, >=, <, <=, !=.` | | | `>=1.0,<1.1 | >=1.2` | 您可以定义由逗号分隔的多个范围,这些范围将被视为逻辑 AND。管道符号|将被视为逻辑或。并且具有比 OR 更高的优先级。 | | 通配符 | `1.0.*` | 您可以使用*通配符指定模式。`1.0.*`相当于`>=1.0,<1.1.` | | 波浪号运算符 | `∼1.2` | 对于遵循语义版本化的项目非常有用。`∼1.2`相当于`>=1.2,<2.0.` |虽然这里没有显示,但是您可以通过使用require-dev.为只开发包添加一个映射,behat、phpspec 和 clockwork 都是只开发包的很好的候选者。
半自动的
前面我提到 Composer 附带了一个自动加载器,甚至优化了 PHP 以使其运行得更快。因为有了autoload部分,它知道如何做到这一点。
composer.json
"autoload": {
"classmap": [
"database"
],
"psr-4":{
"App\\": "app/"
}
},
您也可以使用 PSR 自动加载。如果你从未听说过 PSR,请花点时间去 http://petermoulding.com/php/psr 。基本上,它处理 PHP 的文件夹结构、名称空间和类名的标准化。
Laravel 5 使用 psr-4 自动加载,不像它的前身 Laravel 4。如果您查看 composer.json 的内部,您会注意到这几行
"psr-4": {
"App" : "app/"
}
这允许我们在另一个 php 文件中使用完全限定的名称空间use App\Services\FooService;来引用一个文件,比如app/Services/FooService.php。
现在,您的应用将查看app文件夹,并为您自动加载该目录中的任何文件。很漂亮,对吧?
生命周期挂钩/脚本
下面是运行composer install或composer update或composer create-project后执行的脚本列表。
composer.json
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate"
],
"post-install-cmd": [
"illuminate\\Foundation\\ComposerScripts::postInstall",
"php artisan optimze"
],
"post-update-cmd": [
"illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
],
},
如果您想在这里运行自定义命令,可以进入 composer 的某些事件。我使用这些钩子在服务器上执行composer install的任何时候自动运行迁移之类的东西。当您部署到生产服务器时,您只需遵循这个简单的两步流程:
-
git pull -
composer install
您不必记得运行迁移或清理资产或其他任何事情,因为您是在composer install完成运行后才这么做的。
composer install和composer update?有什么区别
运行composer update会做两件事。
-
它会将所有必需的软件包更新到匹配的最新版本。
-
它将使用依赖项的确切版本更新
composer.lock。
运行composer install将安装composer.lock中列出的依赖项。如果锁文件不存在,这个命令变得和composer update一样,因为它会在下载完依赖项后为您创建composer.lock文件。
我们为什么要这样做?
假设您在机器 1 上运行 Composer,然后在机器 2 上运行。您需要确保两台机器上的包完全相同。如果你运行composer update,包的版本可能会因机器而异。这可能会导致问题。假设您作为依赖项需要的特定包改变了一个特性。突然,机器 2 抛出了一个大的 fat 500 内部服务器错误,而机器 1 仍然工作正常。你总是想避免那种行为。
理想情况下,你希望你的production、staging,和各种local环境尽可能相似。
您可以通过从.gitignore中删除vendor文件夹并提交所有内容来解决这个问题,但是还有一个更好的方法。锁文件已经有了特定的 github 提交散列,这不应该改变,您可以通过遵循以下基本原则来利用这一点:
仅在您的本地开发机器上运行
composer update。从未投入生产。
稳定性
现在您已经到达了您的composer.json文件的末尾。
composer.json
"config": {
"preferred-install": "dist"
},
Composer 可以通过源代码或分布式 zip 文件获取您的依赖项。这个配置选项告诉 Composer 使用优先于源代码的分发文件。你可以在 https://getcomposer.org/doc/04-schema.md#config 了解更多配置选项。
minimum-稳定性标志用于防止其他包将其他包的不稳定版本安装到您的应用中。
-
你需要 a 包。
-
包 A 需要包 B@dev。
-
作曲家会抱怨的。
因为您的稳定性被设置为stable,但是一个子包依赖于一个不太稳定的版本,所以会发生这样的问题。在这种情况下,包 A 依赖于包 b 的开发版本。
你会如何解决这个问题?在这个场景中,为了安装包 A,您需要显式地将包 B@dev 添加到require数组中。另一种解决方法是,将minimum-stability改为下面的一种:dev, alpha, beta, RC,或者stable。
运行作曲家
您可以通过 http://getcomposer.org/ 下载作曲家。安装后,通过运行以下命令来验证您的安装
> composer -v
您可以使用 Composer 运行许多命令。一个例子是不用文本编辑器编辑您的composer.json,您可以运行 Composer 命令来要求依赖关系:
> composer require
另一个有用的 Composer 命令是validate。尽管我接受了所有的 JavaScript 培训,但我仍然设法留下了尾随逗号,这是无效的 JSON,所以在更改后验证您的composer.json文件是一个好习惯。
设置环境变量
Laravel 为您的应用提供了简单的环境变量管理。在您新创建的 laravel 应用中查找一个名为. env 的文件。该文件将包含如下内容:
APP_ENV=local APP_KEY= APP_DEBUG=true APP_LOG_LEVEL=debug APP_URL=http://localhost DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret BROADCAST_DRIVER=log CACHE_DRIVER=file SESSION_DRIVER=file QUEUE_DRIVER=sync REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_DRIVER=smtp MAIL_HOST=mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null PUSHER_APP_ID= PUSHER_KEY= PUSHER_SECRET=
这个。env 文件允许您使用相同的应用设置不同的服务器。你可以想象我们可能有一个不同的临时服务器。env 设置文件比我们的生产服务器。
更多信息可以在 dot env 文件这里找到: https://laravel.com/docs/5.3/configuration#environment-configuration 。
在这里,您可以随意试验不同的配置文件和环境。在 Laravel 中,Die 和 dump 对于快速调试非常有用。但是,它不能替代好的测试或 Xdebug。既然你已经在 Laravel 中学到了一些基础知识,让我们继续学习坚实的原则。
Footnotes 1
2
二、让我们构建坚实的基础
$> git checkout solid_design
2009 年,一款名为 Farmville 的游戏在脸书爆红。我妈妈上瘾了;我可能也是,但我太骄傲了,不愿承认。我们不会创造另一个 Farmville,但让我们做一些类似的事情。
在这一章中,你将浏览罗伯特·c·马丁在他的著作《面向对象的原则》中首次介绍的五个坚实的原则。在这个展示区中,您将创建一个数字花园。有一个农民种了一个花园。咿呀咿呀咿呀。
$garden = new App\EmptyGarden(20, 30); // this is a pretty good size\
20x30' garden
$items = $garden->items(); // no plants here, just handfuls of dirt\
单一责任原则
第一个坚实的原则是单一责任原则。它规定一个类应该有一个单一的职责。 1 一个好的实践是在注释文档块中列出类的职责。这样你可以提醒自己和其他人这门课的目的,并尽量把责任降到最低。
app/EmptyGarden.php
namespace App;
<?php
/**
* @purpose
*
* Provides empty garden space full of dirt which can
* grow and produce items.
*
*/
class EmptyGarden
{
private $width;
private $height;
public function __construct($width, $height)
{
$this->width = $width;
$this->height = $height;
}
public function items()
{
$numberOfSpots = ceil($this->width * $this->height);
return array_fill(0, $numberOfSpots, 'handful of dirt');
}
}
你可以把items()方法叫做harvest(),但是这意味着一个花园可以自己收获。一般来说,农民或收获者从我们的花园里采摘甜美的花蜜。花园只是占用空间,收集生长的东西。收获自己,意味着又多了一份责任。
不要把单一责任原则走得太远。一个阶级经历的变化越少,所有这些原则就越不重要。如果
harvest()永远不会真正改变,那么将harvest()添加到EmptyGarden类中是没问题的。
记住,使用原则是有代价的。如果维护一个较大的类比维护五个解耦的较小的类更容易,就不要对抗潮流。封装变化的内容,不去管其他的。另外,也不要碰左边 4 个死去的女巫。
开/关原则
$garden = new App\MarijuanaGarden(10, 10);
$garden->items(); // about a day's worth for Seth Rogan
等一下。你为什么要创造一个 MarijuanaGarden?是因为狗吗(见图 2-1 )?将字符串Marijuana传递给Garden类不是更简单吗?从表面上看,这似乎更容易;当然要跟踪的班级更少了。不过,最终,沿着字符串路径前进会导致编写一些条件逻辑(if、else、switch ),并且每次想要添加新类型的花园时,都需要修改Garden类。
图 2-1。
You were promised drugs, remember?
实线中的 O 代表开放/封闭原则,表示类应该对扩展开放,但对修改关闭。 2
如果每次出现一个新类型的花园,你都必须修改这个Garden类,那么你就违反了开放/封闭原则。items()方法根据花园的类型返回不同的项目。请注意,下面的方法允许您轻松地添加许多花园,而无需修改原始的Garden类:
app/大麻
class MarijuanaGarden extends EmptyGarden
{
public function items()
{
return array_fill(0, $this->width * $this->height, 'weed');
}
}
利斯科夫替代原理
下一个坚实的原则被称为 Liskov 替换原则,考虑到 PHP 是一种鸭子类型的语言,这很难做到。它没有严格的变量类型。然而,PHP 确实有类型提示,所以让我们使用它。
程序中的对象应该可以用其子类型的实例替换,而不会改变程序的正确性。 3
如果当你创建一个EmptyGarden you的时候为你的宽度和高度传递一个字符串或者一个类或者负数会发生什么?
> new EmptyGarden("foo", -1);
那可不好!那会导致东西破裂。你想冒这个险吗?也许你可以在这里做一点重构。与其让你的花园依赖于width和height,不如让它依赖于PlotArea。这可以是占据空间的某个区域的界面。
src/EmptyGarden.php
public function __construct(use App\PlotArea; $plot)
{
$this->plot = $plot;
}
一个PlotArea长什么样?当然是接口啦!
应用/绘图.php
namespace App;
interface PlotArea
{
public function totalNumberOfPlots();
}
A PlotArea告诉你在这个地块区域你有多少块地可以种植。一个圆形花园可能有 20 块地,半径为 10 英尺。一个长方形的花园可能有 40 块地,面积为 10×10 英尺。注意,它是一个接口,而不是一个具体的类。
你现在需要改变你的EmptyGarden和MarijuanaGarden类。
app/EmptyGarden.php
public function __construct(PlotArea $plot)
{
$this->plot = $plot;
}
public function items()
{
$numberOfPlots = $this->plot->totalNumberOfPlots();
return array_fill(0, $numberOfPlots, 'handful of dirt');
}
但是你现在怎么称呼你的EmptyGarden?PlotArea不是类,是接口。如果创建一个实现了PlotArea的RectangleArea类会怎么样?
app/RectangleArea.php
namespace App;
use App\PlotArea;
class RectangleArea implements PlotArea
{
private $width;
private $height;
public function __construct($width, $height)
{
$this->width = $width;
$this->height = $height;
}
public function totalNumberOfPlots()
{
return ceil($this->width * $this->height / 2);
}
}
现在你可以修改你的课程了。
php 工匠修补匠
$garden = newApp\EmptyGarden(new App\RectangleArea(10, 10))
$garden->items();
你有 50 把土!这么多脏东西!查理·布朗的猪圈会很骄傲的。他为什么这么脏?他的父母在哪里?他是如何避免葡萄球菌感染的?
破坏 LSP 的另一种方法是从同一个方法返回不同的类型。在像 Java 这样的强类型语言中,这不是一个大问题;在 PHP 中,这可能是个问题。
想象一下,如果您调用$garden->items();,您希望返回的类型是什么?一个数组,对吗?如果MarijuanaGarden返回一个字符串呢?这会很快变得一团糟!这也打破了利斯科夫替代原理。
不幸的是,如果你使用 php 5,那么你将无法在方法 4 上设置返回类型提示。好消息是,返回类型提示是 php 7 中的一个新特性!因此,您可以使用以下语法(见下文):
public function items() : array { ... }
如果你仍然在使用 php 5,那么你仍然可以使用 doc-blocks 和@return 注释。这对编译器没有任何帮助,但可以帮助其他开发人员查看您的代码。如果你使用的是 php 7,那么我绝对推荐使用类型提示。如果不小心的话,很容易通过在子类方法中返回不同的类型来破坏 PHP 中的 Listov 替换原则。
界面分离原理
如果发现某样东西有用,那就一定要改变,对吧?你想给你的花园增加更多的功能。花园种植植物。你把种子种在地里,经过一点努力和好运,这些种子会生长并结出果实。
app/GardenInterface.php
namespace App;
{
public function grow($advanceNumberOfDays);
}
这很好。然而,花园也要施肥、浇水、除草,容易受到虫子和兔子等害虫的攻击,并取决于阳光和雨水等天气因素。对于一个班级来说,这是很大的责任。让我们看看如果你添加更多的方法会是什么样子。
app/GardenInterface.php
namespace App;
{
public function grow($advanceNumberOfDays);
public function weed($pickOutPercentage);
public function pestAttack($attackFactor);
public function water($inGallons);
public function sunshine($radiationLevel);
public function fertilize($type, $amount);
...
}
神圣的方法,蝙蝠侠!注意,随着这个接口变得越来越大,任何实现GardenInterface的具体类的责任也变得越来越大。这意味着单一责任原则也可能被违反。当你违反了五条原则中的一条,其他原则也有可能被违反。
坚实的原则和谐地一起工作。这里的主要问题是,当理想情况下你可以封装不同的行为时,你却把许多功能塞进了一个花园。在单个类中放入的功能越多,管理该类就越困难。
人们常常很容易陷入领域建模和考虑实体,以至于我们忘记了面向对象编程不仅仅是为“事物”创建对象封装行为是面向对象设计的一个强大部分。我们可能有一个叫做Cat的物体,它是一个真实的东西(名词)。此外,我们可能还有一个名为CatMeows的对象,它更像是一个动作动词而不是名词。如果你还不够困惑,只要记住不是每个物体都会拉屎。
另外,如果您只想使用grow()方法呢?你会实现所有这些其他的方法来得到那一个方法吗?您可以创建一个只是子接口集合的花园。
src/GardenInterface.php
interface GardenInterface implements GrowableInterface, WeedableInterface, ...
{
}
拥有一个由小界面组成的主界面当然更加灵活,因为它允许你从那些小界面中挑选,但是它不能解决你的花园变得越来越复杂的问题。最终,您将添加更多的类来解决这个问题。现在,您可以为EmptyGarden类创建空方法。
保持你的接口小遵循接口分离原则:许多特定于客户端的接口比一个通用接口更好。 5
从属倒置原则
DIP(依赖倒置原则)声明一个人应该依赖抽象而不是具体化。 6 这是什么意思?要回答这个问题,回想一下之前你为你的花园定义了一个PlotArea;如果你这样做了会怎么样:
app/Garden.php
public function __construct(RectangleArea $plot)
{
$this->plot = $plot;
}
这将迫使你为每个花园使用一个矩形区域。如果你需要各种形状的花园,这根本行不通。因此,为了避免不灵活的问题,您使用抽象(PlotArea接口)来代替具体化(RectangleArea类)。依赖性反转的这一方面被称为依赖性注入,它是通过注入实现特定接口的类来实现的。
另一个发现违反依赖反转的可靠方法是当你开始在代码中看到new关键字时。想象一下,如果您刚刚在items()方法中创建了一个新的矩形区域。
app/Garden.php
public function items()
{
$numberOfPlots = new RectangleArea; // oh no's!
return array_fill(0, $numberOfPlots, 'handful of dirt');
}
在这个例子中,EmptyGarden类是一个高级类,并且依赖于一个低级类RectangleArea。看到new这个词了吗?
在应用中的某个时候,你可能会使用单词new来创建一个对象。这很好,但是这确实意味着这个类与另一个类耦合,并且产生了一个硬依赖。用new,创建对象没有错,但是在错误的地方这样做,会导致脆弱的耦合代码,更难测试和维护。我试图将我的new语句保存在更高级别的代码和工厂中(稍后你会学到更多关于工厂的知识)。
在软件工程中,有一句话:低耦合,高内聚。耦合是一个类对另一个类的依赖程度。内聚性是一个类中的元素属于一起的程度。
假设你的班级是一座孤岛;你会希望它内部一切正常。经济好,犯罪率低等。如果你的岛屿确实依赖其他国家,那么你希望保持最小化。为什么呢?想象一下,如果这些国家中的一个与你在脸书断绝关系,导致你的经济崩溃。现在你的人民在挨饿。不酷。绝对不酷。因此,依赖很少的外部国家是低耦合和高内聚的一个例子。最好的岛是自己运行良好,不会让依赖压倒它的岛。同样,低耦合,高内聚。
为了减轻内聚性和耦合性问题,遵循高级类不应该依赖于低级类的实践,反之亦然。相反,依赖于接口、抽象类等抽象概念。
如何从低级班中分辨出高级?
想象一个指挥音乐会的大师在指挥一堂高级课。低级班是乐队里演奏乐器的人。指挥正在指挥低年级的班级,结果是美妙的音乐。
然而,想象一下大师依赖于一个具体的东西:大号手鲍勃。如果鲍勃因流感生病,大师必须关闭大型音乐会。如果他不依赖鲍勃,而是依赖抽象:大号手。在《德进行曲》中,一个不同类型的大号手,但是今晚的表演被拯救了!大师不必担心是鲍勃,弗雷德,还是萨利吹大号,只要他们有资格吹大号。这是最完美的依赖注入。
需要注意的一点是:依赖倒置和依赖注入不是一回事。另一种实现依赖反转的方法是使用控制容器的反转。
app/master . PHP
class Maestro
{
public function conduct($song)
{
$tubaPlayer = app()->make('tuba.player');
$clarinetPlayer = app()->make('clarinet.player');
foreach([$tubaPlayer, $clarinetPlayer] as $player)
{
$player->play($song);
}
}
}
请注意,这里您不依赖于任何类型的类。相反,您让 app()->make()为您提供所需的大号播放器和单簧管播放器。它们很容易在服务容器中被替换掉。
app/Providers/PlayerServiceProvider.php
如果说 Composer 是 Laravel 的脊梁,那么服务容器就是大脑。在后面的章节中,你会学到更多关于 Laravel 服务容器的知识。
结论
下面是校长总结的一些小技巧。
单一责任原则
不要把所有的工人鸡蛋放在一个篮子里。一个类应该有一个改变的理由。
开/关原则
不要一遍又一遍的换同一个班。如果你发现这种情况发生,抽象出是什么在改变。
李斯托夫替代原理
在重写的子类方法中返回与父类方法相同的类型。这同样适用于方法的参数。保持一致。
界面分离原理
不要用很多(超过五个)方法创建接口。这是你在一个地方做太多事情的迹象。
从属倒置原则
依赖接口和抽象类多于具体类。这样会更灵活。
封装变化的内容
只抽象出应用中不同的东西。例如,如果一个Mailer类永远不会改变,不要纠结于写一大堆抽象的东西:关注什么会改变。
Footnotes 1
http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)#Overview
2
en。维基百科。org/ wiki/ SOLID_(面向对象 _ 设计)#概述
3
http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)#Overview
4
https://wiki.php.net/rfc/returntypehint2
5
http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)#Overview
6
http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)#Overview
三、抽象工厂
$> git checkout abstract_factory
目的
提供创建相关或依赖对象系列的接口,而无需指定它们的具体类。 1
应用
当您拥有一系列多少有些关联的对象时,您可以使用工厂来创建产品。有时你想创造产品的变体。产品可以是任何类别。可能是User, Airplane,也可能是House。在处理房地产时,商业不同于住宅。在这种情况下,您可以创建两个工厂:一个用于商业,一个用于住宅。住宅工厂可以生产房屋和土地等产品。商业工厂可以生产像商店建筑和停车场这样的产品。房地产客户仍然可以进行诸如出售、购买和列出地段、土地、房屋和商店建筑等操作。当你改变你的工厂时,你不必改变你的客户。
抽象结构
-
这个类使用了抽象工厂。见图 3-1 。具体工厂可以作为参数传递给客户端的构造函数,或者客户端可以足够聪明地知道使用哪个具体工厂,而无需依赖注入。例如,客户端可能决定使用基于运行的操作系统(Windows、Linux 或 Mac)的系列。
图 3-1。
This is an abstract uml document of abstract factory. It’s all abstract!
-
AbstractFactory:这可以是具体工厂实现的接口或者抽象类。如果你使用一个抽象类,那么默认的行为可以内置到基础抽象类中。 -
ConcreteFactory(1 和 2):这些类从AbstractFactory继承,并为其具体的系列类型生成产品。图 3-1 中只显示了两个ConcreteFactories,但是如果需要可以有更多。 -
Product(A 和 B):这些是抽象类或接口,将由具体的产品类 A1、B1、A2、B2 等等来实现。 -
(A1/B1 和 A2/B2):这些类都属于一个家族。A1 和 B1 与 A2 和 B2 属于不同的家族,所以它们是由不同的工厂制造的。这些是您关心的类,并且以某种方式被客户端使用。
例子
您正在创建一个可以在 PG-13 或 R 级模式下运行的模拟器。游戏的名字叫花园忍者。植物生长在花园里,商人出售或消费这些植物。你可以创造各种各样的水果和蔬菜。你可以用多种方式出售农产品。商家的基本步骤是
-
种植一个花园。
-
卖农产品。
游戏的目标从未改变。改变的是基于玩家的产品的基本系列,它可以根据玩家的成熟程度而改变。
您将创建两个不同的花园商人家族:
-
毒品贩子
-
稻农
一个稻农将种植一个稻田。稻农不会像毒贩那样种植和销售大米。毒品贩子正在生产非法大麻,所以他的行动很可能是秘密的。
示例结构
图 3-2 为示例结构。
图 3-2。
Put it all together and make a family of related objects. Rice farmers and drug dealers are related by a family of products (gardens and stores). Who knew, right?
履行
如果你想看这个作品,下载资源库并查看 branch abstract_factory.
你的模拟器会从一个随机的成熟度等级中创建一个新的商家,然后从这个商家身上赚钱。由于存在风险,毒品贩子比种植水稻的农民赚得更多。不是说大米没有风险什么的。
app/simulator.php
require __DIR__ . '/../vendor/autoload.php';
$ratings = array(
'PG-13' => new GardenNinja\RatedPG13\RiceFarmer,
'R' => new GardenNinja\RatedR\DrugDealer
);
$merchant = $ratings[array_rand($ratings)];
$client = new App\Client($merchant);
$client->run();
这里有很多缺失的部分。让我们从检查Client类开始。
class Client
{
public function __construct(Merchant $merchant)
{
$this->merchant = $merchant
}
public function run()
{
print "Your merchant made $" . $this->merchant->makeMoney() . PHP_EOL;
}
}
接下来,Merchant类充当这个例子中Client类使用的抽象工厂。
app/Merchant.php
<?php namespace App;
abstract class Merchant
{
abstract public function createStore();
abstract public function createGarden();
您依赖于您的混凝土工厂,它们实现Merchant来创建您的两种产品:商店和花园。所有的商人都试图赚钱,所以你把这个方法放在这个抽象类中。
app/Merchant.php
public function makeMoney()
{
$makeMoneyMoneymakeMoneyMoney = 0;
$store = $this->createStore();
$items = $this->createGarden()->items();
foreach ($items as $item) {
$makeMoneyMoneymakeMoneyMoney += $store->price($item);
}
return $makeMoneyMoneymakeMoneyMoney;
}
如果你理解了RiceFarmer类的工作原理,你就应该理解DrugDealer类的工作原理。它们是相关的,因为它们都属于商人抽象家族。所以让我们来看看RiceFarmer类。
app/rate DPG 13/recrear . PHP
<?php namespace App\RatedPG13;
class RiceFarmer extends Merchant
{
public function createStore()
{
return new RiceStore;
}
public function createGarden()
{
return new RiceGarden;
}
}
很简单,对吧?RiceStore只是负责产品定价,而RiceGarden创造新的Rice商品供我们销售。然而,你可以在这里得到相当复杂的定价和多少项目返回到你的花园从各种外部因素。
当你运行php app/Simulator.php时,你会看到有时你的商家赚 20 美元,有时赚 300 美元。这完全取决于模拟器运行的随机等级以及传递给客户端的具体商家类型。
结论
您使用抽象工厂创建了一系列与Merchant相关的产品。你为什么做这些工作?在这个模拟中,您可以做一些条件语句并获得相同的结果。为什么要创建所有这些类?这对你有什么好处?
在这个例子中,你的类非常简单(RiceStore和RiceGarden)。在现实世界的例子中,这些类可能要复杂得多。您使用抽象工厂模式的模块化设计允许随着您添加额外的商家而增长。
在前面的章节中,您学习了如何封装变化。在您的模拟器中,您可以为其他蔬菜、大豆、草药和香料添加更多的商人类型。你甚至可以支持更疯狂的想法,比如糖果花园。这样做,你不会被迫编辑现有的职业,只会增加更多的商人类型到游戏中。
抽象工厂的一个缺点是对抽象Merchant类的任何改变都会渗透到所有的具体类中。这意味着你必须对你的应用的结构以及产品系列如何组合在一起进行长时间的思考。在您的示例应用中,按照成熟度等级对产品系列进行分组可能没有意义。
另一个缺点是,您可能会在使用这种设计上花费很多精力,如果事情需要大幅改变,重构可能会更加困难。在您的示例场景中,无疑有一种比使用抽象工厂设计模式更简单、更好的方法来构建这个应用,因为这个人为的示例相当简单。你能想到其他的缺点吗?
Footnotes 1
设计模式:可重用面向对象软件的元素,第 99 页
四、构建器
$> git checkout builder
目的
将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。 1
应用
构建器适合于创建复杂的产品。正如上一章所讨论的,产品可以是任何东西。所有的创造模式都集中在生产产品上。不过,有些产品天生就很复杂。因此,在这种情况下,您可以将构建过程委托给主管和构建者。稍后会有更多的介绍。生成器模式的一个真实例子是汽车的构造。装配线和工程师按照建造者模式生产成品:一辆汽车。当您想要对创建产品的许多步骤进行微调控制时,这是您的首选模式。
抽象结构
-
这个类包含一组指令(一种算法)来控制构建器的动作。构建器的特定实例可以作为构造函数或参数传递给 director 类上的公共方法。见图 4-1 。在您的示例中,您将使用后一种方法。
图 4-1。
Bob the Builder says, “YES WE CAN!”
-
这是一个抽象的基类或接口,它列出了可以用来构建产品的所有步骤。
-
ConcreteBuilder:这个类继承自抽象的Builder类,并保存创建产品的实际方法。根据需要,可以有许多不同的构建器。这个类将产生一个特别创建的Product。 -
这是一个复杂的物体,通常有许多螺母和螺栓或活动部件,不容易建造。它可能由许多不同的属性组成。
例子
有时候人们会制造复杂的东西。作为一个承包商的儿子,我是第一手见证人,盖房子不是一件小事。建设过程中要做大量的工作。幸运的是,有建筑师的蓝图来指导整个过程。这些蓝图是建筑师关于如何建造房子的说明清单。然而,两个不同的木匠阅读相同的蓝图会产生不同的结果。
几年前,我住在郊区的一栋三室两卫的房子里。在这种情况下,您将重建我的房子。这个Architect将扮演导演的角色,你将有一个NoviceCarpenter和ExpertCarpenter按照同一个建筑师的指示建造我的老房子。
示例结构
图 4-2 为结构示意图。
图 4-2。
Building a house
履行
在这个场景中,Architect将是导演,指导不同的Carpenter(builder如何建造房子。你真正关心的产品是房子,但为了得到房子,你必须使用木匠,为了使它更容易,你也使用建筑师。
app/Architect.php
namespace App;
{
public function createMyOldHouse(Carpenter $builder)
{
// house foundation
$builder->outside(25, 13);
// master bedroom
$builder->sidewall(5, range(1, 9));
$builder->wall(range(1, 5), 10);
$builder->wall(range(2, 5), 5);
$builder->door(5, 4, 'left bottom');
$builder->door(1, 5, 'left bottom');
$builder->door(5, 9, 'left bottom');
// bathrooms
$builder->sidewall(2, range(6, 9));
// bedroom 2
$builder->wall(range(8, 11), 10);
$builder->wall(8, 7);
$builder->wall(range(8, 11), 5);
//
//
// lots of code omitted here for brevity
//
//
$builder->label(8, 21, ' K');
$builder->label(11, 22, ' U');
}
}
Architect只是向构建器调用一些执行。builder接受这些执行并调整最终产品:??。你可以让Carpenter对此负责,但他已经有足够的精力去盖房子,而不用担心我在郊区的老房子。
让我们来看看Carpenter的方法。
app/Carpenter.php
namespace App;
{
protected $house;
public function __construct(House $house = null)
{
$this->house = $house ?: new House;
}
public function getHouse()
{
return $this->house;
}
public function outside($width, $height)
{
$this->house->layout = array_fill(0, $height, array_fill(0, $width, " "));
$this->topOutsideWall($width, $height);
$this->leftOutsideWall($width, $height);
$this->rightOutsideWall($width, $height);
$this->bottomOutsideWall($width, $height);
}
abstract public function wall($rows, $columns, $wallType = 'left side');
abstract public function sidewall($rows, $columns);
abstract public function door($rows, $columns, $doorType = 'left entry');
abstract public function blank($rows, $columns);
abstract public function label($rows, $columns, $label);
abstract public function topOutsideWall($width, $height);
abstract public function leftOutsideWall($width, $height);
abstract public function rightOutsideWall($width, $height);
abstract public function bottomOutsideWall($width, $height);
protected function items($rows, $columns, $item)
{
// put the item where it needs to go inside the house
}
protected function assertInBounds($row, $column)
{
// make sure the requested row/column is inside of the house
}
}
Carpenter正在扮演builder的角色。在这个应用中,有两种类型的Carpenters,它们的行为不同,即Novice和Expert。这些木匠会用导演给的同一套指令(你的Architect)造出不同的房子。如果你需要的话,在你的应用中添加更多的构建器(例如DrunkenCarpenter)会非常容易。再来看NoviceCarpenter。
app/NoviceCarpenter.php
namespace App;
{
public function wall($rows, $columns, $wallType = 'left side')
{
$this->items($rows, $columns, $this->wallChar($wallType));
}
public function sidewall($rows, $columns)
{
$this->items($rows, $columns, '--');
}
public function door($rows, $columns, $doorType = 'left entry')
{
$this->items($rows, $columns, $this->doorChar($doorType));
}
public function blank($rows, $columns)
{
$this->items($rows, $columns, ' ');
}
public function label($rows, $columns, $label)
{
$this->items($rows, $columns, $label);
}
public function topOutsideWall($width, $height)
{
$this->items(0, range(0, $width - 1), '--');
}
public function leftOutsideWall($width, $height)
{
$this->items(range(1, $height - 1), 0, '| ');
}
public function rightOutsideWall($width, $height)
{
$this->items(range(1, $height - 1), $width - 1, ' |');
}
public function bottomOutsideWall($width, $height)
{
$this->items($height - 1, range(0, $width - 1), '--');
$this->items($height - 1, 0, '|-');
}
protected function wallChar($wallType)
{
// returns the correct wall character for this type
}
protected function doorChar($doorType)
{
// returns the correct door character for this type
}
}
NoviceCarpenter实现了您的抽象方法,并使用了特定类型的材料,即|和–字符。ExpertCarpenter用更强壮的=和)角色建造东西,因为他更有经验。
最后,如果你运行你的模拟器,你会得到一些漂亮的我的旧家的布局的 ASCII 艺术。
app/simulator.php
require __DIR__ . '/../vendor/autoload.php';
$director = new App\Architect
$builder1 = new App\Architect
$builder2 = new App\Architect
$director->createMyOldHouse($builder1);
$director->createMyOldHouse($builder2);
print '-- Novice Carpenter --' . PHP_EOL;
print $builder1->getHouse();
print PHP_EOL . '-- Expert Carpenter --' . PHP_EOL;
print $builder2->getHouse();
$ php app/simulator.php
-- Novice Carpenter --
------------------------------ -------------------
| Ba | \ |
| |--------| |
| MB | | |
| | Ba | |
| ----- | ------ | LR -- -- |
| --| | | | |
| --\ --- -- -----| | | |
| \ | | /| | | | K | |
| | | ----\ ---- | | | |
| Br | Br | | | | | -- | |
| | | \ | | | | / U| |
|------------------- |-\------------ | | ----| --
-- Expert Carpenter --
============================== ==================
= Ba ) \ =
= ) ________) =
= MB ) ) =
= ) Ba ) =
= ____ ) ______ ) LR __ __ =
= __) ) ) ) =
= __\___ __ ______ ) ) ) =
= (\ ) /) ) ) ) K ) =
= ) ) ____\ ____) ) ) =
= Br ) Br ) ) ) ) ) ) __) =
= ) ) \ ) ) ) ) / U) =
======================\ ============) ) ====)==
结论
这种模式对于可以为同一组指令创建不同的复杂对象的情况很有用。给猫剥皮的方法有很多,尽管我很想在这里放一张猫的照片,但我认为这对我的 Photoshop 技能来说太难了(见图 4-3 )。思考一下如何将算法与实际的猫剥皮部分分开。您可以使用构建器模式,最终得到不同的指令集(指导器)和不同的 cat 皮肤方法(构建器)。
图 4-3。
Cat skinner blues
这种模式的一个缺点是,您的 director 与您的抽象构建器耦合在一起。如果建造者改变了,那么你的导演也必须改变。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 110 页
五、工厂方法
$> git checkout factory_method
目的
定义一个创建对象的接口,但是让子类决定实例化哪个类。工厂方法让类将实例化推迟到子类。 1
应用
当你在创造某种事物的变体时,你可以将这些变体分解成不同的产品类别。然而,这些类可能很难构造,所以您为每个产品创建附带的工厂。工厂可以用来替换或重构类构造函数,这样产品类构造函数中就不存在逻辑了。这种模式不同于抽象工厂,因为您不是在创建产品系列。事实上,抽象工厂可以由许多不同的工厂方法组成。
抽象结构
-
抽象类,作为所有具体创建者的接口。这将可能包含所有/大多数具体创作者使用的共享功能。如果工厂对每种类型的产品都没有区别,那么这可以成为一个具体的创造者本身,而不再是抽象的。见图 5-1 。
图 5-1。
The factory method uses a concrete subclass to create a concrete product.
-
ConcreteCreator:当一个具体产品的创建有不同的创建逻辑时,那么你就用一个专门针对ConcreteProduct.的创建者来覆盖Creator基类 -
Product:所有ConcreteProducts使用的抽象类或接口。 -
ConcreteProduct:这是Product的变种。它包含特定于其变体的逻辑。这个物体是由一个ConcreteCreator创造的。
例子
你将种植植物。花园的类型产生不同类型的植物。如果你的工厂是一个大麻园,那么它创造了大麻植物。你的花园工厂生产的产品是植物,但具体的植物类型是大麻。一个菜园可能生产玉米、南瓜和土豆作为产品。菜园是工厂接口的另一种具体类型。玉米、南瓜和土豆是你的产品界面的具体类型。希望这有意义。
示例结构
图 5-2 为结构示意图。
图 5-2。
Please don’t send the FBI to my house.
履行
这是你的模拟器,可以让你的花园成长。一旦一个花园被种植,你可以迭代返回的植物并消费它们。请注意,您可以用不同的花园工厂替换掉大麻花园,模拟器的其余部分可以不加修改地运行。现在,你会坚持吸毒。
app/simulator.php
$garden = new App\MarijuanaGarden
$plants = $garden->grow();
foreach ($plants as $plant) {
$plant->consume();
}
如果你想知道大麻花园是什么样子,我不会在这里给你看一张照片。不如去上课吧?
app/MarijuanaGarden.php
namespace App;
{
public function harvest()
{
return [new MarijuanaPlant, new MarijuanaPlant]
}
}
注意你正在扩展Garden类。这可能是一个接口,但你将从抽象 garden 类继承一些基本功能,如下所示。植物也会死,就像人一样。这真的很令人难过,但它总是发生。在你的花园里,总有一株植物死去。请不要问我为什么。
app/Garden.php
namespace App;
{
abstract public function harvest();
public function grow()
{
$items = $this->harvest();
// one plant died, oh noes!!!
$died = array_shift($items);
return $items;
}
}
接下来,你应该看看大麻植物长什么样。同样,这里没有你的照片;无法让该死的摄像机工作。
app/MarijuanaPlant.php
namespace App;
{
public function consume()
{
print "you now have a strong hunger for a bag of Bugles" . PHP_EOL;
}
}
结论
工厂方法模式使得以后引入其他类型的植物变得容易。植物的不同变化和组合很容易用你的工厂来建造。主要的要点是抽象出一个类的困难的构造过程。
您可能想知道工厂方法模式与抽象工厂模式有何不同。抽象工厂用于创建产品系列(有时非常不同),工厂方法实际上关注的是创建单一的不同产品。抽象工厂经常使用工厂方法。
这种模式的一个缺点是,有时对于您想要做的事情来说,它可能是多余的。工厂方法的一个更简化/淡化的版本叫做简单工厂,我将在后面的章节中讨论它。工厂非常有用,尤其是在与领域驱动设计结合使用时。在重逻辑的应用中,工厂将是你的盟友。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 121 页
六、原型
$> git checkout prototype
目的
使用原型实例指定要创建的对象种类,并通过复制该原型来创建新对象。 1
应用
当您想要派生和修改现有对象时,请使用原型模式。这种模式的一个很好的用途是,当您希望避免构建一个需要花费大量时间或者创建起来很复杂的类时。创建成本高的对象的一个例子是使用 web 服务获取数据的对象。但是,一旦有了数据,就不再需要从 web 服务获取数据;你只需克隆数据。还有另一种称为代理的模式,它也是这里描述的 web 服务示例的一个很好的候选。很快您就会看到,您也可以使用这种模式来克隆复杂的对象。
抽象结构
图 6-1 为结构示意图。
-
Client:使用Prototype类。这可以是一个类或脚本本身。在这个例子中,您甚至不用担心客户端。 -
这是一个抽象类,其他类可以扩展。但是,如果只有一个
ConcretePrototype,它不一定是抽象的。clone方法用于复制类的内部结构,这样你就可以创建一个具有相同内部结构的新对象。 -
ConcretePrototype(1/2):这些类从Prototype类扩展而来,可以为原型的每个变体提供额外的方法。如果原型没有变化,你可以把它合并成Prototype。
图 6-1。
Clone, clone, everywhere a clone
例子
1996 年 7 月 5 日,世界永远地改变了,那一天,第一只由成年体细胞克隆而成的哺乳动物绵羊多利诞生了。新生的多莉之所以被命名为多莉,是因为她是用乳腺克隆的,而歌手多莉·帕顿在这方面尤其臭名昭著。多莉死于攻击其储存系统的逆转录病毒。肺部问题实际上在克隆体中很常见。这就是为什么你要在模拟器中跟踪每只羊的肺部。
示例结构
注意在图 6-2 中,你没有在这个例子中创建绵羊的变体;所以ConcretePrototype1变成了Prototype。
图 6-2。
Example prototype structure
履行
您将使用 PHP 的本机内置克隆机制来应用原型模式。这不是实现原型模式的必要条件,因为您可以创建自己的克隆方法;然而,以我的非专业观点来看,了解当地人要容易得多,也更酷。
假装你有羊…
app/Sheep.php
namespace App;
{
public $name = "Big Momma";
}
现在你已经证实了羊的存在,我感到非常兴奋,因为我有一些非常糟糕的关于羊的笑话要告诉你。 2
-
你怎么称呼一只裹着巧克力的羊?糖果咩。
-
如果你把一只愤怒的绵羊和一头喜怒无常的母牛杂交,你会得到什么?一只哞哞叫的动物。
-
墨西哥的羊怎么说圣诞快乐?抓绒纳维达!
-
织一件毛衣需要多少只羊?别傻了。羊不会织!
-
绵羊在哪里剪羊毛?在咩咩商店!
好吧,我希望我们喜欢这些笑话。现在我已经从我的系统中获得了这些,下一步是构建一个模拟器,为您创建和管理绵羊。
src/模拟器
$sheep = new App\Sheep;
$dolly = $sheep;
$dolly->name = "Dolly Parton";
var_dump($sheep, $dolly);
你的模拟器应该会给你吐出$sheep和$dolly的名字。你知道Dolly Parton是$dolly;的名字,但是,在这种情况下,你认为出发$sheep的名字是什么?
模拟器输出
class Sheep
#2 (2) {
public $name =>
string(5) "Dolly Parton"
}
class Sheep
#2 (2) {
public $name =>
string(5) "Dolly Parton"
}
哦哦。看起来$sheep->name不再是Big Momma.了,如果你习惯于面向对象编程,并且知道内存指针是如何工作的,那么这可能对你来说并不奇怪。不过,在本例中,您不需要担心 sheep 对象中的数据是 baaaaaaa-ad。你可能已经注意到这两个对象都指向绵羊#2。这告诉你$sheep和$dolly都指向内存中完全相同的地址。在 PHP 中,当一个对象被设置为等于另一个对象时,那么两个对象都引用内存中的同一个地址空间。见图 6-3 。
图 6-3。
We are the same object, boss!
如果你想让它们有不同的内存地址,你应该克隆绵羊,这正是你要做的。
src/模拟器
$dolly = clone $sheep;
这利用了另一个内存槽,并将$name变量复制到新的内存地址。现在当你更新$dolly的名字时,它不会影响$sheep的$name,因为它在内存中使用了一个完全不同的地址。见图 6-4 。
图 6-4。
Cloning at its finest
如果您再次运行模拟器,您可以看到输出正是您想要的。
模拟器输出
class Sheep
#2 (2) {
public $name =>
string(5) "Big Momma"
}
class Sheep
#4 (2) {
public $name =>
string(5) "Dolly Parton"
}
暂时一切都好;然而,这里您还没有真正实现原型模式。下一件要做的事情是修改 sheep 类,至少添加一个复合类。
app/simulator.php
$sheep = new App\Sheep(new App\Lungs);
$dolly = clone $sheep;
$dolly->name = "Dolly Parton";
$dolly->applyVirus('JaagsiekteVirus');
var_dump($sheep, $dolly);
在你运行你的模拟器之前,你应该给 sheep Lungs(一个新的类),并且给Sheep类添加applyVirus方法。这种方法会损害羊的肺部。就像真人快打角色的健康指示器一样,肺也有一个健康指示器。健康范围从 0 到 100 %;当一只新绵羊出生时,肺部是 100%健康的。使用JaagsiekteVirus后,肺部健康达到 20%。
app/Sheep.php
namespace App;
{
public function __construct(Lungs $lungs)
{
$this->name = "Big Momma";
$this->lungs = $lungs;
}
public function appyVirus($virusType)
{
$this->lungs->health(20);
}
}
现在,当您运行模拟器时,您会得到以下输出:
模拟器输出
class Sheep
#2 (2) {
public $name =>
string(9) "Big Momma"
public $lungs =>
class Lungs
#4 (1) {
protected $health =>
int(20)
}
}
class Sheep
#3 (2) {
public $name =>
string(12) "Dolly Parton"
public $lungs =>
class Lungs
#4 (1) {
protected $health =>
int(20)
}
}
哦,又来了。你克隆了绵羊,但克隆的只是原始内部变量的肤浅拷贝。这意味着在您的模拟中,两只不同名称的绵羊共享同一套肺。这是不应该发生的,所以你需要强制进行深度克隆,并在你的羊被克隆的任何时候创造不同的肺。要解决这个问题,您可以使用神奇的clone方法。
app/Sheep.php
public function __clone()
{
$this->lungs = clone $this->lungs;
}
现在,只要一只羊被克隆,它们的肺也会被克隆。这使得羊和肺对象不能共享内存地址。克隆的缺点是使用了更多的内存地址空间,所以除非确实需要,否则不要克隆对象。再次运行模拟器会显示正确的输出。
模拟器输出
class Sheep
#2 (2) {
public $name =>
string(9) "Big Momma"
public $lungs =>
class Lungs
#4 (1) {
protected $health =>
int(100)
}
}
class Sheep
#3 (2) {
public $name =>
string(12) "Dolly Parton"
public $lungs =>
class Lungs
#5 (1) {
protected $health =>
int(20)
}
}
这就完成了原型模式。该模式的重点是将任何对象引用复制到它们的外部内存地址空间,这样您就可以彼此独立地使用这些对象。在您的示例中,Sheep并不复杂,但是您可以添加许多变量来测量模拟器中动物的稳定性和健康状况。
这里为什么不用简单的工厂呢?这个原型模式看起来需要做很多额外的工作,不是吗?你可以用一个简单的工厂来创造第一只原羊。然而,如果你通过应用一些病毒对绵羊进行内部改造,你会得到一只看起来不同的绵羊。如果你想开始克隆那只经过改造的羊呢?这就是原型模式优于简单工厂的地方。
app/simulator.php
$sickSheep1 = clone $sheep;
$sickSheep1->applyVirus();
$sickSheep2 = clone $sickSheep1;
$sickSheep1->applyMedicine('Medicine 1');
$sickSheep2->applyMedicine('Medicine 1');
// compare the health of two sick sheep...
结论
在本例中,您使用克隆技术轻松地复制了绵羊。在这个例子中,Sheep对象非常简单,但是在现实生活的模拟器中,Sheep对象可能有许多变量和数据与之相关联。创造新的绵羊不应该是乏味的,你实际上很少关心这一部分。你更关心改造一只羊,看看它抵抗病毒的能力有多强。你当然不希望 new sheep 的构造方面压倒了你模拟器中的逻辑。
原型模式的一个缺点是您很容易违反单一责任原则。一个已经存在的类有一个责任,克隆只是给那个类增加了另一个责任。这是次要的,考虑到你现在可以非常容易地克隆对象。
这里没有看到的另一个缺点是,当使用原型模式时,您可能会以不同的方法结束不同的ConcretePrototypes。这使得管理新克隆的对象变得困难。例如,想象一下,如果你有两只具体类型的羊,分别叫做WoolySheep和MilkingSheep。它们有不同的方法:MilkingSheep有一个方法叫gotMilk(),WoolySheep有一个方法叫gotWool()。现在客户必须知道你在和哪种类型的羊打交道。如果客户端没有跟上,那么方法gotMilk()可能会在WoolySheep上被调用,这将抛出一个错误。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 133 页
2
http://jokes4us.com/animaljokes/sheepjokes.html
七、单例
$> git checkout singleton
目的
确保一个类只有一个实例,并提供对它的全局访问点。 1
应用
简而言之:没有。老实说,我还没有发现 PHP 中单例模式的任何实际应用。最初的单例设计模式是为了确保一个类只能创建一个实例。它被用在后台打印程序的上下文中,在那里您只需要创建一个实例。开箱即用,PHP 是一个单线程环境,通常在 apache/nginx 线程中的生命周期很短。即使你使用了类似于React PHP2的东西,你也不能使用阻塞操作(sleep, file_get_contents, global),所以在你的应用中,你不需要担心限制对某个类的单个实例的全局访问,因为即使在 React 中也会导致死锁。
因此,你将会学到一个变体,我称之为简化的单例。这些是您在 Laravel 服务容器中注册的单例,或者如果您没有使用 Laravel,您可以在某个 IoC 容器中注册它们。然而,如果你对如何实现单例模式感兴趣,让我们先看一下,然后通过展示用于单例模式的 Laravel 服务容器来结束这一章。
抽象结构
图 7-1 为结构示意图。
Singleton:注意对构造函数的protected final限制。构造函数只能从类本身内部调用。如果你还不存在,怎么可能调用一个方法来创造你自己?这是一个鸡和蛋的问题。不过,在这种情况下,通过使用静态方法instance(),您可以创建一个新的 singleton 实例,它存储在protected $instance变量中。
图 7-1。
The singleton pattern
例子
在本例中,您将制作一个计数器,它会在每次发出请求时递增。您将尝试两种方式:使用单例模式和简单单例模式。作为额外的奖励,您将创建具有 PHP 特性的 singleton 模式,称为 trait 3 。为什么呢?因为你可以。
履行
首先:您需要创建一个对请求进行计数的类。
app/RequestCounter.php
namespace App;
class RequestCounter
{
private $numberOfRequestsMade = 0;
public function numberOfRequestsMade()
{
return $this->numberOfRequestsMade;
}
public function makeRequest()
{
$this->numberOfRequestsMade++;
}
}
接下来,我们来谈谈特质。什么是特质?如果你已经知道这一点,你可以跳过这一部分。当我第一次了解特质时,我非常努力,到处都在使用它们,不管是左还是右。我很疯狂。我花了几个星期才意识到我在滥用特质。在 PHP 中,我们使用特征作为混合功能的方式。在 PHP 中,我们不能扩展多个类(也称为多重继承)。特质是绕过这种限制的一种方式。然而,没有多重继承并不是一件坏事。如果不增加从多个类继承的能力,常规的经典继承已经足够困难了。有时我们有一小部分不属于任何特定类的功能,但是我们想添加到许多不同的类中;特征在这方面非常有用。不要像我一样对特质着迷。不要沉醉于特质力量,否则几周后你会发现自己患有严重的特质宿醉头痛。
您将创建一个名为SingletonPattern的特征,它可以被添加到任何类中,以便将它变成单例。我之前说过,你可能永远不会使用单例模式,所以你可以把它作为一个练习,看看特征的酷因素,并学习一个新的模式。
app/SingletonPattern.php
namespace App;
trait SingletonPattern
{
static protected $instance;
final protected function __construct()
{
// no one but ourselves can create ourselves
}
static public function instance()
{
if (! static::$instance) {
static::$instance = new static;
}
return static::$instance;
}
}
现在让我们利用这个特性。您将创建一个新类,它从RequestCounter扩展而来,并使用您的SingletonPattern特征。
app/requestcountersingleton . PHP
namespace App;
class RequestCounterSingleton extends RequestCounter
{
use SingletonPattern;
}
在这里等一下。为什么要创建这样一个新的类?原因很简单,而且是双重的。
-
您希望能够对您的
RequestCounter类进行单元测试。测试一个单体比一个普通的 ol' PHP 类更困难。 -
稍后您将在 Laravel 服务容器中使用
RequestCounter。服务容器会为你处理单例的东西,在这种情况下,你不需要这个SingletonPattern特征。
至此,您已经准备好使用您的 singleton 了。同样,这只是一个如何通过 traits 实现单例模式的例子。这样做实际上没有任何可行性;这纯粹是一种学习特质的教育尝试。
app/simulator.php
App\RequestCounterSingleton::instance()->makeRequest();
App\RequestCounterSingleton::instance()->makeRequest();
App\RequestCounterSingleton::instance()->makeRequest();
// Singleton request hits: 3
print 'Singleton request hits: ' . RequestCounterSingleton::instance\
()->numberOfRequestsMade() . PHP_EOL;
现在让我们看看如何使用 Laravel 服务容器实现一个简单的 singleton。
app/simulator.php
app()->instance('request.counter', new App\RequestCounter);
app()->make('request.counter')->makeRequest();
app()->make('request.counter')->makeRequest();
app()->make('request.counter')->makeRequest();
app()->make('request.counter')->makeRequest();
app()->make('request.counter')->makeRequest();
// Simple singleton request hits: 5
print 'Simple singleton request hits: ' . app('request.counter')
->numberOfRequestsMade() . PHP_EOL;
每次调用app()->make(),都是重用同一个RequestCounter类。它这样做只是因为您使用了app()->instance(),这使得 Laravel 将'request.counter'作为单例处理。您也可以使用服务容器,这样每次调用app()->make()时,它都会创建一个新的RequestCounter。如果你不想让'request.counter'成为单例,你可以创建一个绑定而不是一个实例,如下面的代码所示。
绑定示例(非单例)
app()->bind('request.counter', function ($app) {
return new RequestCounter;
});
在这一点上,你可能想知道为什么你要为一个单例使用服务容器而不是仅仅使用一个全局变量。事实上,服务容器是在bootstrap/app.php的第 14 行创建的一个全局变量。那么为什么不使用全局变量呢?为什么要做额外的工作?
引导程序. app
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
答案是,通过使用服务容器,您已经将所有客户端从RequestCounter类中分离出来。您可以在任何时候用具有相同接口的其他类替换掉RequestCounter,理论上您的应用的其余部分将继续运行。你可以通过替换掉app()->instance('request.counter', new SomeOtherRequestCounter).来做到这一点,这是你的框架库中一个非常灵活和强大的东西。
结论
您了解了单例模式和修改后的简单单例模式。您在 Laravel 和 traits 中介绍了服务容器。虽然您可能不会使用 singleton 模式,但您可能会在某个时候发现自己出于各种原因在 Laravel 服务容器中使用 singleton,包括性能增强或跨系统共享数据。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 144 页
2
3
http://php.net/manual/en/language.oop5.traits.php
八、简单工厂方法
$> git checkout simple_factory_method
简单工厂不是你在最初的 90 年代四人组设计模式书中找到的设计模式。然而,这是一种非常有用的创建对象的方法——非常有用,以至于我专门为它写了一章。
目的
简单工厂方法简化了创建新的具体对象的过程。
应用
使用一个简单的工厂可以使代码更干净,更容易处理。当您想要创建一个具有依赖关系的对象时,可以应用这种模式。这种模式可以用来重构代码中创建产品的地方。有人可能会说这就是我们之前讨论过的工厂方法模式。在某种程度上,这可能是真的。不同之处在于,我们不会为多种产品创建多个工厂。我们只创建一个可以生产各种产品的工厂。
抽象结构
-
Factory:这个类为你创建了Product(见图 8-1 )。有时由于依赖关系,一个产品需要几行代码来创建;如果您在整个应用中都这样做,这就为 bug 潜入您的代码打开了大门。这也是一种将Product的创建与所有使用它的客户端分离的方法。你可以稍后在一个地方替换掉Product:??。图 8-1。
Factory creates a shiny new product for you
-
Product:这个类可以由几个依赖项组成:Subclass1、Subclass2、Subclass3等…
示例场景
您将重构下面的代码以使用一个简单的工厂。
一些人为的例子
$bar = new Bar('test', 123);
$baz = new Baz;
$foo = new Foo($bar, $baz);
履行
如果你发现自己一遍又一遍地做着同样的代码,那么把它抽象成一个工厂会使事情变得更容易。
另一个人为的例子
$foo = Foo::factory();
// or perhaps using Laravel's service container
$foo = app()->make('foo');
让我们看看如何用简单的 PHP 创建这个工厂方法。然后,您将把它工作到 Laravel 服务容器中。
app/Foo.php
namespace App;
class Foo
{
public function __construct(Bar $bar, Baz $baz)
{
$this->bar = $bar;
$this->baz = $baz;
}
static public function factory(Bar $bar = null, Baz $baz = null)
{
$bar = $bar ?: new Bar('test', 123);
$baz = $baz ?: new Baz;
return new Foo($bar, $baz);
}
}
这里发生了什么事?基本上,您已经将创建一个新的Foo对象的代码移动到这个工厂方法中。现在,如果您选择这样做,您可以覆盖依赖项,但是依赖项是默认硬连接的。
在工匠作坊里修修补补
> Foo::factory()
// object(Foo)(
// 'bar' => object(Bar)(
// 'var1' => 'test',
// 'var2' => 123
// ),
// 'baz' => object(Baz)(
//
// )
// )
也许你不喜欢在你的Foo课堂上无所事事的factory方法。这是一个好迹象,表明您应该在其他地方重构代码。让我们专门为此创建一个Factory类。
app/Factory.php
namespace App;
class Factory
{
static public function foo(Bar $bar = null, Baz $baz = null)
{
$bar = $bar ?: new Bar('test', 123);
$baz = $baz ?: new Baz;
return new Foo($bar, $baz);
}
}
用App\Factory::foo();运行 tinker 将产生与之前类似的结果。
您可能会注意到您一直在使用静态方法。许多工厂方法是静态的,因为这样调用它们更容易。你不会想先创建一个新工厂,然后调用foo方法。如果一步就能完成,为什么要分两步做呢?测试在这里也不是问题。你不会嘲笑或单元测试这个工厂方法;那样做你不会有太大收获。您想要测试的真正代码在工厂正在创建的类内部。
结论
简单的工厂方法清理了类,并将具有多个依赖关系的复杂对象的创建放在一个地方。
值得注意的是,Laravel 服务容器足够智能,可以自动为您注入依赖项,所以尽可能利用这一点。
服务容器自动依赖解析
class First
{
}
class Second
{
protected $first;
public function __construct(First $first)
{
$this->first = $first;
}
}
class Third
{
protected $second;
public function __construct(Second $second)
{
$this->second = $second;
}
}
$third = app()->make('Third'); // this works!
在这个例子中,Laravel 服务容器会自动将一个Second对象注入到Third对象中,同样,它会将一个First对象解析为Second对象。这允许您创建干净且可测试的代码,因为您的所有依赖项都被注入到构造函数中。当你测试Third类时,如果你愿意,你可以注入一个模拟的Second类,这使得测试变得更加容易,因为你可以专注于Third的公共方法(它的接口)。
此时,您应该知道简单工厂模式和工厂方法模式之间的区别。不过,为了确保万无一失,我还是要再说一遍:工厂方法模式使用子类来创建产品类的不同变体。简单工厂模式是一个简单的助手函数,它取代了关键字new,并清理了具有多个依赖关系的更复杂对象的创建。
九、适配器
$> git checkout adapter
目的
将一个类的接口转换成客户端期望的另一个接口。适配器允许类一起工作,否则由于不兼容的接口而无法工作。 1
应用
有时你想使用现有的代码,但接口并不符合你的需要。例如,您希望利用供应商/外部代码,而无需重写所有现有代码。这类似于在圆孔中安装一个方栓。只要钉子足够小,方形的钉子可以放入任何圆孔中。你应该注意去适应什么是需要的,因为你添加到你的适配器接口的方法越多,你的方钉在你的圆孔里就变得越大。
你可能已经注意到我在最后一段强调了存在。这是因为我想强调适配器模式的主要目的/意图:处理现有代码。想象一下,把欧洲所有的电源插座从 220 伏换成美国的 110 伏标准是多么的不可能。当然,如果你是从零开始给房子布线,你可能会用 110 伏的电压,但我们不是从零开始。欧洲有数百万套公寓、酒店和住宅,多得无法重构。这是适配器的亮点。使用适配器可以让我们将现有的(和经过验证的)系统保持在适当的位置。我们不改变系统;我们只需要担心适配器,这要容易得多,尤其是当两个系统相互兼容的时候。
您已经知道了适配器模式,因为您在现实生活中使用过适配器。你的智能手机插入一个 USB 适配器,该适配器插入一个 110 伏的墙上插座。您的电脑显示器插入 HDMI-to-DVI 适配器或 Thunderbolt for Mac 用户。电源适配器将汽车的点烟器转换成可以给手机充电的东西。
重申一下,在开始新代码时,您可能不会使用适配器模式。它的真正好处来自于修改已经确定的现有代码。好了,说说打聋马(图 9-1 )。我现在就不说什么时候使用适配器模式了。
图 9-1。
Mr. Ed would be proud to call you his son… HORSE PRIDE!
抽象结构
-
Client:这些是期望一个Target类的类(图 9-2 )。因为您在这里处理的是一个现有的系统,Client实际上可能不仅仅是一个类。图 9-2。
Adapter pattern
-
Target:这是客户端希望看到的界面。理想情况下,这是一个抽象类或接口。但是,如果它是一个常规类,那么Adapter仍然可以扩展这个类并覆盖所有的公共方法。 -
Adapter:这个类将扩展或实现Target。它的方法与Target的方法相匹配,通常是Adaptee方法的包装。例如,在图 9-2 ,Adapter::someMethod()调用Adaptee::differentMethod. -
这是你试图用一个
Adapter包装的类。这个类通常是您希望引入到现有应用中的供应商、包或遗留代码。也可能是你写的代码,但是你害怕接触它,因为它很旧,没有单元测试,但是已经被证明是有效的,因为它正在应用中使用。无论如何,我们的目标是把这个Adaptee代码和
示例场景
你有现有的系统,邮寄信件到一个地址。这个系统已经被证明是有用的,现在楼上的头面人物想把它集成到你公司的客户关系管理(CRM)数据库中,这个数据库里有很多很多的客户地址。您的工作是添加从公司 CRM 数据库向客户发送邮件的功能。
示例结构
图 9-3 为结构示意图。
图 9-3。
Mail system using the adapter pattern
履行
你可以跑进去,拿着枪,尝试重构你的MailClient或者甚至创建一个新的MailClient,但是MailClient只是这个难题的一小部分,让我们假设(虽然这个例子没有显示出来)许多事情都依赖于MailClient来工作,所以创建一个新的需要一段时间来构建。此外,您不希望接触 CRM 代码,在本例中,它应该被视为非常类似于您从软件包中获得的供应商代码。
让我们看看 CRM 代码,它已经由另一个团队为您提供了。
app/CRM/Address.php
<?php namespace CRM;
class Address
{
private $primaryAddress, $secondaryAddress, $city, $state, $zipCode;
public function __construct($primaryAddress, $secondaryAddress, $ci\ ty, $state, $zipCode)
{
$this->primaryAddress = $primaryAddress;
$this->secondaryAddress = $secondaryAddress;
$this->city = $city;
$this->state = $state;
$this->zipCode = $zipCode;
}
public function getFullAddress()
{
return $this->primaryAddress . PHP_EOL
. ($this->secondaryAddress ? $this->secondaryAddress . PHP_'')
. $this->city . ', ' . $this->state . ' '
. $this->zipCode . PHP_EOL;
}
public function getPrimaryAddress()
{
return $this->primaryAddress;
}
public function getSecondaryAddress()
{
return $this->secondaryAddress;
}
public function getCity()
{
return $this->city;
}
public function getState()
{
return $this->state;
}
public function getZipCode()
{
return $this->zipCode;
}
public function setPrimaryAddress($primaryAddress)
{
$this->primaryAddress = $primaryAddress;
}
public function setSecondaryAddress($secondaryAddress)
{
$this->secondaryAddress = $secondaryAddress;
}
public function setCity($city)
{
$this->city = $city;
}
public function setState($state)
{
$this->state = $state;
}
public function setZipCode($zipCode)
{
$this->zipCode = $zipCode;
}
}
这是一个非常简单的类。我把它说得很简单,但实际上你可能会有一些奇怪的代码被塞进一个巨大的类中,这个类有一些名为doStuff的超级大方法,没有注释。您甚至可能有多个需要适应的类。我们将在后面的Facade章节中讨论这个问题,但是现在让我们只关注这里的一个简单的Address。接下来,让我们看看你的Address类,并将其与CRMAddress进行比较。
app/Address.php
namespace App;
interface Address
{
public function to();
public function address1();
public function address2();
public function city();
public function region();
public function postalCode();
public function __toString();
}
所以你可能注意到的第一件事是你甚至没有一个Address类;它是一个接口。公共方法(接口)肯定和CRM\Address里面的方法不匹配。我可以给你看另一个类,它实际上实现了。然而,这无关紧要,因为您只打算使用这个接口。同样,您的适配器将实现Address接口。
app/CRMAddressAdapter.php
namespace App;
class CRMAddressAdapter implements Address
{
protected $to, $Address;
public function __construct($name, App\CRM\Address)
{
$this->address = $address;
$this->to = $name;
}
public function to()
{
return $this->to;
}
public function address1()
{
return $this->address->getPrimaryAddress();
}
public function address2()
{
return $this->address->getSecondaryAddress();
}
public function city()
{
return $this->address->getCity();
}
public function region()
{
return $this->address->getState();
}
public function postalCode()
{
return $this->address->getZipCode();
}
public function __toString()
{
return $this->to . PHP_EOL . $this->address->getFullAddress();
}
}
下面是模拟器代码,它将所有这些拼图拼在了一起。这不是你在上面的 UML 模式中看到的Client类;这里的Client其实就是MailClient。模拟器只是运行所有不同的代码。请注意MailClient是如何依赖Address的。
app/simulator.php
$crmAddress = with(new App\CRM\AddressLookup)->findByTelephone('555 867-\
5309');
$address = new App\CRMAddressAdapter('Jenny Call', $crmAddress);
$mailClient = new App\MailClient;
$mailClient->sendLetter($address, 'Hello there, this is the body of \
the letter');
我在这里不介绍MailClient,但基本上它向一个Address发送消息。我没有展示MailClient类,因为它确实与适配器模式没有任何关系。你可以在这个模拟器中看到,你将你的CRMAddress改编成了Address,并将其传递给了MailClient。如果您在这个例子中从头开始编写MailClient,那么跳过适配器模式,只编写依赖于CRMAddress的MailClient会更有意义。希望您现在理解了如何使用适配器模式。
结论
适配器模式也称为包装器模式,因为它将现有的接口包装在客户端期望的接口内。当您没有现有代码时,您可能会发现适配器模式的用途,但最有可能的是,这种模式将用于现有代码的情况。
一个缺点是,两个类可能真的很难适应这么多的方法。这可能会导致适配器部分损坏,如果客户端希望使用目标接口公开的所有方法,那么当客户端调用与适配器完全不兼容的方法时,这可能会导致问题。尽管目标和适配者有不兼容的接口,但这两者很可能是相关的。事实上,如果客户的适配器和目标没有任何共同之处,为什么还要为它们进行调整呢?
使用适配器模式,通过将Ambulance类包装在垃圾车适配器类中,可以使救护车看起来像垃圾车(图 9-4 )。这是有用的还是最好的方法取决于具体情况。救护车是用来在紧急情况下把人送到医院的。垃圾车被用来运送垃圾到荒地。他们有完全不同的目的和目标。然而,适配器不必是完美的;他们只需要在客户期望的目标接口上实现所有公共方法调用。因此,如果在这种情况下,你的Garbage卡车等级的唯一方法是pickupTrash,那么尽管救护车将成为一辆可怕的垃圾车,但没有什么可以阻止你将垃圾倾倒在这辆医疗车内,并将其运往垃圾填埋场。
图 9-4。
Adapter pattern
将两个不相关的类用于不同的目的可能会很困难,但这并不是不可能的。当简单的重构可以工作,或者当您可以创建与解决您的问题更相关的新类时,请谨慎创建适配器。例如,在您的场景示例中,如果MailClient和Address只在整个应用中的一两个地方使用,那么重写一个新的MailClient来使用CRMAddress并丢弃Address会更容易。长话短说,编写适配器来避免大规模重构。
有些人混淆了适配器模式和策略模式。我还没有谈到策略模式,但是总的来说,这两种模式都使用了复合。事实上,很多模式都使用组合,因为当一个变化,比如一个新的特性请求,稍后出现时,组合更灵活,更容易处理。我在本书的前几章谈到了组合,使用组合比使用继承更好。策略模式是关于使用组合来切换算法,而适配器模式使用组合来变形/修改现有的接口。与适配器模式相混淆的还有桥接模式。我将在下一章介绍桥的模式。这两种模式看起来相似,但意图不同。一些模式在代码方面看起来非常相似,但是意图是不同的。我将在下一章的结尾介绍这些不同之处。
Footnotes 1
设计模式:可重用面向对象软件的元素,第 157 页