精通-PHP-设计模式(全)
精通 PHP 设计模式(全)
原文:
zh.annas-archive.org/md5/40e204436ec0fe9f5a036c3d1b49caeb
译者:飞龙
前言
你有没有去过 PHP 会议?如果没有,我强烈推荐你去,这是你能接触到活生生的 PHP 社区的最近距离。几周前,我从伦敦飞到密苏里州圣路易斯市,参加了 php[tek](由 php[architect]主办的 PHP 会议)。会议结束后,PHP 社区内有一个小传统,被称为 WurstCon。基本上,数百名 PHP 会议与会者挤进一个小热狗店,举办一个热狗大会,往往让店员完全惊讶。同样,PHP 活动的社区之夜是你可能遇到的最温暖和最接纳的社区场合;PHP 社区无疑是其他开发语言社区所羡慕的。
从 PHP 7 开始,PHP 项目发生了巨大变化;但我所热爱的东西依然坚固。在任何 PHP 会议上你会感受到的温暖,文档的开放性,以及语言的采纳。是的,PHP 本身存在着无疑糟糕的实践;然而,请想一想 PHP 社区最近取得的成就,从 PHPUnit 到 Composer。在本书中,请记住 PHP 7 的改进,其中我将与你分享一些。项目的轨迹现在肯定是向上的,让我们不要忘记这并不总是真的。PHP 社区从过去中吸取了教训,而语言本身保持了编写糟糕代码的灵活性。
这本书将致力于向你传授强大的软件工程技能,并专注于在 PHP 中实施这些技能。在出版这本书的时候,这种材料有一定的空白和必要性。这本书旨在成为不仅展示软件设计理论,而且还寻求传授实际有价值的实用信息,以改进你编写的代码的质量和可维护性的灯塔。这本书在整个软件开发周期中不遗余力,并将寻求解决大多数软件项目失败的原因,同时解决设计、重设计和保护有效代码的问题。
这本书超越了四人帮所设想的传统设计模式,并详细介绍了热情的 PHP 开发人员在详细的 PHP 项目中成功的必备实践。本书将介绍你理解项目管理技术所需的核心知识,为什么大多数软件开发项目失败,以及为什么你可以让你的项目成功。
最初,我考虑写一本关于 PHP 的书,当时我之前一起工作过的 Mandi Rose 建议我写一本关于我在 PHP 中学到的实践的书。不用说,在那个建议提出的时候,我职业生涯的最好时光无疑还在前方;当实际出现写这样一本书的机会时,我感到自己随着时间的推移学到了更多。你绝对不应该把这本书看作是 PHP 实践的全部;相反,你应该用它来增加你对 PHP 的知识基础,但绝不仅限于此。在这本书中,我希望能够给 PHP 社区一些东西,无论多么微小;阅读完这本书后,我鼓励你投入其中,与他人分享你所学到的东西。
在本书的后面,我将倡导极限编程作为一种方法论,勇气作为这种方法论的关键价值。我会要求你牢记《极限编程的价值观》中对勇气的解释:“我们将如实地讲述进展和估算的真相。我们不会为失败找借口,因为我们计划成功。我们不会害怕任何事情,因为没有人是独自工作的。我们将随时适应变化。”当然,这是一些建议,我们都应该遵循,并真正理解风险,而不是在风险背后畏缩。对于我们许多人来说,我们在职业生涯的某些阶段编写的代码是我们劳动的最高表现。事实上,我们花费的深夜转变为清晨的时间来调试和开发,最终让我们能够展示我们劳动成果。实际上,作为软件工程师,我们编写的代码定义了我们是谁,因此我们应该不断地完善和重构我们的流程,这正是本书的目标所在。我非常荣幸你选择让我帮助你达到这个目标。
这本书涵盖了什么
第一章 为什么“优秀的 PHP 开发人员”不是一个矛盾词,介绍了设计模式的概念,作为常见问题的重复解决方案。
第二章 反模式,介绍了模式如何导致明显的负面后果。
第三章 创建型设计模式,讨论了四人帮设计模式,即围绕对象创建的模式。
第四章 结构型设计模式,涵盖了如何组合多个类和对象以提供更清晰的接口。
第五章 行为设计模式,解释了如何通过识别可以帮助对象之间通信的模式,来增加对象之间通信的灵活性。
第六章 架构模式,围绕解决与 Web 应用/系统架构相关的常见问题,可能超出了代码本身。
第七章 重构,展示了如何重新设计已经编写的代码以提高可维护性。
第八章 如何编写更好的代码,涵盖了一系列在其他地方没有讨论的概念,并最后为开发人员提供了一些建议。
你需要为这本书做好准备
在整本书中,安装 PHP 7 将对你有所帮助。你应该准备根据需要改变你的开发环境;我们将在遇到它们时解决各种工具的安装。
这本书不适合绝望的敌对者或 passively 对接近新软件工程原则持敌对态度的人。它也不适合那些试图成为孤胆英雄的人。在改变给定的代码库时,你必须努力改进整个代码库的代码和所有工作在其中的人。你必须愿意对你编写的代码承担个人责任,而不是责怪外部因素。在共享代码库上,代码可维护性不能单方面改进;你必须编写你的代码,以便为那些在你之后维护它的人维护代码质量。此外,试图以能够分享你所学到的知识的心态来阅读本书,无论是与你的团队、用户组还是更大的 PHP 社区。换句话说,以最终目标为导向来阅读本书;以改进你的代码和你维护的代码库中的人为目标来阅读本书。
这本书是为谁准备的
这本书显然是针对 PHP 开发人员,他们希望了解成为软件工程师所需的完整技能,特别是一些关于软件设计的课程;这本书将教育您如何使您的代码更具可扩展性和更易于开发。这本书旨在使您的代码不仅仅是一堆函数和类,而是更倾向于设计良好、编写良好和经过良好测试的代码。
您需要对 PHP 有一定的了解,并且足够构建一个应用程序,但绝不必在 PHP 的所有方面都成为专家;对软件工程基础知识的掌握肯定会让您事半功倍。
您必须以开放的心态和愿意挑战您对软件开发的先入之见来阅读本书。本书将揭示一些关于您作为开发人员可能个人失败的真相;您必须愿意接受这些原则。
本书提供了一套您可以采用的软件开发模式和原则。您必须理解这些模式应该和不应该应用的地方;这将在整本书中得到解释,特别是在最后一章。
阅读本书的一个关键原则是了解 PHP 的用途和不用途。我希望您在阅读本书时了解您期望 PHP 解决的问题以及您期望使用其他软件开发语言解决的问题。
约定
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些示例以及它们的含义解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“index.php
文件现在产生了这个结果”。
代码块设置如下:
<?php
abstract class Notifier
{
protected $to;
public function __construct(string $to)
{
$this->to = $to;
}
abstract public function validateTo(): bool;
abstract public function sendNotification(): string;
}
任何命令行输入或输出都以以下方式编写:
**echo $richard->hasPaws;**
新术语和重要单词以粗体显示。例如,您在屏幕上看到的单词,比如菜单或对话框中的单词,会在文本中以这种方式出现:“将您的网络浏览器指向您选择的网络服务器,您应该会在屏幕上看到 Hello world!弹出”。
注意
警告或重要说明会以这样的方式出现在一个框中。
提示
提示和技巧会以这种方式出现。
第一章:为什么“优秀的 PHP 开发者”不是一个矛盾修辞
2010 年,MailChimp 在他们的博客上发表了一篇名为“呃,你用 PHP?”的文章。在这篇博文中,他们描述了当他们向认为“优秀的 PHP 程序员”是一个矛盾修辞的开发人员解释他们选择 PHP 时的恐怖。在他们的反驳中,他们辩称他们的 PHP 不是“你爷爷的 PHP”,他们使用了一个复杂的框架。我倾向于根据 PHP 的质量来评判,不仅仅是它的功能,还有它的安全性和架构。这本书关注的是关于如何设计代码的想法。软件的设计允许开发人员以无 bug 和优雅的方式轻松扩展代码的用途。
马丁·福勒说:
“任何傻瓜都可以写出计算机能理解的代码。优秀的程序员写出人类能理解的代码。”
这不仅仅局限于代码风格,而是开发人员如何设计和构建他们的代码。我遇到过许多开发人员,他们总是埋头于文档,复制和粘贴代码片段,直到它能够工作;拼凑代码片段直到它能够工作。此外,我经常看到软件开发过程迅速恶化,因为开发人员将他们的类与越来越长的函数紧密耦合。
软件工程师不仅仅是编写软件;他们必须知道如何设计软件。事实上,一个优秀的软件工程师在面试其他软件工程师时会询问关于代码设计本身的问题。获得一段可以执行的代码是微不足道的,询问开发人员strtolower
或str2lower
哪个是函数的正确名称也是无害的(记录上,是strtolower
)。知道类和对象之间的区别并不能让你成为一个称职的开发人员;一个更好的面试问题可能是如何将子类型多态应用于真实的软件开发挑战。未能评估软件设计技能会使面试变得肤浅,并导致无法区分谁擅长它,谁不擅长。这些高级话题将在本书中讨论,通过学习这些策略,你将更好地理解在讨论软件架构时应该问什么问题。
Moxie Marlinspike 曾在推特上发表过以下言论:
“作为一名软件开发人员,我羡慕作家、音乐家和电影制作人。与软件不同,当他们创作出一些东西时,它真的完成了,永远。”
在开发软件时,我们不能忘记我们是作者,不仅仅是为了机器的指令,而且我们还在创作一些我们希望其他人能够扩展的东西。因此,我们的代码不仅仅是针对机器的,也是针对人类的。代码不仅仅是机器的诗歌,也应该是人类的诗歌。
这当然说起来容易做起来难。在 PHP 中,这可能特别困难,因为 PHP 为开发人员提供了如何设计和构建他们的代码的自由。由于自由的本质,它可能被使用和滥用,这在 PHP 提供的自由中也是真实的。
因此,开发人员理解适当的软件设计实践以确保他们的代码保持长期可维护性变得越来越重要。事实上,另一个关键技能在于重构代码,改进现有代码的设计,使其更容易在长期内扩展。
技术债务,糟糕系统设计的最终后果,是我发现作为 PHP 开发人员职业生涯的一部分。这对我来说是真实的,无论是处理提供高级功能还是简单网站的系统。它通常是因为开发人员选择以各种原因实施糟糕的设计而产生的;这是在向现有代码库添加功能或在软件的初始构建过程中做出糟糕的设计决策时。重构可以帮助我们解决这些问题。
SensioLabs(Symfony 框架的创建者)有一个名为Insight的工具,允许开发人员计算他们自己代码中的技术债务。2011 年,他们使用这个工具对各种项目进行了技术债务评估;毫不奇怪,他们发现 WordPress 4.1 在他们评估的所有平台中名列前茅,他们声称解决项目中的技术债务需要 20.1 年的时间。
熟悉 WordPress 核心的人可能不会对此感到惊讶,但这个问题当然不仅与 WordPress 有关。在我的 PHP 职业生涯中,从处理安全关键的加密系统到处理与使命关键嵌入式系统相关的系统,处理技术债务是工作的一部分。处理技术债务对于 PHP 开发人员来说并不是一件可耻的事情,有些人甚至可能认为这是勇敢的。处理技术债务并不是一件容易的事情,特别是面对越来越苛刻的用户群、客户或项目经理;不断要求更多功能,而不了解项目所带来的技术债务。
我最近给 PHP Internals 组发送了电子邮件,询问他们是否应该考虑废弃错误抑制运算符@
。当任何 PHP 函数前面加上@符号时,该函数将抑制其返回的错误。这可能是残酷的,特别是当该函数产生致命错误导致脚本停止执行时,使得调试变得困难。如果错误被抑制,脚本可能无法执行,而不提供开发人员原因。在某些情况下,使用这个运算符可能被描述为反模式,我们将在第四章结构设计模式中进行介绍。
尽管没有人反对处理错误的更好方法(try
/catch
,适当的验证
)比滥用错误抑制运算符,并且废弃应该是 PHP 的最终目标,但事实是,一些函数即使已经有了成功/失败值,仍会返回不必要的警告。这意味着由于 PHP 核心中的技术债务,这个运算符在很多其他先决条件工作完成之前无法被废弃。与此同时,开发人员需要决定处理错误的最佳方法。在不解决不必要的错误报告的固有问题之前,这个运算符无法被废弃。因此,开发人员需要接受教育,了解应该用于处理错误的适当方法,而不是不断地使用@
符号。
从根本上说,技术债务减缓了项目的开发速度,并经常导致部署的代码存在问题,因为开发人员试图在一个脆弱的项目上工作。
在开始新项目时,永远不要害怕讨论架构,因为架构会议对开发人员的合作至关重要;正如我与之合作过的 Scrum Master 在面对“会议是工作的绝佳替代品”这一批评时所说的,“会议就是工作……如果没有会议,你会做多少工作?”
在本章的其余部分,我们将涵盖以下几点:
-
编码风格-PSR 标准
-
修订面向对象编程
-
使用 Composer 设置环境
-
谁是四人帮?
编码风格 - PSR 标准
在编码风格方面,我想向你介绍 PHP 框架互操作组创建的 PSR 标准。具体来说,适用于编码标准的两个标准是 PSR-1(基本编码风格)和 PSR-2(编码风格指南)。除此之外,还有 PSR 标准涵盖其他领域,例如,截至今天;PSR-4 标准是该组发布的最新的自动加载标准。你可以在www.php-fig.org/
找到更多关于这些标准的信息。
我坚信使用编码风格来强制执行代码库中的一致性。这确实会对整个项目中的代码可读性产生影响。特别是在开始一个项目时(很可能你正在阅读本书以找出如何正确做到这一点),因为你的编码风格决定了在这个项目上跟随你的开发人员将采用的风格。使用 PSR-1 或 PSR-2 这样的全局标准意味着开发人员可以轻松地在项目之间切换,而不必在他们的 IDE 中重新配置他们的代码风格。良好的代码风格可以使格式错误更容易发现。毋庸置疑,随着时间的推移,编码风格会发展,迄今为止,我选择遵循 PSR 标准。
我坚信这句话:永远编写代码,就好像最终维护你的代码的人是一个知道你住在哪里的暴力精神病患者。虽然不知道谁最初写下这句话,但普遍认为可能是约翰·伍兹或者马丁·戈尔丁。
我强烈建议在继续阅读本书之前熟悉这些标准。
修订面向对象编程
面向对象编程不仅仅是类和对象;它是一种基于对象(数据结构)的整个编程范式,其中包含数据字段和方法。理解这一点至关重要;使用类来组织一堆无关的方法并不是面向对象。
假设你已经了解了类(以及如何实例化它们),让我提醒你一些不同的要点。
多态
多态是一个相当长的词,表示一个相当简单的概念。本质上,多态意味着相同的接口与不同的基础代码一起使用。因此,多个类可以有一个绘制函数,每个函数接受相同的参数,但在基础级别上,代码实现是不同的。
在这一部分,我想特别谈谈子类型多态(也称为子类型或包含多态)。
假设我们的超类型是动物;我们的子类型可能是猫、狗和羊。
在 PHP 中,接口允许你定义一个类必须包含的一组功能,从 PHP 7 开始,你还可以使用标量类型提示来定义我们期望的返回类型。
例如,假设我们定义了以下接口:
interface Animal
{
public function eat(string $food) : bool;
public function talk(bool $shout) : string;
}
然后我们可以在我们自己的类中实现这个接口,如下所示:
class Cat implements Animal {
}
如果我们在没有定义类的情况下运行此代码,将会收到以下错误消息:
Class Cat contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (Animal::eat, Animal::talk)
本质上,我们需要实现我们在接口中定义的方法,所以现在让我们创建一个实现这些方法的类:
class Cat implements Animal
{
public function eat(string $food): bool
{
if ($food === "tuna") {
return true;
} else {
return false;
}
}
public function talk(bool $shout): string
{
if ($shout === true) {
return "MEOW!";
} else {
return "Meow.";
}
}
}
现在我们已经实现了这些方法,我们可以实例化我们想要的类并使用其中包含的函数:
**$felix = new Cat();echo**
**$felix->talk(false);**
那么多态是如何发挥作用的呢?假设我们有另一个类代表狗:
class Dog implements Animal
{
public function eat(string $food): bool
{
if (($food === "dog food") || ($food === "meat")) {
return true;
} else {
return false;
}
}
public function talk(bool $shout): string
{
if ($shout === true) {
return "WOOF!";
} else {
return "Woof woof.";
}
}
}
现在假设我们在pets
数组中有多种不同类型的动物:
$pets = array(
'felix' => new Cat(),
'oscar' => new Dog(),
'snowflake' => new Cat()
);
现在我们可以逐个循环遍历所有这些宠物,以便运行talk
函数。我们不关心宠物的类型,因为我们扩展了动物接口,所以我们得到的每个类中实现的talk
方法都是由我们实现的。
假设我们想让所有的动物都运行talk
方法。我们可以使用以下代码:
foreach ($pets as $pet) {
echo $pet->talk(false);
}
不需要不必要的switch
/case
块来包装我们的类,我们只需使用软件设计来让事情变得更容易。
抽象类的工作方式类似,不同之处在于抽象类可以包含功能,而接口则不行。
需要注意的是,任何定义了一个或多个抽象类的类也必须被定义为抽象类。你不能有一个普通类定义抽象方法,但你可以在抽象类中有普通方法。让我们从重构我们的接口开始,将其变成一个抽象类:
abstract class Animal
{
abstract public function eat(string $food) : bool;
abstract public function talk(bool $shout) : string;
public function walk(int $speed): bool {
if ($speed > 0) {
return true;
} else {
return false;
}
}
}
你可能已经注意到我还添加了一个walk
方法作为一个普通的、非抽象的方法;这是一个标准的方法,可以被任何继承父抽象类的类使用或扩展。它们已经有了它们的实现。
请注意,无法实例化抽象类(就像无法实例化接口一样)。相反,我们必须扩展它。
所以,在我们的Cat
类中让我们移除以下内容:
class Cat implements Animal
我们将用以下代码替换它:
class Cat extends Animal
这就是我们需要重构的全部内容,以便让类扩展Animal
抽象类。我们必须按照我们为接口所概述的那样在类中实现抽象函数,另外我们可以使用普通函数而不需要实现它们:
**$whiskers = new Cat();**
**$whiskers->walk(1);**
从 PHP 5.4 开始,也可以在一个系统中实例化一个类并访问它的属性。PHP.net 宣传它为:在实例化时添加类成员访问,例如(new Foo)->bar()。你也可以对单个属性这样做,例如,(new Cat)->legs。在我们的例子中,我们可以这样使用它:
(new \IcyApril\ChapterOne\Cat())->walk(1);
再来回顾一下 PHP 如何实现 OOP 的其他一些要点,类声明之前或者函数声明之前的final
关键字意味着在它们被定义之后不能覆盖这样的类或函数。
所以,让我们尝试扩展一个我们称之为final
的类:
final class Animal
{
public function walk()
{
return "walking...";
}
}
class Cat extends Animal
{
}
这将导致以下输出:
Fatal error: Class Cat may not inherit from final class (Animal)
同样地,让我们在函数级别做同样的事情:
class Animal
{
final public function walk()
{
return "walking...";
}
}
class Cat extends Animal
{
public function walk () {
return "walking with tail wagging...";
}
}
这将导致以下输出:
Fatal error: Cannot override final method Animal::walk()
Trait(多重继承)
Trait被引入到 PHP 中作为引入水平重用的机制。PHP 传统上是一种单继承语言,因为你不能将多个类继承到一个脚本中。
传统的多重继承是一个备受软件工程师鄙视的有争议的过程。
让我先给你举一个使用 Trait 的例子;让我们定义一个抽象的Animal
类,我们想要将其扩展到另一个类中:
class Animal
{
public function walk()
{
return "walking...";
}
}
class Cat extends Animal
{
public function walk () {
return "walking with tail wagging...";
}
}
现在假设我们有一个函数来为我们的类命名,但我们不希望它适用于所有扩展Animal
类的类,我们希望它适用于某些类,无论它们是否继承了抽象Animal
类的属性。
所以我们定义了我们的函数如下:
function setFirstName(string $name): bool
{
$this->firstName = $name;
return true;
}
function setLastName(string $name): bool
{
$this->lastName = $name;
return true;
}
现在的问题是,除了使用水平重用之外,没有地方可以放置它们,除了复制和粘贴不同的代码片段或者诉诸于使用条件继承。这就是 Trait 出手的地方;让我们首先把这些方法放在一个叫做Name
的 Trait 中。
trait Name
{
function setFirstName(string $name): bool
{
$this->firstName = $name;
return true;
}
function setLastName(string $name): bool
{
$this->lastName = $name;
return true;
}
}
现在我们已经定义了我们的 Trait,我们可以告诉 PHP 在我们的Cat
类中使用它:
class Cat extends Animal
{
use Name;
public function walk()
{
return "walking with tail wagging...";
}
}
注意Name
语句的使用?这就是魔法发生的地方。现在你可以调用 Trait 中的函数而不会出现任何问题:
**$whiskers = new Cat();
$whiskers->setFirstName('Paul');
echo $whiskers->firstName;**
把所有东西放在一起,新的代码块看起来是这样的:
trait Name
{
function setFirstName(string $name): bool
{
$this->firstName = $name;
return true;
}
function setLastName(string $name): bool
{
$this->lastName = $name;
return true;
}
}
class Animal
{
public function walk()
{
return "walking...";
}
}
class Cat extends Animal
{
use Name;
public function walk()
{
return "walking with tail wagging...";
}
}
$whiskers = new Cat();
$whiskers->setFirstName('Paul');
echo $whiskers->firstName;
标量类型提示
让我借此机会向你介绍一个名为标量类型提示的 PHP 7 概念;它允许你定义返回类型(是的,我知道这严格来说不属于 OOP 的范围;接受它吧)。
让我们定义一个函数,如下所示:
function addNumbers (int $a, int $b): int
{
return $a + $b;
}
让我们来看看这个函数;首先,您会注意到在每个参数之前,我们定义了我们想要接收的变量类型;在这种情况下,它是 int(或整数)。接下来,您会注意到在函数定义之后有一些代码:int
,它定义了我们的返回类型,因此我们的函数只能接收一个整数。
如果您没有提供正确类型的变量作为函数参数,或者没有从函数中返回正确类型的变量;您将收到TypeError
异常。在严格模式下,如果启用了严格模式,并且您还提供了不正确数量的参数,PHP 也将抛出TypeError
异常。
在 PHP 中也可以定义strict_types
;让我解释为什么您可能想要这样做。没有strict_types
,PHP 将尝试在非常有限的情况下自动将变量转换为定义的类型。例如,如果您传递一个仅包含数字的字符串,它将被转换为整数,然而,一个非数字的字符串将导致TypeError
异常。一旦您启用了strict_types
,这一切都会改变,您将不再具有这种自动转换行为。
以前的例子中,没有strict_types
,您可以执行以下操作:
**echo addNumbers(5, "5.0");**
在启用strict_types
后再次尝试,您将发现 PHP 抛出TypeError
异常。
这个配置只适用于单个文件,将其放在包含其他文件之前不会导致这个配置被继承到那些文件中。PHP 选择这条路线有多个好处;它们在实现标量类型提示的 RFC 版本 0.5.3 中非常清楚地列出,名为PHP RFC:标量类型声明。您可以通过访问www.wiki.php.net
(维基,而不是主要的 PHP 网站)并搜索scalar_type_hints_v5
来了解更多信息。
为了启用它,请确保将其作为 PHP 脚本中的第一个语句:
declare(strict_types=1);
除非您将strict_types
定义为 PHP 脚本中的第一个语句;不允许对此定义进行其他用途。实际上,如果您尝试稍后定义它,您的脚本 PHP 将抛出致命错误。
当然,出于对这本书的愤怒的 PHP 核心狂热分子的利益,我应该提到,还有其他有效的类型可以用于类型提示。例如,PHP 5.1.0 引入了数组,PHP 5.0.0 引入了开发人员可以对其自己的类进行类型提示的功能。
让我给你一个快速的实例,说明这在实践中是如何工作的,假设我们有一个Address
类:
class Address
{
public $firstLine;
public $postcode;
public $country;
public function __construct(string $firstLine, string $postcode, string $country)
{
$this->firstLine = $firstLine;
$this->postcode = $postcode;
$this->country = $country;
}
}
然后我们可以对我们注入到Customer
类中的Address
类进行类型提示:
class Customer
{
public $name;
public $address;
public function __construct($name, Address $address)
{
$this->name = $name;
$this->address = $address;
}
}
这就是它如何全部组合在一起:
**$address = new Address('10 Downing Street', 'SW1A 2AA', 'UK');
$customer = new Customer('Davey Cameron', $address);
var_dump($customer);**
限制对私有/受保护属性的调试访问
如果您定义一个包含私有或受保护变量的类,您会注意到一个奇怪的行为,如果您var_dump
该类的对象。您会注意到,当您将对象包装在var_dump
中时,它会显示所有变量;无论它们是受保护的、私有的还是公共的。
PHP 将var_dump
视为内部调试函数,这意味着所有数据都变得可见。
幸运的是,这方面有一个解决方法。PHP 5.6 引入了__debugInfo
魔术方法。在类中以双下划线开头的函数代表魔术方法,并与特殊功能相关联。每当您尝试var_dump
一个设置了__debugInfo
魔术方法的对象时,var_dump
将被该函数调用的结果覆盖。
让我向您展示这在实践中是如何工作的,让我们从定义一个类开始:
class Bear {
private $hasPaws = true;
}
让我们实例化这个类:
**$richard = new Bear();**
现在,如果我们尝试访问私有变量hasPaws
,我们将收到致命错误:
**echo $richard->hasPaws;**
前面的调用将导致抛出以下致命错误:
**Fatal error: Cannot access private property Bear::$hasPaws**
这是预期的输出,我们不希望private
属性在其对象外部可见。也就是说,如果我们用var_dump
包装对象如下:
var_dump($richard);
然后我们会得到以下输出:
object(Bear)#1 (1) {
["hasPaws":"Bear":private]=>
bool(true)
}
正如您所看到的,我们的private
属性被标记为private
,但它仍然是可见的。那么我们该如何防止这种情况发生呢?
因此,让我们重新定义我们的类如下:
class Bear {
private $hasPaws = true;
public function __debugInfo () {
return call_user_func('get_object_vars', $this);
}
}
现在,当我们实例化我们的类并var_dump
生成的对象时,我们会得到以下输出:
object(Bear)#1 (0) {
}
现在,整个脚本看起来是这样的,您会注意到我添加了一个额外的public
属性,名为growls
,我将其设置为true
:
<?php
class Bear {
private $hasPaws = true;
public $growls = true;
public function __debugInfo () {
return call_user_func('get_object_vars', $this);
}
}
$richard = new Bear();
var_dump($richard);
如果我们要var_dump
这个脚本(同时使用public
和private
属性),我们会得到以下输出:
object(Bear)#1 (1) {
["growls"]=>
bool(true)
}
正如您所看到的,只有public
属性是可见的。那么从这个小实验中的故事的道德是什么呢?首先,var_dumps
暴露了对象中的私有和受保护属性,其次,这种行为是可以被覆盖的。
使用 Composer 设置环境
Composer是 PHP 的一个依赖管理器,受 Node 的 NPM 和 Bundler 的强烈启发。它现在已经成为多个 PHP 项目的重要组成部分,包括 Laravel 和 Symfony。然而,对我们来说它有用的原因是,它包含符合 PSR-0 和 PSR-4 标准的自动加载功能。您可以从getcomposer.org
下载并安装 Composer。
注意
要在 Mac OS X 或 Linux 上全局安装 Composer,首先可以运行安装程序:
curl -sS https://getcomposer.org/installer | php
然后您可以将 Composer 移动到全局安装:
**mv composer.phar /usr/local/bin/composer**
如果前面的命令由于权限问题而失败,请重新运行命令,但在开头加上 sudo
。在输入命令后,您将被要求输入密码,只需输入密码并按 Enter。
一旦您按照上述步骤安装了 Composer,您可以通过运行composer
命令来运行它。
要在 Windows 上安装 Composer,最简单的方法是直接在 Composer 网站上运行安装程序;目前您可以在以下位置找到它:
getcomposer.org/Composer-Setup.exe
。
Composer 相当容易更新,只需运行此命令:
**Composer self-update**
Composer 通过使用名为composer.json
的文件中的配置来工作,在这里您可以概述外部依赖项和自动加载样式。一旦 Composer 安装了此文件中列出的依赖项,它将编写一个composer.lock
文件,其中详细说明了它安装的确切版本。在使用版本控制时,重要的是要提交此文件(以及composer.json
文件),如果您使用 Git,则不要将其添加到您的.gitignore
文件中。这非常重要,因为锁定文件详细说明了在版本控制系统中特定时间安装的软件包的确切版本。但是,您可以排除一个名为vendor
的目录,稍后我会解释它的作用。
让我们首先在项目目录中创建一个名为composer.json
的文件。这个文件是以 JSON 格式结构化的,所以让我提醒您一下 JSON 的工作原理:
-
JSON 由数据的键/值对组成,可以将其视为在文件中定义一组变量。
-
键值对用逗号分隔,例如,
"key" : "value"
-
花括号包含对象
-
方括号包含数组
-
多个数据必须用逗号分隔,不要在数据末尾留下逗号
-
包括字符串的键和值必须用引号括起来
-
反斜杠
\
是转义键
现在我们可以将以下标记添加到composer.json
文件中:
{
"autoload": {
"psr-4": {
"IcyApril\\ChapterOne": "src/"
}
}
}
所以让我解释一下这个文件的作用;它告诉 Composer 将src/
目录中的所有内容自动加载到IcyApril\ChapterOne
命名空间中,使用 PSR-4 标准。
那么,下一步是创建我们的src
目录,其中包括我们想要自动加载的代码。搞定了吗?好的,现在让我们打开命令行,并进入我们放置composer.json
文件的目录。
为了在您的项目中安装composer.json
文件中的所有内容,只需运行composer install
命令。对于后续更新,composer update
命令将根据composer.json
中定义的所有依赖项的最新版本进行更新。如果您不想这样做,还有另一种选择;运行composer dump-autoload
命令将仅重新生成需要包含在项目中的 PSR-0/PSR-4 类的列表(例如,您添加、删除或重命名了一些类)。
现在让我来介绍一下你实际上将如何创建一个类。所以,在我们的项目中创建一个src
目录,在该src
目录中创建一个名为Book
的新类。您可以通过创建一个名为Book.php
的文件来实现这一点。在该文件中,添加类似以下内容:
<?php
namespace IcyApril\ChapterOne;
class Book
{
public function __construct()
{
echo "Hello world!";
}
}
这是一个标准类,只是我们定义了一个构造函数,当实例化类时将会输出Hello world!
。
您可能已经注意到,我们遵循了一些命名约定;首先,PSR-1 标准声明类名必须以 StudlyCaps 形式声明。PSR-2 有一些额外的要求;举个例子:四个空格而不是制表符,命名空间或使用声明后面有一个空格,以及将括号放在新行上。如果您还没有阅读这些标准,那么花时间阅读这些标准绝对是值得的。您可能不同意每个标准,您可能对如何格式化自己的代码有主观偏好;我的建议是将这些偏好放在一边,为了更大的利益。通过利用 PSR 标准标准化的代码在共同的代码库上进行协作时具有巨大的优势。通过 PHP-FIG 组织等外部标准的好处是,您的 IDE 中已经预先构建了您的配置(例如,PHPStorm 支持 PSR-1/PSR-2)。不仅如此,当涉及到格式化参数时,您有一个具体的公正文件,概述了应该如何做事情,这对于在代码审查期间阻止宗教性的代码格式化争论非常有益。
既然我们已经创建了类,我们可以继续运行composer dump-autoload
命令,以刷新我们的自动加载程序脚本。
所以,我们已经配置了 Composer 自动加载程序,也有了一个测试类可以操作,但下一个问题是我们如何实现这一点。所以,让我们继续实现这一点。在我们实现composer.json
文件的同一目录中,让我们添加我们的index.php
文件。
在您放入 PHP 开标签后的一行,我们需要引入我们的自动加载程序脚本:
require_once('vendor/autoload.php');
然后我们可以实例化我们的Book
类:
new \IcyApril\ChapterOne\Book();
设置您的 Web 服务器,将文档根目录指向我们创建的文件夹,将您选择的 Web 服务器指向您的 Web 浏览器,然后您应该在屏幕上看到 Hello world!现在您可以拆开代码并进行操作。
完成的代码示例可与本书一起使用,因此您可以直接从那里打开它并进行操作,以防您需要帮助调试您的代码。
无论您的类是抽象类还是纯接口,在自动加载时我们都将它们视为类。
四人帮(GoF)
建筑师克里斯托弗·亚历山大提到了模式如何用于解决常见的设计问题,最初记录了这一概念。这个想法来自亚历山大;他提出设计问题可以严格记录,以及它们的解决方案。设计模式最显著地应用于解决软件设计中的架构问题。
用克里斯托弗·亚历山大的话说:
“这种语言的元素是被称为模式的实体。每个模式描述了我们环境中反复出现的问题,然后以这样一种方式描述了解决这个问题的核心,以便您可以使用这个解决方案一百万次,而不必重复一样的方式。”
亚历山大写了一本自己的书,早于四人帮的书,名为《模式语言》。在这本书中,亚历山大创造了自己的语言,他创造了“模式语言”这个词来描述这一点;这种语言是由建筑模式的基本构建模块形成的。通过利用这些建筑模式,该书提出普通人可以将这种语言用作改善他们的社区和城镇的框架。
书中记录的一种这样的模式是“模式 12”,被称为 7000 人的社区;书中通过以下方式记录了这种模式:
“个人在超过 5,000-10,000 人的任何社区中都没有有效的发言权。”
通过使用这样的问题及其记录的解决方案,该书最终形成了模式,这些模式旨在成为改善社区的基本构建模块。
正如我所提到的,亚历山大先于四人帮;但他的工作对于播种软件设计模式的种子至关重要。
现在,让我们直接转向被称为“四人帮”的作者。
不,我们不是指 1981 年英国工党的叛逃者,也不是指一支英国后朋克乐队;但我们谈论的是一本名为《设计模式:可复用面向对象软件的元素》的书的作者。这本书在软件开发领域具有很高的影响力,在软件工程领域广为人知。
在书的第一章中,作者们从自己的个人经验讨论了面向对象的软件开发;这包括争论软件开发人员应该为接口而不是实现编程。这最终导致代码最终利用了面向对象编程的核心功能。
人们普遍误解这本书只包含四种设计模式,这是不正确的;它涵盖了来自三个基本类别的 23 种设计模式。
让我们来看看这些类别是什么:
-
创建型
-
结构型
-
行为
所以让我们逐一解释这些。
创建型设计模式
创建型设计模式涉及对象本身的创建。在不使用设计模式的情况下基本实例化类可能会导致不必要的复杂性,也可能会导致重大的设计问题。
创建型设计模式的主要用途是将类的实例化与该实例的使用分开。不使用创建型设计模式可能意味着您的代码更难理解和测试。
依赖注入
依赖注入是一种过程,通过这种过程,您可以直接将应用程序需要的依赖项输入到对象本身中。
John Munsch 在 Stack Overflow 上留下了一个名为“五岁孩子的依赖注入”的答案,这个答案被重新发表在书籍《Mark Seeman's Dependency Injection in .NET》中:
提示
当你自己去冰箱里拿东西时,可能会引起问题。你可能会把门打开,你可能会拿到爸爸妈妈不想让你拿的东西。你甚至可能在找我们根本没有或者已经过期的东西。
提示
你应该说出你的需求,“我需要午餐时喝点什么”,然后我们会确保你坐下来吃饭时有东西喝。
在编写类时,自然会使用其他依赖项;也许是数据库模型类。因此,通过依赖注入,类不是在自身中创建其数据库模型,而是在对象外部创建它并注入它。简而言之,我们将客户端的行为与客户端的依赖项分开。
在考虑依赖注入时,让我们概述涉及的四个独立角色:
-
要注入的服务
-
依赖于被注入服务的客户端
-
确定客户端如何使用服务的接口
-
负责实例化服务并将其注入客户端的注入器
结构设计模式
结构设计模式相当容易解释,它们充当实体之间的连接器。它作为基本类如何组合形成更大实体的蓝图,所有结构设计模式都涉及对象之间的互连。
行为设计模式
行为设计模式用于解释对象之间的交互方式;它们如何在对象之间发送消息,以及如何将各种任务的步骤分配给不同的类。
结构模式描述设计的静态架构;行为模式更加灵活,描述了一个流动的过程。
架构模式
这不是严格意义上的设计模式(但四人帮在他们的书中没有涵盖架构模式);但由于 PHP 的面向 Web 的特性,它对 PHP 开发人员非常相关。架构模式通过解决计算机系统中的各种不同约束来解决性能限制、高可用性以及业务风险的最小化。
大多数开发人员在涉及 Web 框架时会熟悉模型-视图-控制器架构,最近其他架构开始出现;例如,微服务架构通过一组独立且相互连接的 RESTful API 工作。一些人认为微服务将问题从软件开发层转移到系统架构层。与微服务相反,通常被称为单块架构,是所有代码都集中在一个应用程序中。
总结
在本章中,我们复习了一些 PHP 原则,包括面向对象编程原则。我们还复习了一些 PHP 语法基础。我们已经看到您可以如何在 PHP 中使用 Composer 进行依赖管理。除此之外,我们还讨论了 PSR 标准以及如何在自己的代码中实现它们,以使您的代码更易于他人阅读,并且符合其他重要标准(无论是自动加载还是 HTTP 消息传递)。最后,我们介绍了设计模式和四人帮以及设计模式背后的历史。
第二章:反模式
这就是我们开始讨论反模式的地方;在你满怀希望地认为我将告诉你一些了不起的东西,可以在不使用设计模式的情况下奇妙地简化你的代码之前,我在这里不会这样做(我是否提到过我擅长粉碎希望和梦想?)。简而言之,反模式是你不想在你的代码中出现的东西。
说到粉碎希望和梦想,如果你曾经有过初级开发人员,反模式也是教授应该避免的方法论的好方法。学习反模式还可以提高代码审查的效率;你可以有一个外部来源来咨询代码质量,而不是基于个人意见来辩论代码质量。
反模式构成了一种解决经常出现的问题的可怕方法,通常是无效的,并且有很高的反生产力风险。它们可能会产生技术债务,因为开发人员必须后来努力重构以解决最初的问题,但希望使用更具弹性的设计模式。
我们都遇到过意大利面代码;我和一个合同开发人员一起工作时,他在面对高技术债务的产品负责人时大声喊道:“意大利面太多了,我可能还不如开一家餐厅!”意大利面代码是指程序的控制结构几乎无法理解,因为它太混乱和过于复杂,可以被描述为一种反模式。在 PHP 5.3.0 中的一个主要批评是语言中 goto 操作符的实现。事实上,批评它们的实施的人声称,goto 操作符将为 PHP 中的意大利面代码提供另一个借口。
在 PHP 中,goto 语句曾引起了很大的争议,甚至有人将其报告为一个 bug,并表示:“PHP 5.3 包括 goto。这是一个问题。说真的,PHP 在没有 goto 的情况下已经走到了这一步,为什么要把这种语言变成公共威胁呢?”
除此之外,bug 报告的提交者将预期结果列为:“世界将会结束”,实际结果是“世界已经结束”。尽管如此,在 PHP 中,goto 操作符受到严格限制,因此你不能随意跳入和跳出函数。有些人还认为它们在有限状态机中很有用(基本上是基于多个输入的二进制输出),但这也是有争议的;所以我会让你自己对它们做出判断。
你可能也经历过复制粘贴编程,整个代码块被复制并粘贴到程序中;这是另一个糟糕软件设计的例子。实际上,开发人员应该设计他们的软件,以创造通用的解决方案来解决问题,而不是复制、重构和粘贴代码来适应某种情况。
我将在本章中介绍为什么学习反模式很重要。在本章中,我将讨论传统的与反模式相关的软件设计,还有与反模式相关的网络基础设施和管理风格。除此之外,我想讨论一些 PHP 特定的反模式,或者 PHP 中的缺陷,这可能需要你在自己的代码中进行补偿。
本书包含了一个专门的章节,讲述了重构的过程;如果重构的过程对你感兴趣,这一章将帮助你打下你可能想要开始思考的理念基础;除此之外,专门讲述设计模式的章节可能会帮助你意识到你最终可能要达到的代码。在专门讲述重构的章节中,我们还将涵盖一些代码异味,这可以帮助你发现你正在维护的代码库中的反模式。
为什么反模式很重要
大多数程序员都来自采用某种反模式的背景,直到最终意识到它无法扩展或效果不佳。当我 17 岁时,在我的第一份学徒开发人员工作中,我会被送到伦敦,从周一到周五,以某种方式把我的西装和完全黑色的衣服压缩成一个令人惊讶的小手提箱,然后学习软件开发。周五,我们经常在中午 12:00 放半天假,但我会提前预订公司的火车票,所以我会在快餐店或咖啡店里工作简单的项目。每周,当我回来尝试扩展其中一个解决方案时,我会意识到新的可扩展性问题和代码质量问题。当然,我以前也做过开发,但这些主要是处理全新的、非常简短的编程任务,使用预先制作的框架,或者处理已经完成架构的遗留代码(或者,我现在意识到,是用非常迟钝的刀切割的)。扩展自己代码的这个学习过程很棒;我迅速教会自己如何更好地设计软件。作为人类,我们经常对某个主题了解不够,以至于不知道自己知道的有多少(我发现这在那些管理软件开发人员但从未自己编写过任何代码的人身上非常真实);在牢记这一点的同时,我们应该记住,我们永远不会超越从自己的错误中学习。虽然这非常重要,但教会自己记录的反模式也是为了从他人的错误中学习至关重要。
我曾经是一位开发人员的技术负责人和导师,他从犯错中学习受到了最严厉的对待。在我和这位开发人员的第一次评估中,我的人力资源对手告诉我,每次他犯错时,之前的技术负责人和人力资源负责人都会把他拉到会议室进行正式的纪律程序。这两个人的技术知识非常有限,而且在管理开发人员方面完全无能(以至于他们是那种生活在自己的泡泡中,对更成功的环境中人们的工作方式一无所知,基本上陷入了没有前途的职业生涯,没有知识去做任何有意义的事情)。等他们离开时,这位可怜的开发人员的自信已经被压垮到了一个程度,以至于他对网络没有真正的职业抱负,也不急于学习。在你的职位上快乐并没有错。正如我的一位前上司在我告诉他一些非常个人的事情后所说的,“最重要的是你快乐”。是的,让世界运转需要很多人,但一旦你把自己置于指导或管理其他开发人员的位置,你就有义务使自己保持在游戏的前沿。如果你是一个经理,你应该知道如何有效地做好你的工作。我遇到的最好的人事经理是那些拥有丰富知识的人,他们不断更新最新的管理方法,就像我喜欢不断更新 PHP 核心和社区最新最伟大的方法一样。在写这本书的过程中,我的项目和人事管理知识有所提高,但仍有很长的路要走,因此我不会在没有先教育自己的情况下接受这样的线路管理职责。在我工作的公司,实际上以恐吓作为管理策略,我曾经向部门负责人提到过这一点,他回答说“我们并不是说这是最好的做法”;如果这是真的,那么为了公司的利益,肯定应该采取一些措施来解决这个问题!这并不是整个公司都是这样;其他部门有着非常不同的态度,事实上,技术总监曾经就这个问题和知道自己不知道的重要性做了一个技术讲座。公司的 CEO 也和我开始了类似的对话,说他知道自己不知道。旧的做法很难改变,但至少他们已经开始迎接变革的风。
所以除了发牢骚(我确实喜欢发牢骚),我为什么要谈论这个?我的观点是你的态度很重要。关于这个问题,我最喜欢的一句话是“如果你把你的开发人员当作白痴,他们很快就会变成白痴”。让我进一步说一下:
-
学生的糟糕表现往往反映了老师的糟糕表现。
-
每个人都会犯错,但错误失控是管理者愚蠢行为的错,而不是开发人员的错。
-
白痴会吸引白痴。如果你在自己的专业知识领域是个白痴,那么你很可能会招募更多的白痴。
-
如果你在工作场所实行恐惧统治,你就是个白痴,害怕被发现。
-
如果你不知道自己知道的有多少,也不寻求有效地消除自己的无知,你就是个白痴。
-
如果你把你的开发人员当作白痴,那么你就是个白痴。
简而言之,了解自己知道的有多少,然后成长。听起来很残酷,但这是事实。我们都是无知的,我们无法知道一切。有效地利用我们自己的知识与他人的知识是成功的关键。认识到自己的无知是关键。例如,去年我决定,我在基础计算机科学方面的知识不够广泛,无法满足自己对它的需求,也无法满足我指导的人的需求;因此,我决定去读计算机科学的兼职硕士学位。学习过程非常棒,教会了我之前不知道存在的计算机科学领域。
一些软件开发人员使用其他人的工作,没错,WordPress 或 Drupal 的开发可以给你一个快乐和富有成效的职业,但你会发现为自己建立和设计东西是一次很好的学习经历。在传统的工程环境中工作过后,我已经被说服,计算机科学的坚实理论基础对软件工程师是非常有益的。事实上,理解计算机科学基本原理所需的知识体系实际上是相当容易掌握的。当然,在很多方面,我是在对已经信奉的人说教;如果你正在阅读这本书,你可能理解需要更深入的理论计算机科学知识基础,但请不要读完这本书就停止积极学习。继续制定计划来提高你的知识,努力改进我们头脑中存储的信息。
经常有人说“在盲人国度,有一只眼睛的人是国王”;较小的开发团队在良好的软件开发方面可能经常缺乏基础(也许是因为没有必要),而一些陷入过去的较大的开发环境最终可能陷入同样的境地。在这方面,知识变得更加珍贵,对开发人员了解软件开发同样重要。
反模式不仅仅是你的团队可以学会避免的东西;良好的软件开发需要对编程语言和软件开发的理论理解有坚实的了解。
最后,让我从 SourceMaking 的一篇文章中引用这句话:
“以架构为驱动的软件开发是构建系统的最有效方法。架构驱动方法优于需求驱动、文档驱动和方法论驱动方法。项目通常成功是尽管方法论,而不是因为它。”
情绪发泄完毕。让我们来谈谈一些反模式。
非自行开发综合症
密码学可以教给我们关于软件的一个非常重要的教训;这对于克尔克霍夫原则尤其如此。该原则陈述了这一点:
“即使系统的一切都是公开知识,一个加密系统也应该是安全的,除了密钥。”
这是由克劳德·香农改编的,称为香农定律:
“应该在假设敌人立即完全熟悉它们的情况下设计系统。”
简而言之,为了拥有一个安全的系统,它不应该只因为没有人知道它是如何实现的而被视为安全(“安全通过模糊性”)。如果你通过模糊性来保护你的钱,你会把它埋在树下,希望没有人会找到它。而当你使用真正的安全机制,比如把你的钱放在银行的保险柜里,你可以把安全系统的每一个细节都公开,只需要保密保险柜的钥匙,其他细节都可以是公开的信息。如果有人找到了你的保险柜的钥匙,你只需要改变组合,而如果有人真的找到了你的钱埋在树下,你就必须挖出钱,找别的地方放。
只通过模糊性来保护安全是一个坏主意(尽管并不总是坏主意)。正如你可能知道的,当你把密码存储在数据库中时,你应该使用一种单向的加密算法,称为哈希算法,以确保如果数据库被盗,没有人能够使用数据库中的数据找到用户的原始密码。当然,在现实中,你不应该只是对密码进行哈希处理,你应该对其进行盐处理,并使用诸如 PBKDF2 或 BCrypt 的算法,但本书不是关于密码安全的。
然而,情况的现实是,有时候,当开发人员真的费心去对密码进行哈希处理时,他们决定创建自己的密码哈希函数,这些函数很容易被逆向,并且只有通过不知道算法的模糊性来保护。这是非本地研发(NIH)综合症的一个完美例子;开发人员没有使用备受尊重的密码哈希库,而是决定自己创建,假装自己是一个密码学家,却不理解这样的决定的安全影响。
值得庆幸的是,PHP 现在让对密码进行哈希处理变得非常容易;password_hash
函数和password_verify
函数使这变得非常容易,而password_needs_rehash
函数甚至可以告诉你何时需要重新计算哈希。尽管如此,我岔开了话题。
那么,什么是 NIH 综合症?NIH 综合症是指对组织或个人开发者自身能力的虚假自豪感导致他们建立自己的解决方案,而不是采用更优秀的第三方解决方案。重新发明轮子不仅成本高昂、不必要,并且会增加不必要的维护开销;它也可能非常不安全。
也就是说,如果解决方案是封闭源和封锁的,那么最好避免使用它们。这样做也可以避免供应商锁定和对业务灵活性的限制。
NIH 综合症依赖于现有解决方案的良好性能和达到预期。使用第三方库并不是不检查其代码质量的借口。
为开源解决方案做贡献是缓解这些问题的好方法。对现有库有改进的空间?分叉它,提出合并的修改。没有符合你需求的功能的库?那么你可能需要考虑编写自己的库并发布它。
我将以这一节结束,说世界已经变得多元化;人们不再寻求一个技术堆栈来满足他们所有的需求;如今人们追求的是最适合工作的最佳工具。值得考虑如何利用这一事实来使自己受益。
使用 Composer 的第三方依赖
Composer 使管理第三方依赖变得非常容易。在第一章中,为什么“优秀的 PHP 开发人员”不是一个自相矛盾的词,我简要描述了如何使用 Composer 进行自动加载。自动加载从 PHP 5.1.2 以来就作为核心功能得到支持,但 Composer 的伟大之处在于你还可以用它进行依赖管理。Composer 可以根据你指定的版本约束有效地获取你需要的依赖项。
让我们从以下composer.json
文件开始:
{
"autoload": {
"psr-4": {
"IcyApril\\ChapterOne": "src/"
}
}
}
所以让我们拉取一个依赖项:
{
"autoload": {
"psr-4": {
"IcyApril\\ChapterOne": "src/"
}
},
"require": {
"guzzlehttp/guzzle": "⁶.1"
}
}
请注意,我们所做的只是添加了一个require
参数,指定我们想要的软件。没有手动将文件粘贴到你的项目或根目录,或者使用 Git 中的子模块!
在这种情况下,我们拉取了 Guzzle,一个用于 PHP 的 HTTP 库。
Composer 默认从一个名为Packergist的中央仓库查询仓库,该仓库汇总了你可以从各种版本控制系统(如 GitHub、BitBucket 或其他仓库主机)安装的软件包。如果你愿意,Packergist 就像一个电话簿,将 Composer 对代码仓库的软件包请求连接起来。
也就是说,Composer 不仅支持 Packergist 仓库。为了支持开源精神,它支持来自各种 VCS 系统(如 Git/SVN)的仓库,无论它们托管在何处。
让我们看一下以下composer.json
文件:
{
"autoload": {
"psr-4": {
"IcyApril\\ChapterTwo": "src/"
}
}
}
让我演示一下如何在没有在 Packergist 上的情况下包含一个来自 BitBucket 的仓库:
{
"autoload": {
"psr-4": {
"IcyApril\\ChapterOne": "src/"
}
},
"require": {
"IcyApril/my-private-repo": "dev-master"
},
"repositories": [
{
"type": "vcs",
"url": "git@bitbucket.org:IcyApril/my-private-repo.git"
}
]
}
就是这么简单!你只需指定你想要从中拉取的仓库,Composer 就会完成剩下的工作。使用其他版本控制系统也同样简单:
{
"autoload": {
"psr-4": {
"IcyApril\\ChapterOne": "src/"
}
},
"require": {
"IcyApril/myLibrary": "@dev"
},
"repositories": [
{
"type": "vcs",
"url": "http://svn.example.com/path/to/myLibrary"
}
]
}
有点厚颜无耻,Composer 甚至可以支持 PEAR PHP 仓库:
{
"autoload": {
"psr-4": {
"IcyApril\\ChapterOne": "src/"
}
},
"require": {
"pear-pear2.php.net/PEAR2_Text_Markdown": "*",
"pear-pear2/PEAR2_HTTP_Request": "*"
},
"repositories": [
{
"type": "pear",
"url": "https://pear2.php.net"
}
]
}
在你对composer.json
文件进行更改后更新依赖项的简单方法就是运行composer update
。
请注意,你不能仅使用composer dump-autoload
来更新外部依赖项。原因是dump-autoload
将仅更新你的自动加载器的类映射。它实质上是更新它需要自动加载的类的列表;它不会去拉取新的依赖项。
偶尔在使用 Composer 并拉取依赖项时,Git 可能会说你需要生成一个 GitHub 身份验证密钥。这是因为如果你在本地机器上安装了 Git,Composer 将会通过版本控制系统克隆依赖项;然而,偶尔,如果它从 GitHub 克隆仓库,你可能会遇到它的速率限制。如果发生这种情况,没有必要惊慌。Composer 会给你关于如何实际获取 API 密钥的指示,这样你就可以在没有速率限制的情况下继续进行。
解决这个问题的一个简单方法就是生成一个本地 SSH 密钥,然后将你的公钥放入你的 GitHub 账户。这样,当你从 GitHub 克隆到你的本地机器时,你就不会面临任何速率限制,也不需要设置 API 密钥。
为了在 Linux/Mac OS X 机器上生成 SSH 密钥,你可以使用ssh-keygen
命令,它将创建一个你可以用于 SSH 身份验证的公钥和私钥,包括 Github 或 BitBucket。这些密钥(通常)将存储在~/.ssh
目录中,注意波浪号(~
代表你的主目录)。因此,为了将你的密钥打印到你的终端窗口中,运行cat ~/.ssh/id_rsa.pub
命令。注意.pub
后缀表示id_rsa.pub
是你可以公开分享的公钥。你不应该分享你的私钥,通常只命名为id_rsa
。在 Windows 上,你可以使用一个名为PuttyGen的 GUI 工具来生成公钥和私钥。
一旦您获得了公钥和私钥,您可以简单地将它们放在 GitHub 上,方法是访问 GitHub 网站,转到设置菜单中的 SSH 密钥页面,粘贴您的密钥,然后保存。
对于后续更新,composer update
将根据composer.json
中定义的所有依赖项的最新版本进行更新。如果您不想这样做,还有另一种选择;运行 Composer dump-autoload
将仅重新生成需要包含在项目中的 PSR-0/PSR-4 类的列表(例如,您添加、删除或重命名了一些类)。
Composer 还支持私有存储库,允许您有效地管理跨多个项目的代码重用。另一个关键好处是 Composer 会自动生成一个锁定文件,您可以将其与项目一起提交。这使您能够有效地管理在特定时间点安装的依赖项的确切版本。
Composer 使管理第三方依赖项变得简单而有效。一些关键库已经通过 Composer 可用,例如 PHPUnit,但还有一些其他很棒的库可以让您的生活更轻松。在 Composer 上,我最喜欢的两个数据库库是 Eloquent(来自 Laravel 的数据库 ORM 系统,您可以在illuminate
/database
找到)和 Phinx(一个数据库迁移/填充系统,您可以在robmorgan
/phinx
找到)。除此之外,还有一些来自 Packergist 的各种 API 的 SDK 可用(Google 发布了一些其 SDK,还有一些更具体的 SDK,例如用于从您的 PHP 应用程序发送短信的 Twilio SDK)。
Composer 允许您为特定环境指定依赖项;假设您只想在开发环境中引入 PHPUnit...那就没问题!
上帝对象
上帝对象是糟糕的软件设计和糟糕的对象导向的诱人结果。
本质上,上帝对象是一个具有太多方法或太多属性的对象;本质上,它是一个知识过多或做得过多的类。上帝对象很快就会与应用程序中的许多其他代码紧密耦合。
那么这到底有什么问题呢?简而言之,当您的一小段代码与每一小段其他代码都紧密联系在一起时,您很快就会发现维护成为一场灾难。如果您为上帝对象中的一个用例调整了方法的逻辑,您可能会发现它对另一个元素产生了意想不到的后果。
在计算机科学中,采用分而治之的策略通常是一个好主意。通常,大问题只是一系列小问题。通过解决这一系列小问题,您可以迅速解决整体问题。对象通常应该是自包含的;它们只应该了解自己的问题,并且只应该解决一组问题,即自己的问题。任何与此目标无关的东西都不应该属于该类。
可以说,与物理对象相关的对象应该被实例化,而与物理对象无关的对象应该是抽象类。
上帝对象作为反模式的反面是在开发嵌入式系统时。嵌入式系统用于处理从计算器到 LED 标识的任何数据;它们是基本上是自包含计算机且成本相当低的小芯片。在这种用例中,由于计算能力受限,您经常会发现编程优雅和可维护性变得边缘化。轻微的性能提升和控制的集中化可能更重要,这意味着使用上帝对象可能是合理的。幸运的是,PHP 极少用于编程嵌入式系统,因此您极不可能陷入这种特殊情况。
处理这些类的最有效方法是手动将它们拆分为单独的类。
另一个反模式,称为害怕添加类,也可能在其中发挥作用,以及未能加以缓解。这是开发人员不愿意创建必要的类。
所以,这是一个 God 类的例子:
<?php
class God
{
public function getTime(): int
{
return time();
}
public function getYesterdayDate(): string
{
return date("F j, Y", time() - 60 * 60 * 24);
}
public function getDaysInMonth(): int
{
return cal_days_in_month(CAL_GREGORIAN, date('m'), date('Y'));
}
public function isCacheWritable(): bool
{
return is_writable(CACHE_FILE);
}
public function writeToCache($data): bool
{
return (file_put_contents(CACHE_FILE, $data) !== false);
}
public function whatIsThisClass(): string
{
return "Pure technical debt in the form of a God Class.";
}
}
因此,正如你所看到的,在这个类中,我们基本上结合了许多不相关的方法。为了解决这个问题,我们可以将这个类分成两个子类,一个是Watch
类,另一个是CacheManager
类。
这是Watch
类;这个类只是用来以各种格式显示时间:
<?php
class Watch
{
public function getTime(): int
{
return time();
}
public function getYesterdayDate(): string
{
return date("F j, Y", time() - 60 * 60 * 24);
}
public function getDaysInMonth(): int
{
return cal_days_in_month(CAL_GREGORIAN, date('m'), date('Y'));
}
}
这是CacheManager
类;这个类将所有缓存功能分离出来,因此它与Watch
类完全分离:
<?php
class CacheManager
{
public function isCacheWritable(): bool
{
return is_writable(CACHE_FILE);
}
public function writeToCache($data): bool
{
return (file_put_contents(CACHE_FILE, $data) !== false);
}
}
PHP 源中的环境变量
经常你会在 GitHub 上遇到一个项目,你会注意到原始开发人员留下了一个包含(在最好的情况下)无用的数据库信息或(在最坏的情况下)非常重要的 API 密钥的config.php
文件。
当这些文件不小心被版本化时,它们通常会被塞进一个.gitignore
文件中,并附上一个示例文件供开发人员根据需要修改。一个这样做的平台的例子是 WordPress。
有一些小的改进,比如将核心配置放在一个 XML 文件中,这个文件被埋在一些不相关的配置中。
我发现在 PHP 中管理环境变量通常有两种好方法。第一种方法是将它们放在root
文件夹中的一个文件中,格式可以是 YML,并根据需要读取这些变量。
第二种方法,我个人更喜欢的方法,是一个名为dotenv
的库实现的方法。基本上,发生的情况是创建一个.env
文件并将其放在项目的房间里。为了从这个文件中读取配置,你只需要调用env()
函数。然后,你可以将这个文件添加到你的.gitignore
文件中,这样当你从开发环境推送并拉到各种其他服务器配置时,这个过程会变得更容易。除此之外,你还可以在 Web 服务器级别指定环境变量,从而确保额外的安全级别,也使管理变得更容易。
所以,例如,如果我的.env
文件有一个DB_HOST
属性,那么我可以使用env('DB_HOST');
来访问它。
如果你选择了dotenv
的路线,请确保你的.env
文件不会从文档根目录公开可见。要么将它放在公共 HTTP 目录之外(例如,在上一级),要么在 Web 服务器级别限制对它的访问(例如,限制权限,或者如果你使用 Apache,使用你的.htaccess
文件限制对它的访问)。
在撰写本文时,你可以通过简单运行以下命令来要求这个库:
**composer require vlucas/phpdotenv**
软代码也经常是一个反模式,通过使用配置文件来采用。这是你开始将业务逻辑放在配置文件中而不是源代码中;因此,值得提醒自己要考虑什么时候真正需要配置导向。
单例(以及为什么你应该使用依赖注入)
单例是只能被实例化一次的类。在一个应用程序中,你实际上只能有一个Singleton
类的对象。如果你以前从未听说过单例,你可能会跳起来想“是的!我有一百万个用例可以用这个!”好吧,请不要。单例只是糟糕透了,可以有效地避免使用。
因此,在 PHP 中,Singleton
类看起来像这样:
<?php
class Singleton
{
private static $instance;
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
protected function __construct()
{
}
private function __clone()
{
}
private function __wakeup()
{
}
}
因此,以下是应该避免这样做的原因:
-
它们本质上是紧密耦合的,这意味着它们很难进行测试,例如使用单元测试。它们甚至在应用程序的生命周期中保持它们的状态。
-
它们通过控制自己的创建和生命周期来违反单一责任原则。
-
从根本上讲,这会导致你将应用程序的依赖关系隐藏在一个
global
实例中。你再也不能有效地跟踪你的代码中的依赖关系,因为你无法跟踪它们被注入为函数参数的位置。如果需要分析依赖链,这将使其变得无效。
也就是说,有些人认为它们可以是资源争用的有效解决方案(在这种情况下,你只需要一个资源的单个实例,并且需要管理该单个资源)。
依赖注入
依赖注入是对单例模式的解药。所以,假设你有一个名为Transaction
的类。作为类的构造函数,它接受名为$creditCardNumber
和$clientID
的参数,因此我们可以构造对象如下:
**$order = new Transaction('1234 5678 9012 3456', 26);**
使用依赖注入,我们将传入$creditCard
和$client
的对象,它们将是信用卡和客户的类的实例。如果你使用 ORM,这可能是一个数据库模型类:
**$order = new Transaction($clientCreditCard, $client);**
数据库作为 IPC
在写作时,我目前正在大西洋上空,从伦敦飞往旧金山,这可能是一件好事,因为这意味着我之前与之合作过的一些开发人员已经无法接触到我的颈部。
让我为你澄清一下;你的数据库不是一个消息队列系统。你不要用它来安排作业或排队等待完成的任务。如果你需要做这样的事情,使用一个队列系统。你的数据库是用来存储数据的...提示就在名字里;不要把临时消息塞进去。
有很多原因说明这是一个坏主意。一个主要问题是,在数据库中,没有真正的方法来不执行一个策略,以确保不会发生双重读取,这是通过利用行锁来实现的。这反过来导致进程(无论是传入还是传出)被阻塞,这又导致处理只能以串行方式进行。
此外,为了检查是否有工作要做,你最终基本上要计算数据库中的数据行数,看是否有工作要做;你要持续进行这个操作。MySQL 不支持推送通知;不像 PostgreSQL 那样有NOTIFY
命令来配合LISTEN
通道。
还要注意的是,当你将作业队列与存储真实数据的数据库表合并时,每次完成作业并更新标志时,都会使缓存失效,从而使 MySQL 变得更慢。
简而言之,这会导致你的数据库性能变差,并可能迫使它将关键消息放缓到停滞状态。你必须小心,不要让这个功能悄悄地将你的数据库变成一个作业队列;相反,只使用数据库来存储数据,并在扩展数据库时牢记这一点。
RabbitMQ 提供了一个带有一些出色的 PHP SDK 的开源队列系统。
自增数据库 ID
数据库自增是我非常沮丧的事情;几乎每个 PHP/MySQL 初学者教程都教人们这样做,但你真的不应该这样做。
我有尝试对自增数据库 ID 进行分片的经验,这很混乱。假设你将数据库分片,使数据集分布在两个数据库服务器上...你怎么能指望有人来扩展自增 ID 呢?
MySQL 现在甚至提供了 UUID 函数,允许你生成具有强熵的良好 ID,这意味着它在int
数据类型的表上也具有更高的理论限制。
为了使用 UUID 函数,数据库表理想上应该是 CHAR(20)。
模拟服务的 Cronjob
这是我个人的憎恶。开发人员需要一个无限运行的服务,所以他们只需启用一个永不结束的 cronjob,或者只需设置一个运行非常频繁的 cronjob(比如每隔几秒运行一次)。
cronjob 是在预定时间运行的计划任务。这不仅从架构的角度看很混乱,而且扩展性很差,监控起来也很糟糕。
一个不断处理的任务应该被视为守护进程,而不是基于 cronjob 运行的东西。
Monit是 Linux 系统中的一个工具,允许您模拟服务。
您可以使用apt-get
命令安装 Monit:
**sudo apt-get install monit**
安装 Monit 后,您可以将进程添加到其配置文件中:
**sudo nano /etc/monit/monitrc**
然后可以通过运行monit
命令来启动 Monit。它还有一个status
命令,因此您可以验证它是否仍在运行:
**monitmonit status**
您可以在www.mmonit.com
了解更多关于 Monit 的信息,并了解如何配置它。对于每个专注于 DevOps 的开发人员来说,这是一个非常有价值的工具。
软件代替架构
通常,开发人员会试图在软件开发层面纠正系统的架构问题。虽然这有用,但我非常赞成在不必要的情况下避免这种做法。将问题从软件架构层移至基础架构层具有其优势。
例如,假设您需要代理特定 URL 端点的请求到另一台服务器。我认为最好在 Web 服务器级别完成这项工作,而不是编写一个 PHP 代理脚本。Apache 和 Nginx 都可以处理反向代理,但编写一个库来做这个可能意味着您会遇到一些未知的问题。您有没有考虑过如何处理HTTP PUT
/DELETE
请求?错误处理呢?假设您的库很完美,性能如何?一个 PHP 代理脚本真的比使用低级系统工程语言编写的 Web 服务器级别代理更快吗?在 Web 服务器配置中写一两行肯定比在 PHP 中编写整个代理脚本更容易实现。
以下是在 VirtualHost 中创建代理的示例。以下配置作为 Apache VirtualHost 将允许您将test.local/api
中的所有内容重定向到api.local
(在 Nginx 中更容易):
<VirtualHost *:80>
ServerName test.local
DocumentRoot /var/www/html/
ProxyPass /api http://api.local
ProxyPassReverse /api http://api.local
</VirtualHost>
这比在 PHP 库中维护成千上万行代码来模拟 ProxyPass Apache 模块中已经可用的功能要容易得多。
我听说过对微服务的批评,认为它们试图将问题从软件开发层移至基础架构层,但我们真的在说这总是一件坏事吗?
是的,软件开发人员有兴趣在软件开发层面做事情,但通常值得让自己了解一下链条上更高层面可用的功能,并看看是否可以纠正您遇到的任何问题。
以奥卡姆剃刀的观点来思考:最简单的解决方案通常是最好的,因为它的字面意思是“不应该使用比必要更多的东西”。
界面膨胀
我遇到过多次人们认为他们在进行出色的架构,但结果证明他们的努力是适得其反。界面膨胀是这种情况的常见后果。
有一次,当我与一个 Scrum Master 讨论在 PHP 中进行多态时接口的重要性时,他告诉我他曾在一个环境中工作过,那里有一个工程师花了几个月时间开发接口,并认为自己在进行出色的架构工作。不幸的是,事实证明他并没有做出出色的基础架构工作,他实际上是在实现界面膨胀。
界面膨胀,正如其名,是指界面过度膨胀。界面可能膨胀到几乎不可能以其他方式实现类。
接口应该节俭使用;如果类只会被实现一次(实际上,没有人永远不需要修改这样的代码),那么你真的需要一个接口吗?如果需要,你可能要考虑在这种情况下避免使用接口。
接口不应该被用作测试单元功能的手段。在这种情况下,你真的应该使用单元测试,例如通过 PHPUnit。即使如此,单元测试应该测试一个单元的功能,而不是用作确保没有人编辑你的代码的工具。
因此,让我给你举一个接口膨胀的实现。让我们看看 Pheanstalk 开源库中的Pheanstalk
接口类(注意我已经删除了注释以使其更易读):
<?php
namespace Pheanstalk;
interface PheanstalkInterface
{
const DEFAULT_PORT = 11300;
const DEFAULT_DELAY = 0;
const DEFAULT_PRIORITY = 1024;
const DEFAULT_TTR = 60;
const DEFAULT_TUBE = 'default';
public function setConnection(Connection $connection);
public function getConnection();
public function bury($job, $priority = self::DEFAULT_PRIORITY);
public function delete($job);
public function ignore($tube);
public function kick($max);
public function kickJob($job);
public function listTubes();
public function listTubesWatched($askServer = false);
public function listTubeUsed($askServer = false);
public function pauseTube($tube, $delay);
public function resumeTube($tube);
public function peek($jobId);
public function peekReady($tube = null);
public function peekDelayed($tube = null);
public function peekBuried($tube = null);
public function put($data, $priority = self::DEFAULT_PRIORITY, $delay = self::DEFAULT_DELAY, $ttr = self::DEFAULT_TTR);
public function putInTube($tube, $data, $priority = self::DEFAULT_PRIORITY, $delay = self::DEFAULT_DELAY, $ttr = self::DEFAULT_TTR);
public function release($job, $priority = self::DEFAULT_PRIORITY, $delay = self::DEFAULT_DELAY);
public function reserve($timeout = null);
public function reserveFromTube($tube, $timeout = null);
public function statsJob($job);
public function statsTube($tube);
public function stats();
public function touch($job);
public function useTube($tube);
public function watch($tube);
public function watchOnly($tube);
}
呸!注意即使常量也被放在了实现中,这可能是你真正想要更改的唯一事物。显然,这是一个只能以一种方式实现的类的接口,使接口变得无用。
在编写面向对象的代码时,接口提供了很高程度的结构;一旦实现,它们作为保证,确保了实现它的类中的方法已经被实现。
然而,像大多数好事一样,它可能是一把双刃剑。有人曾经用一个极其天真的论点反对架构设计;他引用了他以前的一位同事,后者花了数月时间仅仅编写了非常详细的接口,并认为这是很好的架构。事实上,他是在制造接口膨胀。
接口不应该是强制实现的一种方式;事实上,有一些接口的例子导致某人面临的问题是永远无法以其他方式将接口实现到类中。
接口不应包含数千个引用类内部操作的方法。它们应该是轻量级的,并被视为一种保证,当查询某些内容时,它一定存在。
有一个反模式被称为瑞士军刀(或厨房水槽),围绕着人们试图设计接口以适应类的每种可能的用例的想法。这可能会导致调试、文档编制和维护困难。
本末倒置
像大多数开发人员一样,我偶尔会对一些项目管理策略感到困惑;本末倒置也不例外。
本末倒置是一个反模式,即被设计出来却永远不需要被构建的功能,从而浪费时间。这种情况让我感到恼火的是在技术会议上讨论长期技术计划时,项目经理会讨论一个功能,然后立即要求提供这个功能如何实现的技术细节。
首先,重要的是要注意,优秀的开发人员应该离开并有研究时间来提出解决方案。开发人员只有通过研究他们打算的解决方案,与开发团队一起讨论,在线查找其他人面临类似问题的情况,然后提出一个统一的、良好架构的解决方案,才能变得更加强大。
我曾在伦敦的首届领导开发者大会上发言,有一句话让我印象深刻。这句话源自非洲谚语,但在软件工程环境中尤为真实:
“如果你想走快,就一个人走。如果你想走远,就一起走。”
我曾经与各种公司的董事总经理和首席执行官交谈过,他们喜欢在董事会上拥有各种不同性格的人。首席财务官(CFO)可能是一个无情的完美主义者,只有在所有数字都完美无缺时才感到满意,而首席运营官可能在按时交付方面是一个强硬的实用主义者。在开发团队中也可能是如此;拥有广泛的专业知识和性格的输入,提出经过激烈讨论的想法,以得出一个全面的解决方案,对于需要做出大决策的情况是有益的,一个单独的开发人员无法期望做出决策。是的,你可能需要一个过滤器,甚至说只有开发团队的一小部分可能与某个特定决策相关,但总的来说,你的开发人员需要资源和时间来做出架构决策。此外,做出这种架构决策的最佳地点是在最相关的时候,当有必要做出这些决策时。
地平派是指相信地球是一个平坦圆盘的人。当他们面对重力概念时,他们宣称重力不存在,并声称这个平坦的地球实际上只是以每秒 9.8 米的速度在太空中上升。当面对更多的科学理论时,他们反而创造出自己的不合逻辑的物理宇宙观。当然,这样的理论是荒谬的。我在这里要说的是,你应该基于扎实的计算机科学(例如已发表的 RFC)来做出决策,而不是根据临时基础上的自己的计算机科学。
开发和运营的分离
我曾经遇到过开发环境,开发人员明令禁止进行任何操作,传统的开发结构在 21 世纪的网络环境中受到了严重打击。有着固定的工作角色;你要么是开发人员,要么是负责托管。尽管两个部门有着明显的共同命运,但它们有着独立的预算。
这种设置的结果是开发人员和运营技术人员从未共享知识。通过结合开发和运营(如果你愿意,就叫做 DevOps),不仅可以通过共享知识库有效提高工作质量,而且通过赋予开发人员更多的权力,还可以提高效率。
在我提到的例子中,当公司服务器上托管的网站被黑客入侵或破坏时,所有运营部门所做的就是从备份中恢复。将开发工作整合到这一过程中不仅导致了漏洞的修补,还采取了有效措施来纠正这些问题(无论是暴力插件还是网络应用防火墙)。
过分分离的开发责任
过分明显地分割开发责任可能对团队有害。
有些分离是必要的。例如,与物联网(IoT)平台合作的团队不能指望他们既能保持强大的电子工程知识,又能保持强大的前端网页开发知识。也就是说,开发人员应该期望学习他们遇到的其他技能,并且可以通过鼓励知识共享来帮助他们。拥有多学科团队成员并不是商业劣势,事实上这是一个优势。
错误抑制运算符
PHP 中的错误抑制运算符确实是一个非常危险的工具。只需在语句前面加上一个@
符号,就可以抑制由此产生的任何错误,包括导致脚本停止执行的致命错误。
不幸的是,目前在 PHP 中还不能废弃这一点;与 PHP 内部组的成员交谈后得知,首先需要做大量的先决工作,因为一些 PHP 函数没有伴随的错误函数来产生在执行 PHP 脚本时的错误。因此,唯一的方法是捕获在特定函数操作期间抛出的非致命错误,这样就不会停止脚本的执行。
不幸的是,PHP 核心本身包含相当多的技术债务。不幸的是,一个优秀的 PHP 开发人员应该擅长发现 PHP 核心中的技术债务。事实上,Facebook 试图通过自己重写 PHP 核心并称其为Hak来规避这个问题;我将让你决定是否应该考虑采用它。
在 Go 中我非常喜欢的一个特性(这是 Google 编写的一种系统语言)是你可以进行多返回类型(例如,你可以从一个函数返回两个值)。这样做的额外好处是,你可以简单地在一个函数调用中返回任何错误,而不是需要一个返回错误消息的伴随函数。
我在 Go 中也喜欢的一点是所有警告都被视为错误。你赋值给一个变量,然后不使用它?程序将无法运行(除非你将变量赋值给下划线_
,这是一个空赋值运算符,意味着变量不会被存储在任何地方)。将警告视为错误的结果是,当开发人员遇到错误时,他们知道这是严重的。
所以是的,PHP 可以从诸如 Go 之类的语言中学到很多东西,但基本上,很明显 PHP 核心已经需要做很多工作,而且除此之外,PHP 社区可能需要进行文化转变,更加开放,少一些政治色彩。PHP RFC: 采用行为准则提出 PHP 应该采用行为准则。不用说,如果以某种形式采用,PHP 社区应该会受益。
回到手头的问题,应该避免使用错误抑制运算符,除非绝对必要,以便使开发人员更容易进行调试。
盲目信任
我大约 11 岁的时候,在一节物理课上,我们只有有限数量的量角器,我们慢慢地把它们传递下去,以便画出一个角度。作为一个年轻时的狡猾捷径者,我决定不等待,而是直接复制了别人画的图。当时我的物理老师惊呆了,大声喊道:“不行!物理是关于精确的!”
他说得有道理,这在编程世界中也是非常真实的。
为了避免盲目信任,你应该注意以下错误:
-
未检查返回类型
-
未检查你的数据模型
-
假设你的数据库中的数据是正确的,或者是你期望的格式
让我们把这个问题提升到更极端的程度;看看这段代码:
<?php
$isAdmin = false;
extract($_GET);
if ($isAdmin === true) {
echo "Hey ".$name."; here, have some secret information!";
}
在上面的代码中,有两个关键错误。第一个错误是我们直接提取GET
变量;我们将远程定义的变量导入到当前符号表中,有效地允许任何人覆盖在提取之前定义的任何变量。
此外,显然存在 XSS 漏洞,因为我们在返回GET
变量时没有对其进行消毒处理。
所以我们可以这样改进:
<?php
$isAdmin = false;
if ($isAdmin === true) {
echo "Hey ".htmlspecialchars($_GET['name'])."; here, have some secret information!";
}
顺序耦合
顺序耦合是指创建一个类,该类具有必须按特定顺序调用的方法。以init
、begin
或start
开头的方法名称可能表明这种行为;根据上下文,这可能表明一种反模式。有时,工程师使用汽车来解释抽象概念,在这里我也会这样做。
例如,看下面的类:
<?php
class BadCar
{
private $started = false;
private $speed = 0;
private $topSpeed = 125;
/**
* Starts car.
* @return bool
*/
public function startCar(): bool
{
$this->started = true;
return $this->started;
}
/**
* Changes speed, increments by 1 if $accelerate is true, else decrease by 1\.
* @param $accelerate
* @return bool
* @throws Exception
*/
public function changeSpeed(bool $accelerate): bool
{
if ($this->started !== true) {
throw new Exception('Car not started.');
}
if ($accelerate == true) {
if ($this->speed > $this->topSpeed) {
return false;
} else {
$this->speed++;
return true;
}
} else {
if ($this->speed <= 0) {
return false;
} else {
$this->speed--;
return true;
}
}
}
/**
* Stops car.
* @return bool
* @throws Exception
*/
public function stopCar(): bool
{
if ($this->started !== true) {
throw new Exception('Car not started.');
}
$this->started = false;
return true;
}
}
正如您可能注意到的,我们必须在使用其他函数之前运行startCar
函数,否则会抛出异常。实际上,如果您尝试加速未启动的汽车,它不应该做任何事情,但是为了论证的目的,我已经更改了它,以便汽车首先会启动。在停止汽车的下一个示例中,我已更改了类,以便如果您尝试在汽车未运行时停止汽车,该方法将返回false
:
<?php
class GoodCar
{
private $started = false;
private $speed = 0;
private $topSpeed = 125;
/**
* Starts car.
* @return bool
*/
public function startCar(): bool
{
$this->started = true;
return $this->started;
}
/**
* Changes speed, increments by 1 if $accelerate is true, else decrease by 1\.
* @param bool $accelerate
* @return bool
*/
public function changeSpeed(bool $accelerate): bool
{
if ($this->started !== true) {
$this->startCar();
}
if ($accelerate == true) {
if ($this->speed > $this->topSpeed) {
return false;
} else {
$this->speed++;
return true;
}
} else {
if ($this->speed <= 0) {
return false;
} else {
$this->speed--;
return true;
}
}
}
/**
* Stops car.
* @return bool
*/
public function stopCar(): bool
{
if ($this->started !== true) {
return false;
}
$this->started = false;
return true;
}
}
大改写
开发人员的一个诱惑是重写整个代码库。您需要权衡利弊,是的,阅读现有代码通常比编写新代码更困难;但请记住,重写需要时间,对您的业务可能造成巨大成本。
请记住,任何项目的技术债务总和永远不会超过从头开始启动项目。
Maiz Lulkin 在一篇博客文章中写道:
“大改写的问题在于它们是对文化问题的技术解决方案。”
大改写非常低效,特别是当您无法保证开发人员现在会更好时。在截止日期内设计新系统并迁移数据可能是一项艰巨的任务。
此外,部署大改写可能会带来巨大问题;将这样的更改部署到应用程序的整个代码库可能是致命的。尝试定期在频繁的间隔内部署代码。尝试一次更改一件事。
您现有的软件就是您现有的规范。通过进行重写,您正在基于遗留代码构建代码。
幸运的是,还有一种替代方法;在周期中快速改进您当前的代码库。您可以采取三个主要步骤来改进您的代码库:
-
测试(单元测试、行为测试等)
-
服务拆分
-
完美的分阶段迁移
本书有一章专门讲述重构以及我们如何改变遗留代码的设计。
自动化测试
您需要测试;是的,编写自动化测试可能会很慢,但对于确保重写或重构时不会出现问题至关重要。
测试和开发发生在尽可能接近生产环境的环境中也至关重要。Web 服务器软件或数据库权限的微小变化可能会产生灾难性后果。
使用自动化部署系统,如 Vagrant 与 Puppet 或 Docker,可能是一个很好的解决方案。
在使用 PHPUnit 和 Composer 进行单元测试时,您可以将其包含在composer.json
文件中以引入:
{
"autoload": {
"psr-4": {
"IcyApril\\Example": "src/"
}
},
"require": {
"illuminate/database": "*",
**"phpunit/phpunit": "*",**
"robmorgan/phinx": "*"
}
}
除此之外,phpunit.xml
文件也可能很有用,这样 PHPUnit 就知道测试在哪里,还知道 Composer 自动加载器在哪里(这样它就可以继续引入类):
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true" bootstrap="./vendor/autoload.php">
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
然后,您可以像在 PHPUnit 中一样编写测试:
<?php
class App extends PHPUnit_Framework_TestCase
{
public function testApp()
{
$this->assertTrue(true);
}
}
当然,您还可以在需要时将 PHP 类引入自动加载器,从而获得额外的好处。
并非所有测试都需要是单元测试。编写外部测试脚本来测试 API 也可能很有益。一个名为Selenium(www.seleniumhq.org
)的工具甚至可以帮助您进行浏览器自动化。
服务拆分
将您的单体应用程序拆分为小的独立松耦合服务是减少技术债务的好方法。
具有技术债务的大型单体应用程序可能很难处理,因为技术债务根植于应用程序的核心。在这样不稳定的基础上构建可能很难以后拆分。然而,有一个解决方案;通过构建独立服务的新功能,您可以有效地在稳定的基础上构建新的核心,与旧的脆弱基础设施分道扬镳。然后,您可以使用 RESTful 结构与旧的单体和这样的新服务进行互联。
这种结构允许您在迁移到新的微服务架构的同时继续开发新功能。
马丁·福勒提出了一种称为分支抽象的系统,它允许您逐渐对系统进行大规模的更改,同时可以继续发布。
第一步是捕捉客户端代码的一个部分与其供应商之间的交互;然后我们可以更改代码的这一部分,使其通过一个抽象层进行相互通信。
然后我们对与供应商的交互进行同样的操作。在这样做的同时,我们有机会提高单元测试覆盖率。一旦一个供应商完全不再使用,我们可以将客户端迁移到使用该供应商,然后删除旧的供应商。
完美的分阶段迁移
将您的单体架构拆分为小型独立松耦合的服务是减少技术债务的好方法,但在这个过程中,您显然增加了对架构层面的额外负担。
在迁移数据或托管环境时,您可能会在这个过程中遇到困难。当部署过程不重复并且对每个部署都是独特的时,这一点尤为真实(例如在不使用持续集成的环境中)。
使用诸如 Docker 之类的容器技术可以让您更好地进行快速应用程序部署,使您能够更快地部署,同时增加可移植性并简化维护。一些人可能会发现其他技术,如 Vagrant,对他们更有利;不过,所有这些技术都有一个共同的因素:基础设施即代码。
基础设施即代码是通过代码管理和配置计算基础设施的过程,而不是通过交互式配置工具;然而,我们在这里追求的比这更基本。我们想要能够在事实之前分阶段和测试任何类型的迁移,并在执行迁移时重新运行确切的过程。
通过脚本化迁移,您可以像代码一样事先测试它们。您可以确保在实际服务器上完成时,与在暂存服务器上相比,减少了任何错误的机会。
除此之外,迁移后可以用于在部署中出现问题时逆向工程该过程,或者可以看到决策的理由。它实质上充当了软件部署过程的工件。
在可能的情况下,应尽可能多地提供资源;这包括部署代码的人员、组装项目的开发人员,以及在极端情况下,一个沟通人员来及时更新客户。这些资源可以快速调试问题,但至关重要的是,部署代码的个人必须主导并协调这些资源的使用,以防止分散注意力。
按照正式的预先计划的程序工作,同时也留有纠正任何问题的空间,通常可以帮助使部署尽可能无痛。
测试驱动开发
这是对测试驱动开发(TDD)的一种半开玩笑式引用。TDD 是一种软件开发策略,主要围绕使用开发测试来驱动实现以满足需求。
然而,测试驱动开发是指需求是一种捷径,软件团队开始通过错误报告来指定需求。测试驱动开发也可以称为错误驱动开发,因为它实质上导致错误报告被用来指定开发人员应该实现的操作和功能。
例如,开发人员构建了一个工具,用于将数据库中的数据导出到电子表格中。它运行得很完美,但测试人员仍然回来提出了一个问题,说产品中存在一个错误;他们说它没有包含导出到 PDF 的功能。如果这不在需求中,就不应该被提出作为错误。是的,您应该有需求。
QA 团队和测试人员的存在是为了验证软件是否符合要求。他们的存在并不是为了规定要求本身。
臃肿的优化
通常,开发人员可能会在试图过度优化他们的代码或设计工件时自相矛盾,甚至在他们的代码甚至没有执行基本功能之前,甚至在任何代码被创建之前。这可能会迅速在生产中出现问题。
在这一部分,我想专门讨论与这个主题相关的三种反模式:
-
分析瘫痪
-
无关紧要的争论
-
过早的优化
分析瘫痪
简而言之,这就是一个策略被过度分析到进展被减缓甚至在极端情况下完全停滞的地方。这样的解决方案不仅可能迅速过时,而且可能是在教育不足的情况下做出的,例如,在一个过度分析的老板试图在允许他们的开发人员实际进行一些研究之前就过于深入细节的会议上。
过度分析问题并试图提前寻求完美解决方案是行不通的;程序员应该寻求完善他们的解决方案,而不是提前想出完善的解决方案。
无关紧要的争论
基本上,这就是分析瘫痪可能发生的地方,基于一些非常琐碎的决定,例如登录页面的颜色。唯一需要修复的是不要浪费时间在琐碎的决定上。尽量避免委员会决策,因为大多数人,无论他们认为自己的设计技能有多好,在设计上都是相当无能的。
过早的优化
到目前为止,在这一部分,我主要是在批评项目经理;没有时间去批评开发人员。通常,开发人员会试图过早地优化他们的代码,而没有经过教育的数据驱动的结论来驱动何时何地进行优化。
编写清晰易读的代码是你的首要任务;然后你可以使用一些很棒的性能分析工具来确定你的瓶颈在哪里。XDebug 和 New Relic 只是一些擅长这方面的工具。
也就是说,有些情况下必须进行优化,特别是在一些长时间的计算任务上,从 O(N2)时间减少到 O(N)可能是至关重要的。也就是说,大多数简单的 PHP Web 应用程序不需要考虑这一点。
未受教育的经理综合症
你的经理有没有亲自构建过 Web 应用程序?我发现这是一个经理必须具备的相当重要的特征。就像初级医生会向曾经经历过初级医生过程的医生汇报一样,或者老师会向曾经是老师的校长汇报一样,软件开发人员应该向曾经经历过这个过程的人汇报。
显然,在小团队中(例如,一个小型设计公司在业余时间进行 Web 开发),可能并不严格需要工程经理。这在经理明白必要时需要把决策推迟给程序员的情况下运作良好。然而,一旦事情扩大,就需要有结构。
诸如谁来雇佣,谁来解雇,如何解决技术债务,哪些元素需要最关注等决定,需要由开发人员来做;此外,有时候这些决定不应该民主地做,因为这样做会导致委员会决策。在这种情况下,需要一个工程经理。
在大规模团队中,应该总有一个开发人员花费超过 90%的时间不是在编写代码。
我将进一步深入;一个 Web 工程经理不应该只有技术背景,他们应该有 Web 背景。开发 Java 应用程序开发人员可能与构建 PHP Web 应用程序完全不同,因此这样的工程经理应该通过一些 Web 经验来理解这样的学科(尽管不一定要在一种特定的语言中)。
错误的基础
SensioLabs Insight 工具被用来评估各种项目中的技术债务,并且他们评估并发布了响应。SensioLabs 在他们的博客上回应说,结果没有考虑项目的年龄或项目的大小,但无论如何,它确实显示了在使用某些框架作为基础时你所面临的技术债务:
不要误会:WordPress 是一个很棒的 CMS;是的,它在核心部分有一些怪癖,并且来自 OOP 之前的时代,但它是一个很棒的博客平台。通常情况下,你不应该去摆弄它的核心代码,所以你不需要担心它。当然,你不应该编写自己的博客平台或 CMS,但与此同时,WordPress 也不适合构建营销资产系统或保险报价生成器(是的,这两个都是我最初被要求在 WordPress 中完成的真实项目)。
简而言之:为你的任务使用最佳基础。
长方法
在某些情况下,PHP 中的方法可能过于复杂;例如,在下面的类中,我故意省略了一些有意义的注释,并且构造函数过长:
<?php
class TaxiMeter
{
const MIN_RATE = 2.50;
const secondsInDay = 60 * 60 * 24;
const MILE_RATE = 0.2;
private $timeOfDay;
private $baseRate;
private $miles;
private $dob;
/**
* TaxiMeter constructor.
* @param int $timeOfDay
* @param float $baseRate
* @param string $driverDateOfBirth
* @throws Exception
*/
public function __construct(int $timeOfDay, float $baseRate, string $driverDateOfBirth)
{
if ($timeOfDay > self::SECONDS_IN_DAY) {
throw new Exception('There can only be ' . self::SECONDS_IN_DAY . ' seconds in a day.');
} else if ($timeOfDay < 0) {
throw new Exception('Value cannot be negative.');
} else {
$this->timeOfDay = $timeOfDay;
}
if ($baseRate < self::MIN_RATE) {
throw new Exception('Base rate below minimum.');
} else {
$this->baseRate = $baseRate;
}
$dateArr = explode('/', $driverDateOfBirth);
if (count($dateArr) == 3) {
if ((checkdate($dateArr[0], $dateArr[1], $dateArr[2])) !== true) {
throw new Exception('Invalid date, please use mm/dd/yyyy.');
}
} else {
throw new Exception('Invalid date formatting, please use simple mm/dd/yyyy.');
}
$this->dob = $driverDateOfBirth;
$this->miles = 0;
}
/**
* @param int $miles
* @return bool
*/
public function addMilage(int $miles): bool
{
$this->miles += $miles;
return true;
}
/**
* @return float
* @throws Exception
*/
public function getRate(): float
{
$dynamicRate = $this->miles * self::MILE_RATE;
$totalRate = $dynamicRate + $this->baseRate;
if (is_numeric($totalRate)) {
return $totalRate;
} else {
throw new Exception('Invalid rate output.');
}
}
}
现在,让我们做两个小改变;让我们将一些方法提取到它们自己的函数中,并添加一些 DocBlock 注释。这仍然并不完美,但请注意所做的区别:
<?php
class TaxiMeter
{
const MIN_RATE = 2.50;
const SECONDS_IN_DAY = 60 * 60 * 24;
const MILE_RATE = 0.2;
private $timeOfDay;
private $baseRate;
private $miles;
/**
* TaxiMeter constructor.
* @param int $timeOfDay
* @param float $baseRate
* @param string $driverDateOfBirth
* @throws Exception
*/
public function __construct(int $timeOfDay, float $baseRate, string $driverDateOfBirth)
{
$this->setTimeOfDay($timeOfDay);
$this->setBaseRate($baseRate);
$this->validateDriverDateOfBirth($driverDateOfBirth);
$this->miles = 0;
}
/**
* Set timeOfDay class variable.
* Only providing it doesn't exceed the maximum seconds in a day (const secondsInDay) and is greater than 0\.
* @param $timeOfDay
* @return bool
* @throws Exception
*/
private function setTimeOfDay($timeOfDay): bool
{
if ($timeOfDay > self::SECONDS_IN_DAY) {
throw new Exception('There can only be ' . self::SECONDS_IN_DAY . ' seconds in a day.');
} else if ($timeOfDay < 0) {
throw new Exception('Value cannot be negative.');
} else {
$this->timeOfDay = $timeOfDay;
return true;
}
}
/**
* Sets the base rate variable providing it's over the MIN_RATE class constant.
* @param $baseRate
* @return bool
* @throws Exception
*/
private function setBaseRate($baseRate): bool
{
if ($baseRate < self::MIN_RATE) {
throw new Exception('Base rate below minimum.');
} else {
$this->baseRate = $baseRate;
return true;
}
}
/**
* Validates
* @param $driverDateOfBirth
* @return bool
* @throws Exception
*/
private function validateDriverDateOfBirth($driverDateOfBirth): bool
{
$dateArr = explode('/', $driverDateOfBirth);
if (count($dateArr) == 3) {
if ((checkdate($dateArr[0], $dateArr[1], $dateArr[2])) !== true) {
throw new Exception('Invalid date, please use mm/dd/yyyy.');
}
} else {
throw new Exception('Invalid date formatting, please use simple mm/dd/yyyy.');
}
return true;
}
/**
* Adds given milage to the milage class variable.
* @param int $miles
* @return bool
*/
public function addMilage(int $miles): bool
{
$this->miles += $miles;
return true;
}
/**
* Calculates rate of trip.
* Times class constant mileRate against the current milage in miles class variables and adds the base rate.
* @return float
* @throws Exception
*/
public function getRate(): float
{
$dynamicRate = $this->miles * self::MILE_RATE;
$totalRate = $dynamicRate + $this->baseRate;
if (is_numeric($totalRate)) {
return $totalRate;
} else {
throw new Exception('Invalid rate output.');
}
}
}
长方法是代码异味的指标;它们指的是代码中可能存在更深层问题的症状。其他例子包括重复的代码和人为的复杂性(在更简单的方法可以满足的情况下使用高级设计模式)。
魔术数字
请注意,在前面的例子中,我总是将我的常量数值变量放在类常量中,而不是直接放在代码中:
const minRate = 2.50;
const secondsInDay = 60 * 60 * 24;
const mileRate = 0.2;
我这样做的原因是为了避免一种被称为魔术数字或未命名的数值常量的反模式。使用类常量可以使代码更易于阅读、理解和维护;当然,在 PSR 标准下,它们应该以大写字母分隔下划线的形式声明。
摘要
在本章中,我们介绍了一些基本的反模式,供你避免;有些是架构上的,有些是与 PHP 相关的,还有一些是在管理层上的。
基本上,反模式会导致技术债务。所谓技术债务,指的是代码难以扩展,以至于以后更改变得更加困难。
以下是我希望你做的事情清单:
-
在开始编码之前制定计划
-
做注释,并在你的代码目的不明显时添加注释
-
确保你的代码有结构。
-
尽量避免将太多的代码放在一个方法中
-
使用 DocBlock
-
使用常识方法来处理 PHP
在本章中,我们学习了一些常见的设计问题,这些问题可能导致严重的问题;这些原则可以帮助你在以后避免重大问题。编写可扩展的代码是设计的一个重要因素。在其核心,这需要理解约束。使用适当的进程间通信策略可以帮助你的服务扩展,编写松耦合的代码可以增加代码重用和调试。最后,在部署这个令人敬畏的代码时,自动化测试和完美的分阶段迁移可以确保一切顺利进行。
在接下来的章节中,我们将继续介绍一些设计模式(大概是你一直在等待的)。
如果你有兴趣了解如何改进现有代码库的设计,你可能会发现本书中关于重构的专门章节特别有趣;但是在阅读其他设计模式之前,了解我们试图朝着重构的模式是值得的。
第三章:创建性设计模式
创建性设计模式是与四人帮经常相关的三种设计模式之一;它们是涉及对象创建机制的设计模式。
在没有控制这个过程的情况下实例化对象或基本类的创建,可能会导致设计问题,或者只是给过程增加额外的复杂性。
在这一章中,我们将涵盖以下主题:
-
软件设计过程
-
简单工厂
-
工厂方法
-
抽象工厂模式
-
延迟初始化
-
建造者模式
-
原型模式
在我们学习创建性设计模式之前,让我们稍微谈谈架构过程。
软件设计过程
软件工程知识体系是由IEEE出版的一本书,通常被称为SWEBoK,它总结了整个软件工程领域的通常被接受的知识体系。
在这本书中,软件设计的定义如下:
“定义系统或组件的架构、组件、接口和其他特征的过程”和“[该]过程的结果”。
具体来说,软件设计可以分为两个层次的层次结构:
-
架构设计,描述软件如何分割成其组成部分
-
详细设计,描述每个组件的具体细节,以描述其组件。
组件是软件解决方案的一部分,具有接口,这些接口作为所需接口(软件需要的功能)和提供的接口(软件提供给其他组件的功能)。
这两个设计过程(架构设计和详细设计)应该产生一组记录主要决策的模型和工件,并解释为什么做出了非平凡决策。将来,开发人员可以很容易地参考这些文档,以了解架构决策背后的原理,通过确保决策经过深思熟虑,并将思考过程传递下去,使代码更易于维护。
这两个过程中的第一个,架构设计,可以对整个团队来说是相当有创意和吸引力的。这个过程的结果,无论你选择如何做,都应该是一个通过接口相互连接组件的组件图。
这个过程通常可以更倾向于一般开发人员的团队,而不是虎队。 虎队通常是在特定产品知识领域的专家小组,他们在一个时间限定的环境中聚集在一起,以解决特定问题,由架构师主持。通常,特别是涉及到遗留系统时,这样的设计工作可能需要广泛的知识来提取必要的架构约束。
有了这个说法,为了防止过程变成委员会设计或群体规则,你可能想要遵循一些基本规则:让架构师主持会议,并从组件级别图开始工作,不要深入到更深层次。在会议之前制作一个组件图通常会有所帮助,并在会议中根据需要进行编辑,这有助于确保团队保持在纠正图表的轨道上,而不深入到具体的操作。
在我曾经参与的一个环境中,有一个非常详细的工程师担任工程团队的负责人;他坚持立即深入组件的细节进行架构,这会迅速使流程瓦解和无组织;他会即兴开始会议中的会议。在这些架构会议上构建组件图在保持会议秩序和确保操作事项和详细设计事项都不会过早涉及方面起到了至关重要的作用。如何和在哪里托管某些东西的操作事项通常不在软件工程的权限范围内,除非它直接影响软件的创建方式。
下一步是详细设计;这解释了组件如何构建。在这一点上可以决定使用的构造中的设计模式、类图和必要的外部资源。无论设计有多好,都将在构建级别进行一些详细设计工作,软件开发人员将需要对设计进行微小的更改,以添加更多细节或弥补架构过程中的一些疏忽。在此设计之前的过程必须简单地指定组件的足够细节,以便促进其构建,并允许开发人员不必过多考虑架构细节。开发人员应该从与代码密切相关的构件(例如详细设计)中开发代码,而不是从高级需求、设计或计划中编写代码。
顺便说一句,让我们记住,单元测试可以成为设计的一部分(例如,在使用测试驱动开发时),每个单元测试都指定一个设计元素(类、方法和特定行为)。虽然将代码逆向工程到设计构件中并不现实(尽管有人会声称是),但可以将架构表示为代码;单元测试就是实现这一目标的一种方式。
正如本书前面提到的,设计模式在软件设计中起着至关重要的作用;它们允许设计更复杂的软件部分,而无需重新发明轮子。
好了,现在是创建型设计模式。
简单工厂
什么是工厂?让我们想象一下,您订购了一辆新车;经销商将您的订单发送到工厂,工厂建造您的汽车。您的汽车以组装好的形式发送给您,您不需要关心它是如何制造的。
同样,软件工厂为您生产对象。工厂接受您的请求,使用构造函数组装对象并将它们交还给您使用。其中一种工厂模式称为简单工厂。让我向您展示它是如何工作的。
首先,我们定义一个抽象类,我们希望用其他类扩展:
<?php
abstract class Notifier
{
protected $to;
public function __construct(string $to)
{
$this->to = $to;
}
abstract public function validateTo(): bool;
abstract public function sendNotification(): string;
}
这个类用于允许我们拥有共同的方法,并定义我们希望在工厂中构建的所有类中具有的任何共同功能。我们还可以使用接口而不是抽象类来实现,而不定义任何功能。
使用这个接口,我们可以构建两个通知器,SMS
和Email
。
SMS
通知器在SMS.php
文件中如下:
<?php
class SMS extends Notifier
{
public function validateTo(): bool
{
$pattern = '/^(\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}$/';
$isPhone = preg_match($pattern, $this->to);
return $isPhone ? true : false;
}
public function sendNotification(): string
{
if ($this->validateTo() === false) {
throw new Exception("Invalid phone number.");
}
$notificationType = get_class($this);
return "This is a " . $notificationType . " to " . $this->to . ".";
}
}
同样,让我们在Email.php
文件中放出Email
通知器:
<?php
class Email extends Notifier
{
private $from;
public function __construct($to, $from)
{
parent::__construct($to);
if (isset($from)) {
$this->from = $from;
} else {
$this->from = "Anonymous";
}
}
public function validateTo(): bool
{
$isEmail = filter_var($this->to, FILTER_VALIDATE_EMAIL);
return $isEmail ? true : false;
}
public function sendNotification(): string
{
if ($this->validateTo() === false) {
throw new Exception("Invalid email address.");
}
$notificationType = get_class($this);
return "This is a " . $notificationType . " to " . $this->to . " from " . $this->from . ".";
}
}
我们可以按以下方式构建我们的工厂:
<?php
class NotifierFactory
{
public static function getNotifier($notifier, $to)
{
if (empty($notifier)) {
throw new Exception("No notifier passed.");
}
switch ($notifier) {
case 'SMS':
return new SMS($to);
break;
case 'Email':
return new Email($to, 'Junade');
break;
default:
throw new Exception("Notifier invalid.");
break;
}
}
}
虽然我们通常会使用 Composer 进行自动加载,但为了演示这种方法有多简单,我将手动包含依赖项;因此,不多说了,这是我们的演示:
<?php
require_once('Notifier.php');
require_once('NotifierFactory.php');
require_once('SMS.php');
$mobile = NotifierFactory::getNotifier("SMS", "07111111111");
echo $mobile->sendNotification();
require_once('Email.php');
$email = NotifierFactory::getNotifier("Email", "test@example.com");
echo $email->sendNotification();
我们应该得到这样的输出:
工厂方法
工厂方法与普通简单工厂的不同之处在于,我们可以拥有多个工厂。
那么为什么要这样做呢?嗯,为了理解这一点,我们必须看看开闭原则(OCP)。Bertrand Meyer 通常被认为是在他的书《面向对象的软件构造》中首次提出了“开闭原则”这个术语。Meyer 说过以下话:
“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭”
软件实体需要扩展时,应该可以在不修改其源代码的情况下进行。那些熟悉面向对象软件的SOLID(单一职责、开闭原则、里氏替换、接口隔离和依赖倒置)原则的人可能已经听说过这个原则。
工厂方法允许您将某些类组合在一起,并通过一个单独的工厂来处理它们。如果要添加另一组,只需添加另一个工厂即可。
那么,现在我们该怎么做呢?嗯,基本上我们要为每个工厂创建一个接口(或者抽象方法);然后我们将该接口实现到我们想要构建的任何其他工厂中。
让我们克隆我们的简单工厂演示;我们要做的是让我们的NotifierFactory
成为一个接口。然后我们可以重建工厂,为电子通知(电子邮件或短信)建立一个工厂,然后我们可以实现我们的接口来创建,比如说,一个邮政快递通知器工厂。
让我们从在NotifierFactory.php
文件中创建接口开始:
<?php
interface NotifierFactory
{
public static function getNotifier($notifier, $to);
}
现在让我们构建我们的ElectronicNotifierFactory
,它实现了我们的NotifierFactory
接口:
<?php
class ElectronicNotifierFactory implements NotifierFactory
{
public static function getNotifier($notifier, $to)
{
if (empty($notifier)) {
throw new Exception("No notifier passed.");
}
switch ($notifier) {
case 'SMS':
return new SMS($to);
break;
case 'Email':
return new Email($to, 'Junade');
break;
default:
throw new Exception("Notifier invalid.");
break;
}
}
}
我们现在可以重构我们的index.php
来使用我们制作的新工厂:
<?php
require_once('Notifier.php');
require_once('NotifierFactory.php');
require_once('ElectronicNotifierFactory.php');
require_once('SMS.php');
$mobile = ElectronicNotifierFactory::getNotifier("SMS", "07111111111");
echo $mobile->sendNotification();
echo "\n";
require_once('Email.php');
$email = ElectronicNotifierFactory::getNotifier("Email", "test@example.com");
echo $email->sendNotification();
现在这与以前的输出相同:
This is a SMS to 07111111111\.
This is a Email to test@example.com from Junade.
然而,现在的好处是,我们现在可以添加新类型的通知器,而无需打开工厂,所以让我们为邮政通信添加一个新的通知器:
<?php
class Post extends Notifier
{
public function validateTo(): bool
{
$address = explode(',', $this->to);
if (count($address) !== 2) {
return false;
}
return true;
}
public function sendNotification(): string
{
if ($this->validateTo() === false) {
throw new Exception("Invalid address.");
}
$notificationType = get_class($this);
return "This is a " . $notificationType . " to " . $this->to . ".";
}
}
然后我们可以引入CourierNotifierFactory
:
<?php
class CourierNotifierFactory implements NotifierFactory
{
public static function getNotifier($notifier, $to)
{
if (empty($notifier)) {
throw new Exception("No notifier passed.");
}
switch ($notifier) {
case 'Post':
return new Post($to);
break;
default:
throw new Exception("Notifier invalid.");
break;
}
}
}
最后,我们现在可以修改我们的index.php
文件以包含这种新格式:
<?php
require_once('Notifier.php');
require_once('NotifierFactory.php');
require_once('ElectronicNotifierFactory.php');
require_once('SMS.php');
$mobile = ElectronicNotifierFactory::getNotifier("SMS", "07111111111");
echo $mobile->sendNotification();
echo "\n";
require_once('Email.php');
$email = ElectronicNotifierFactory::getNotifier("Email", "test@example.com");
echo $email->sendNotification();
echo "\n";
require_once('CourierNotifierFactory.php');
require_once('Post.php');
$post = CourierNotifierFactory::getNotifier("Post", "10 Downing Street, SW1A 2AA");
echo $post->sendNotification();
index.php
文件现在产生了这个结果:
在生产中,通常会将通知器放在不同的命名空间中,并将工厂放在不同的命名空间中。
抽象工厂模式
首先,如果你在阅读本书之前做了一些背景阅读,你可能已经听说过“具体类”这个词。这是什么意思?简单来说,它是抽象类的相反;它是一个可以实例化为对象的类。
抽象工厂由以下类组成:抽象工厂、具体工厂、抽象产品、具体产品和我们的客户端。
在工厂模式中,我们生成了特定接口的实现(例如,notifier
是我们的接口,电子邮件、短信和邮件是我们的实现)。使用抽象工厂模式,我们将创建工厂接口的实现,每个工厂都知道如何创建它们的产品。
假设我们有两个玩具工厂,一个在旧金山,一个在伦敦。它们都知道如何为两个地点创建两家公司的产品。
考虑到这一点,我们的ToyFactory
接口看起来是这样的:
<?php
interface ToyFactory {
function makeMaze();
function makePuzzle();
}
现在这样做了,我们可以建立我们的旧金山玩具工厂(SFToyFactory
)作为我们的具体工厂:
<?php
class SFToyFactory implements ToyFactory
{
private $location = "San Francisco";
public function makeMaze()
{
return new Toys\SFMazeToy();
}
public function makePuzzle()
{
return new Toys\SFPuzzleToy;
}
}
现在我们可以添加我们的英国玩具工厂(UKToyFactory
):
<?php
class UKToyFactory implements ToyFactory
{
private $location = "United Kingdom";
public function makeMaze()
{
return new Toys\UKMazeToy;
}
public function makePuzzle()
{
return new Toys\UKPuzzleToy;
}
}
正如你注意到的,我们正在在 Toys 命名空间中创建各种玩具,所以现在我们可以为我们的玩具组合起来的抽象方法。让我们从我们的Toy
类开始。每个玩具最终都会扩展这个类:
<?php
namespace Toys;
abstract class Toy
{
abstract public function getSize(): int;
abstract public function getPictureName(): string;
}
现在,对于我们在开始时在ToyFactory
接口中声明的两种类型的玩具(迷宫和拼图),我们可以声明它们的抽象方法,从我们的Maze
类开始:
<?php
namespace Toys;
abstract class MazeToy extends Toy
{
private $type = "Maze";
}
现在让我们来做我们的Puzzle
类:
<?php
namespace Toys;
abstract class PuzzleToy extends Toy
{
private $type = "Puzzle";
}
现在是时候为我们的具体类做准备了,让我们从我们的旧金山实现开始。
SFMazeToy
的代码如下:
<?php
namespace Toys;
class SFMazeToy extends MazeToy
{
private $size;
private $pictureName;
public function __construct()
{
$this->size = 9;
$this->pictureName = "San Francisco Maze";
}
public function getSize(): int
{
return $this->size;
}
public function getPictureName(): string
{
return $this->pictureName;
}
}
这是SFPuzzleToy
类的代码,这是对Maze
玩具类的不同实现:
<?php
namespace Toys;
class SFPuzzleToy extends PuzzleToy
{
private $size;
private $pictureName;
public function __construct()
{
$rand = rand(1, 3);
switch ($rand) {
case 1:
$this->size = 3;
break;
case 2:
$this->size = 6;
break;
case 3:
$this->size = 9;
break;
}
$this->pictureName = "San Francisco Puzzle";
}
public
function getSize(): int
{
return $this->size;
}
public function getPictureName(): string
{
return $this->pictureName;
}
}
现在,我们可以用我们的英国工厂实现来完成这一切。
让我们先为迷宫玩具制作一个,UKMazeToy.php
:
<?php
namespace Toys;
class UKMazeToy extends Toy
{
private $size;
private $pictureName;
public function __construct()
{
$this->size = 9;
$this->pictureName = "London Maze";
}
public function getSize(): int
{
return $this->size;
}
public function getPictureName(): string
{
return $this->pictureName;
}
}
让我们也为拼图玩具制作一个类,UKPuzzleToy.php
:
<?php
namespace Toys;
class UKPuzzleToy extends PuzzleToy
{
private $size;
private $pictureName;
public function __construct()
{
$rand = rand(1, 2);
switch ($rand) {
case 1:
$this->size = 3;
break;
case 2:
$this->size = 9;
break;
}
$this->pictureName = "London Puzzle";
}
public
function getSize(): int
{
return $this->size;
}
public
function getPictureName(): string
{
return $this->pictureName;
}
}
现在,让我们把所有这些放在我们的index.php
文件中:
<?php
require_once('ToyFactory.php');
require_once('Toys/Toy.php');
require_once('Toys/MazeToy.php');
require_once('Toys/PuzzleToy.php');
require_once('SFToyFactory.php');
require_once('Toys/SFMazeToy.php');
require_once('Toys/SFPuzzleToy.php');
$sanFraciscoFactory = new SFToyFactory();
var_dump($sanFraciscoFactory->makeMaze());
echo "\n";
var_dump($sanFraciscoFactory->makePuzzle());
echo "\n";
require_once('UKToyFactory.php');
require_once('Toys/UKMazeToy.php');
require_once('Toys/UKPuzzleToy.php');
$britishToyFactory = new UKToyFactory();
var_dump($britishToyFactory->makeMaze());
echo "\n";
var_dump($britishToyFactory->makePuzzle());
echo "\n";
如果您运行给定的代码,输出应该看起来像以下截图中显示的输出:
现在,假设我们想要添加一个新的工厂,带有一组新的产品(比如纽约),我们只需添加玩具NYMazeToy
和NYPuzzleToy
,然后我们可以创建一个名为NYToyFactory
的新工厂(实现ToyFactory
接口),然后就完成了。
现在,当您需要添加新的产品类时,这个类的缺点就会显现出来;抽象工厂需要更新,这违反了接口隔离原则。因此,如果您需要添加新的产品类,它就不严格符合 SOLID 原则。
这种设计模式可能需要一些时间才能完全理解,所以一定要尝试一下源代码,看看你能做些什么。
延迟初始化
Slappy Joe's 汉堡是一家高品质的餐厅,汉堡的价格是在制作后使用的肉的准确重量来计算的。不幸的是,由于制作时间的长短,让他们在订单之前制作每一种汉堡将会对资源造成巨大的消耗。
与其为每种类型的汉堡准备好让别人点餐,当有人点餐时,汉堡会被制作(如果还没有),然后他们会被收取相应的价格。
Burger.php
类的结构如下:
<?php
class Burger
{
private $cheese;
private $chips;
private $price;
public function __construct(bool $cheese, bool $chips)
{
$this->cheese = $cheese;
$this->chips = $chips;
$this->price = rand(1, 2.50) + ($cheese ? 0.5 : 0) + ($chips ? 1 : 0);
}
public function getPrice(): int
{
return $this->price;
}
}
请注意,汉堡的价格只有在实例化后才计算,这意味着顾客在制作之前无法收费。类中的另一个函数只是返回汉堡的价格。
与直接从Burger
类实例化不同,创建了一个懒初始化类BurgerLazyLoader.php
,这个类存储了每个已制作的汉堡的实例列表;如果请求了一个尚未制作的汉堡,它将制作它。或者,如果已经存在特定配置的汉堡,那么返回该汉堡。
这是LazyLoader
类,它根据需要实例化Burger
对象:
<?php
class BurgerLazyLoader
{
private static $instances = array();
public static function getBurger(bool $cheese, bool $chips): Burger
{
if (!isset(self::$instances[$cheese . $chips])) {
self::$instances[$cheese . $chips] = new Burger($cheese, $chips);
}
return self::$instances[$cheese . $chips];
}
public static function getBurgerCount(): int
{
return count(self::$instances);
}
}
唯一添加的其他函数是getBurgerCount
函数,它返回LazyLoader
中所有实例的计数。
所以让我们把所有这些放在我们的index.php
文件中:
<?php
require_once('Burger.php');
require_once('BurgerLazyLoader.php');
$burger = BurgerLazyLoader::getBurger(true, true);
echo "Burger with cheese and fries costs: £".$burger->getPrice();
echo "\n";
echo "Instances in lazy loader: ".BurgerLazyLoader::getBurgerCount();
echo "\n";
$burger = BurgerLazyLoader::getBurger(true, false);
echo "Burger with cheese and no fries costs: £".$burger->getPrice();
echo "\n";
echo "Instances in lazy loader: ".BurgerLazyLoader::getBurgerCount();
echo "\n";
$burger = BurgerLazyLoader::getBurger(true, true);
echo "Burger with cheese and fries costs: £".$burger->getPrice();
echo "\n";
echo "Instances in lazy loader: ".BurgerLazyLoader::getBurgerCount();
echo "\n";
然后我们得到了这样的输出:
由于价格是随机的,您会注意到数字会有所不同,但带奶酪和薯条的汉堡的价格在第一次和最后一次调用时保持不变。实例只创建一次;而且,它只在需要时才创建,而不是在想要时实例化。
假设汉堡店一边,当您需要时,这种创造性模式可以发挥一些很好的作用,比如当您需要延迟从一个类构造对象时。当构造函数是一个昂贵或耗时的操作时,通常会使用这种方法。
如果一个对象还不能被使用,就会以及时的方式创建一个。
建造者模式
当我们审查工厂设计模式时,我们看到它们对实现多态性是有用的。工厂模式和建造者模式之间的关键区别在于,建造者模式仅仅旨在解决一个反模式,并不寻求执行多态性。所涉及的反模式是望远镜构造函数。
望远镜构造函数问题实质上是指构造函数包含的参数数量增长到一定程度,使用起来变得不切实际,甚至不切实际地知道参数的顺序。
假设我们有一个Pizza
类如下,它基本上包含一个构造函数和一个show
函数,详细说明了披萨的大小和配料。类看起来像这样:
<?php
class Pizza
{
private $size;
private $cheese;
private $pepperoni;
private $bacon;
public function __construct($size, $cheese, $pepperoni, $bacon)
{
$this->size = $size;
$this->cheese = $cheese;
$this->pepperoni = $pepperoni;
$this->bacon = $bacon;
}
public function show()
{
$recipe = $this->size . " inch pizza with the following toppings: ";
$recipe .= $this->cheese ? "cheese, " : "";
$recipe .= $this->pepperoni ? "pepperoni, " : "";
$recipe .= $this->bacon ? "bacon, " : "";
return $recipe;
}
}
注意构造函数包含多少参数,它实际上包含大小和每个配料。我们可以做得更好。事实上,让我们的目标是通过将所有参数添加到一个建造者对象中来构建披萨,然后我们可以使用它来创建披萨。这就是我们的目标:
$pizzaRecipe = (new PizzaBuilder(9))
->cheese(true)
->pepperoni(true)
->bacon(true)
->build();
$order = new Pizza($pizzaRecipe);
这并不难做;实际上,您甚至可能会发现这是我们在这里学到的更容易的设计模式之一。让我们首先为我们的披萨制作一个建造者,让我们将这个类命名为PizzaBuilder
:
<?php
class PizzaBuilder
{
public $size;
public $cheese;
public $pepperoni;
public $bacon;
public function __construct(int $size)
{
$this->size = $size;
}
public function cheese(bool $present): PizzaBuilder
{
$this->cheese = $present;
return $this;
}
public function pepperoni(bool $present): PizzaBuilder
{
$this->pepperoni = $present;
return $this;
}
public function bacon(bool $present): PizzaBuilder
{
$this->bacon = $present;
return $this;
}
public function build()
{
return $this;
}
}
这个类并不难理解,我们有一个设置大小的构造函数,对于我们想要添加的每个额外配料,我们可以调用相应的配料方法,并将参数设置为 true 或 false。如果没有调用配料方法,相应的配料就不会被设置为参数。
最后,我们有一个 build 方法,可以在将数据发送到Pizza
类的构造函数之前调用以运行任何最后一刻的逻辑来组织数据。话虽如此,我通常不喜欢这样做,因为如果方法需要按特定顺序执行,这可能被认为是顺序耦合,这本质上会破坏我们制作建造者来执行这样的任务的一个目的。
因此,每个配料方法也返回它们正在创建的对象,允许任何函数的输出直接注入到我们想要用它来构造的任何类中。
接下来,让我们调整我们的Pizza
类以利用这个建造者:
<?php
class Pizza
{
private $size;
private $cheese;
private $pepperoni;
private $bacon;
public function __construct(PizzaBuilder $builder)
{
$this->size = $builder->size;
$this->cheese = $builder->cheese;
$this->pepperoni = $builder->pepperoni;
$this->bacon = $builder->bacon;
}
public function show()
{
$recipe = $this->size . " inch pizza with the following toppings: ";
$recipe .= $this->cheese ? "cheese, " : "";
$recipe .= $this->pepperoni ? "pepperoni, " : "";
$recipe .= $this->bacon ? "bacon, " : "";
return $recipe;
}
}
对于构造函数来说,这是相当简单的;我们只需在需要时访问建造者中的public
属性。
请注意,我们可以在构造函数中添加对来自建造者的数据的额外验证,尽管您也可以根据所需的逻辑类型在建造者中设置方法时添加验证。
现在我们可以把所有这些放在我们的index.php
文件中:
<?php
require_once('Pizza.php');
require_once('PizzaBuilder.php');
$pizzaRecipe = (new PizzaBuilder(9))
->cheese(true)
->pepperoni(true)
->bacon(true)
->build();
$order = new Pizza($pizzaRecipe);
echo $order->show();
我们应该得到的输出看起来像这样:
建造者设计模式非常容易采用,但在构建对象时可以节省很多麻烦。
这种方法的缺点是每个类都需要一个单独的建造者;这是对对象构建过程如此控制的代价。
在此之上,建造者设计模式允许您改变构造函数变量,并且还提供了对构造对象本身的代码进行良好封装。就像所有设计模式一样,由您决定在代码中何处最适合使用每个设计模式。
传统上,键值数组经常被用来替代建造者类。然而,建造者类可以更好地控制构建过程。
还有一件事我应该提一下;在这里,我们只是使用我们的index.php
方法引用了这些方法;通常,我们在那里运行的方法被放置在一个可以称为Director类的类中。
在此之上,您还可以考虑在您的建造者中应用接口以实现大量逻辑。
原型模式
原型设计模式允许我们有效地复制对象,同时最小化重新实例化对象的性能影响。
如果您曾经使用过 JavaScript,您可能已经听说过原型语言。在这样的语言中,您通过克隆原型对象来创建新对象;反过来,创建新对象的成本降低了。
到目前为止,我们已经广泛讨论了__construct magic
方法的使用,但我们还没有涉及__clone magic
方法。__clone magic
方法是在对象被克隆(如果可能的话)之前运行的;该方法不能直接调用,也不接受任何参数。
在使用这种设计模式时,您可能会发现使用__clone
方法很有用;也就是说,根据您的用例,您可能不需要它。
非常重要的一点是要记住,当我们克隆一个对象时,__construct
函数不会重新运行。对象已经被构造,因此 PHP 认为没有重新运行的理由,因此在使用这种设计模式时,最好避免在这里放置有意义的逻辑。
让我们首先定义一个基本的Student
类:
<?php
class Student
{
public $name;
public $year;
public $grade;
public function setName(string $name)
{
$this->name = $name;
}
public function setYear(int $year)
{
$this->year = $year;
}
public function setGrade(string $grade)
{
$this->grade = $grade;
}
}
现在让我们开始构建我们的index.php
文件,首先包括我们的Student.php
类文件:
require_once('Student.php');
然后,我们可以创建这个类的一个实例,设置各种变量,然后var_dump
对象的内容,以便我们可以调试对象内部的细节,看看它是如何工作的:
$prototypeStudent = new Student();
$prototypeStudent->setName('Dave');
$prototypeStudent->setYear(2);
$prototypeStudent->setGrade('A*');
var_dump($prototypeStudent);
此脚本的输出如下:
到目前为止,一切都很好;我们基本上声明了一个基本类并设置了各种属性。对于我们的下一个挑战,让我们克隆这个脚本。我们可以通过将以下行添加到我们的index.php
文件来实现这一点:
$theLesserChild = clone $prototypeStudent;
$theLesserChild->setName('Mike');
$theLesserChild->setGrade('B');
var_dump($theLesserChild);
这是什么样子?好吧,看一下:
看起来很简单;我们已经克隆了一个对象并成功更改了该对象的属性。我们的初始对象,原型,现在已经被克隆以构建一个新的学生。
是的,我们可以再次这样做,如下所示:
$theChildProdigy = clone $prototypeStudent;
$theChildProdigy->setName('Bob');
$theChildProdigy->setYear(3);
$theChildProdigy->setGrade('A');
但我们也可以做得更好;通过使用匿名函数,也称为闭包,我们实际上可以动态地向这个对象添加额外的方法。
让我们为我们的对象定义一个匿名函数:
$theChildProdigy->danceSkills = "Outstanding";
$theChildProdigy->dance = function (string $style) {
return "Dancing $style style.";
};
最后,让我们同时输出新克隆对象的var_dump
,但也执行我们刚刚创建的dance
函数:
var_dump($theChildProdigy);
var_dump($theChildProdigy->dance->__invoke('Pogo'));
您会注意到,实际上,我们不得不使用__invoke
魔术方法来调用匿名函数。当脚本尝试将对象作为函数调用时,将调用此方法;在类变量中调用匿名函数时,这是至关重要的。
这是因为 PHP 类属性和方法都在不同的命名空间中;为了执行在类变量中的闭包,您需要使用__invoke
;首先将其分配给一个类变量,使用call_user_func
,或者使用__call
魔术方法。
在这种情况下,我们只使用__invoke
方法。
因此,脚本的输出如下:
注意我们的函数是在最底部运行的?
因此,完成的index.php
文件看起来像这样:
<?php
require_once('Student.php');
$prototypeStudent = new Student();
$prototypeStudent->setName('Dave');
$prototypeStudent->setYear(2);
$prototypeStudent->setGrade('A*');
var_dump($prototypeStudent);
$theLesserChild = clone $prototypeStudent;
$theLesserChild->setName('Mike');
$theLesserChild->setGrade('B');
var_dump($theLesserChild);
$theChildProdigy = clone $prototypeStudent;
$theChildProdigy->setName('Bob');
$theChildProdigy->setYear(3);
$theChildProdigy->setGrade('A');
$theChildProdigy->danceSkills = "Outstanding";
$theChildProdigy->dance = function (string $style) {
return "Dancing $style style.";
};
var_dump($theChildProdigy);
var_dump($theChildProdigy->dance->__invoke('Pogo'));
这有一些很好的用例;假设您想执行事务。您可以取一个对象,克隆它,然后在所有查询成功并将克隆的对象提交到数据库中以替换原始对象。
这是一种非常有用且轻量级的方式,可以克隆一个对象,其中您知道克隆的对象需要与其父对象相同或几乎相同的内容。
总结
在本章中,我们开始学习与对象创建相关的一些关键 PHP 设计模式。我们了解了各种不同的工厂设计模式以及它们如何使您的代码更符合常见标准。我们还介绍了建造者设计模式如何帮助您避免在构造函数中使用过多参数。我们还学习了延迟实例化以及它如何帮助您的代码更加高效。最后,我们学习了如何使用原型设计模式从原型对象中复制对象。
继续设计模式,下一章我们将讨论结构设计模式。
第四章:结构设计模式
结构设计模式提供了创建类结构的不同方式;例如,这可以是我们如何使用封装来从较小的对象创建更大的对象。它们存在的目的是通过允许我们识别简单的方式来实现实体之间的关系,从而简化设计。
在上一章中,我们介绍了创造模式如何用于确定如何创建对象;而结构模式可以确定类之间的结构和关系。
在简短介绍了敏捷软件架构之后,本章将涵盖以下主题:
-
装饰者模式
-
类适配器模式
-
对象适配器模式
-
享元模式
-
组合模式
-
桥接模式
-
代理模式
-
外观模式
敏捷软件架构
许多组织正在倾向于采用敏捷形式的项目管理。这给架构师的角色带来了新的关注;事实上,一些人认为敏捷和架构是相互冲突的。敏捷宣言的最初签署者之一 Martin Fowler 和 Robert Cecil Martin 对这一观点持有强烈反对意见。事实上,福勒明确澄清了敏捷宣言虽然对大量的事先设计(例如 Prince2 中看到的类型)持敌对态度,但并不排斥事先设计本身。
计算机科学家 Allen Holub 也持有类似观点。敏捷侧重于做对用户有用的软件,而不是仅仅对销售人员有用的软件。为了使软件长期有用,它必须是可适应、可扩展和可维护的。
福勒还对软件开发团队中的架构师有了一个愿景。他指出,不可逆转的软件很可能会在以后带来最大的麻烦,这就是架构决策必须存在的地方。此外,他声称架构师的角色应该是寻求使这些决策可逆转,从而完全减轻问题。
在许多大规模软件部署中,可能会使用“我们已经到了无法回头的地步”的说法。在“无法回头”的地步之后,将部署恢复到原始状态变得不可行。软件有自己的“无法回头”的地步,当软件变得更难重写而不是简单重建时,就会成为事实。虽然软件可能不会达到这种“无法回头”的最坏情况,但随着可维护性困难的增加,会带来商业困难。
福勒还指出,在许多情况下,软件架构师甚至不检查软件是否符合其原始设计。通过与架构师进行配对编程,以及架构师审查代码更改(即拉取请求),他们可以获得理解,以便向开发人员提供反馈,并减轻进一步的技术债务。
在本书中,您可能会注意到缺少 UML;这是因为我认为这里不需要 UML。我的意思是,我们都在用 PHP 说话,对吧?不过你可能会发现 UML 在您的团队中很有用。
架构过程通常会产生可交付物;我们称这个可交付物为“工件”。在敏捷团队中,这些工件可能以渐进式方式开发,而不是事先产品,但在敏捷环境中完全可以进行架构设计。
事实上,我认为架构使在敏捷环境中工作变得更容易。当编程到接口或抽象层时,更容易替换类;在敏捷环境中,需求可能会发生变化,这意味着可能需要替换类。软件只有对最终客户有用时才有用。敏捷可以帮助实现这一点,但为了实现敏捷,您的代码必须是适应性的。拥有出色的架构对此至关重要。
当我们编写代码时,我们应该采取防御性的编码方式。然而,对手并不是敌人,而是我们自己。破坏可靠代码的最快方式之一是编辑它以使其变得脆弱。
装饰器
装饰器只是在不影响同一类的其他对象行为的情况下,为单个类添加额外功能的内容。
单一责任原则,由 Robert C. Martin(我在本章开头介绍过)简单地表述为“一个类应该只有一个改变的原因”。
该原则规定每个模块或类应该有一个单一的责任,并且该责任应该完全由该类封装。类的所有服务都应该与该责任保持一致。Martin 通过以下方式总结了这一责任:
“指定给唯一的参与者的责任,表示其对于唯一的业务任务的责任。”
通过使用装饰器设计模式,我们能够确保功能在具有独特关注领域的类之间进行划分,从而遵守单一责任原则。
让我们首先声明我们的Book
接口。这是我们期望我们的书能够产生的内容:
<?php
interface Book
{
public function __construct(string $title, string $author, string $contents);
public function getTitle(): string;
public function getAuthor(): string;
public function getContents(): string;
}
然后我们可以声明我们的EBook.php
类。这是我们将用PrintBook
类装饰的类:
<?php
class EBook implements Book
{
public $title;
public $author;
public $contents;
public function __construct(string $title, string $author, string $contents)
{
$this->title = $title;
$this->author = $author;
$this->contents = $contents;
}
public function getTitle(): string
{
return $this->contents;
}
public function getAuthor(): string
{
return $this->author;
}
public function getContents(): string
{
return $this->contents;
}
}
现在我们可以声明我们的PrintBook
类。这是我们用来装饰EBook
类的内容:
<?php
class PrintBook implements Book
{
public $eBook;
public function __construct(string $title, string $author, string $contents)
{
$this->eBook = new EBook($title, $author, $contents);
}
public function getTitle(): string
{
return $this->eBook->getTitle();
}
public function getAuthor(): string
{
return $this->eBook->getAuthor();
}
public function getContents(): string
{
return $this->eBook->getContents();
}
public function getText(): string
{
$contents = $this->eBook->getTitle() . " by " . $this->eBook->getAuthor();
$contents .= "\n";
$contents .= $this->eBook->getContents();
return $contents;
}
}
现在让我们用我们的index.php
文件来测试所有这些。
<?php
require_once('Book.php');
require_once('EBook.php');
$PHPBook = new EBook("Mastering PHP Design Patterns", "Junade Ali", "Some contents.");
require_once('PrintBook.php');
$PHPBook = new PrintBook("Mastering PHP Design Patterns", "Junade Ali", "Some contents.");
echo $PHPBook->getText();
输出如下:
Some contents. by Junade Ali
Some contents.
适配器
适配器模式有两种类型。在可能的情况下,我更偏向于对象适配器而不是类适配器;我稍后会详细解释这一点。
适配器模式允许现有的类与其不匹配的接口一起使用。它经常用于允许现有的类与其他类一起工作,而无需修改它们的源代码。
这在使用具有各自接口的第三方库的多态设置中可能非常有用。
基本上,适配器帮助两个不兼容的接口一起工作。通过将一个类的接口转换为客户端期望的接口,否则不兼容的类可以被使得一起工作。
类适配器
在类适配器中,我们使用继承来创建一个适配器。一个类(适配器)可以继承另一个类(被适配者);使用标准继承,我们能够为被适配者添加额外功能。
假设我们有一个ATM
类,在我们的ATM.php
文件中:
<?php
class ATM
{
private $balance;
public function __construct(float $balance)
{
$this->balance = $balance;
}
public function withdraw(float $amount): float
{
if ($this->reduceBalance($amount) === true) {
return $amount;
} else {
throw new Exception("Couldn't withdraw money.");
}
}
protected function reduceBalance(float $amount): bool
{
if ($amount >= $this->balance) {
return false;
}
$this->balance = ($this->balance - $amount);
return true;
}
public function getBalance(): float
{
return $this->balance;
}
}
让我们创建我们的ATMWithPhoneTopUp.php
来形成我们的适配器:
<?php
class ATMWithPhoneTopUp extends ATM
{
public function getTopUp(float $amount, int $time): string
{
if ($this->reduceBalance($amount) === true) {
return $this->generateTopUpCode($amount, $time);
} else {
throw new Exception("Couldn't withdraw money.");
}
}
private function generateTopUpCode(float $amount, int $time): string
{
return $amount . $time . rand(0, 10000);
}
}
让我们将所有这些内容包装在一个index.php
文件中:
<?php
require_once('ATM.php');
$atm = new ATM(500.00);
$atm->withdraw(50);
echo $atm->getBalance();
echo "\n";
require_once('ATMWithPhoneTopUp.php');
$adaptedATM = new ATMWithPhoneTopUp(500.00);
echo "Top-up code: " . $adaptedATM->getTopUp(50, time());
echo "\n";
echo $adaptedATM->getBalance();
现在我们已经将初始的ATM
类调整为生成充值码,我们现在可以利用这个新的充值功能。所有这些的输出如下:
450
Top-up code: 5014606939121598
450
请注意,如果我们想要适应多个被适配者,这在 PHP 中将会很困难。
在 PHP 中,多重继承是不可能的,除非你使用 Traits。在这种情况下,我们只能使一个类适应另一个类的接口。
我们不使用这种方法的另一个关键架构原因是,通常更倾向于优先使用组合而不是继承(正如复用组合原则所描述的)。
为了更详细地探讨这一原则,我们需要看看对象适配器。
对象适配器
复用组合原则规定,类应该通过它们的组合实现多态行为和代码复用。
通过应用这一原则,当类想要实现特定功能时,应该包含其他类的实例,而不是从基类或父类继承功能。
因此,四人帮提出了以下观点:
“更偏向于‘对象组合’而不是‘类继承’。”
为什么这个原则如此重要?考虑我们上一个例子,我们在那里使用了类继承;在这种情况下,我们无法保证我们的适配器是否符合我们想要的接口。如果父类暴露了我们不想要适配器的函数会怎么样?组合给了我们更多的控制。
通过组合而不是继承,我们能够更好地支持面向对象编程中如此重要的多态行为。
假设我们有一个生成保险费的类。它根据客户希望如何支付保险费提供月度保费和年度保费。通过年度支付,客户可以节省相当于半个月的金额:
<?php
class Insurance
{
private $limit;
private $excess;
public function __construct(float $limit, float $excess)
{
if ($excess >= $limit) {
throw New Exception('Excess must be less than premium.');
}
$this->limit = $limit;
$this->excess = $excess;
}
public function monthlyPremium(): float
{
return ($this->limit-$this->excess)/200;
}
public function annualPremium(): float
{
return $this->monthlyPremium()*11.5;
}
}
假设市场比较工具多态地使用诸如前面提到的类来实际上计算来自多个不同供应商的保险报价;他们使用这个接口来做到这一点:
<?php
interface MarketCompare
{
public function __construct(float $limit, float $excess);
public function getAnnualPremium();
public function getMonthlyPremium();
}
因此,我们可以使用这个接口来构建一个对象适配器,以确保我们的Insurance
类,我们的保费生成器,符合市场比较工具所期望的接口:
<?php
class InsuranceMarketCompare implements MarketCompare
{
private $premium;
public function __construct(float $limit, float $excess)
{
$this->premium = new Insurance($limit, $excess);
}
public function getAnnualPremium(): float
{
return $this->premium->annualPremium();
}
public function getMonthlyPremium(): float
{
return $this->premium->monthlyPremium();
}
}
注意类实际上是如何实例化自己的类以适应它所尝试适配的内容。
然后适配器将这个类存储在一个private
变量中。然后我们使用这个对象在private
变量中代理请求。
适配器,无论是类适配器还是对象适配器,都应该充当粘合代码。我的意思是适配器不应执行任何计算或计算,它们只是在不兼容的接口之间充当代理。
将逻辑保持在我们的粘合代码之外,并将逻辑留给我们正在适应的代码是标准做法。如果在这样做时,我们遇到单一责任原则,我们需要适应另一个类。
正如我之前提到的,在类适配器中适配多个类实际上是不可能的,所以你要么必须将这样的逻辑包装在一个 Trait 中,要么我们需要使用对象适配器,比如我们正在讨论的这个。
让我们试试这个适配器。我们将通过编写以下index.php
文件来看看我们的新类是否符合预期的接口:
<?php
require_once('Insurance.php');
$quote = new Insurance(10000, 250);
echo $quote->monthlyPremium();
echo "\n";
require_once('MarketCompare.php');
require_once('InsuranceMarketCompare.php');
$quote = new InsuranceMarketCompare(10000, 250);
echo $quote->getMonthlyPremium();
echo "\n";
echo $quote->getAnnualPremium();
输出应该看起来像这样:
48.75
48.75
560.625
与类适配器方法相比,这种方法的主要缺点是,我们必须实现公共方法,即使这些方法只是转发方法。
FlyWeight
就像在现实生活中,不是所有的对象都容易创建,有些可能会占用过多的内存。FlyWeight 设计模式可以通过尽可能与类似对象共享尽可能多的数据来帮助我们最小化内存使用。
这种设计模式在大多数 PHP 应用程序中的使用有限,但是了解它在极端有用的情况下仍然是值得的。
假设我们有一个带有draw
方法的Shape
接口:
<?php
interface Shape
{
public function draw();
}
让我们创建一个实现这个接口的Circle
类。在实现这个过程中,我们建立了设置圆的位置和半径以及绘制它(打印出这些信息)的能力。注意颜色特征是如何在类外设置的。
这有一个非常重要的原因。在我们的例子中,颜色是与状态无关的;它是圆的固有部分。然而,圆的位置和大小是与状态相关的,因此是外部的。当需要时,外部状态信息被传递给 FlyWeight 对象;然而,固有选项与 FlyWeight 的每个过程无关。当我们讨论这个工厂是如何制作的时,这将更有意义。
这是重要的信息:
-
外部:状态属于对象的外部上下文,并在使用对象时输入。
-
内在:自然属于对象的状态,因此应该是永久的、不可变的(内部)或与上下文无关的。
考虑到这一点,让我们组合一个实现我们的Shape
接口的实现。这是我们的Circle
类:
<?php
class Circle implements Shape
{
private $colour;
private $x;
private $y;
private $radius;
public function __construct(string $colour)
{
$this->colour = $colour;
}
public function setX(int $x)
{
$this->x = $x;
}
public function setY(int $y)
{
$this->y = $y;
}
public function setRadius(int $radius)
{
$this->radius = $radius;
}
public function draw()
{
echo "Drawing circle which is " . $this->colour . " at [" . $this->x . ", " . $this->y . "] of radius " . $this->radius . ".";
echo "\n";
}
}
有了这个,我们现在可以构建我们的ShapeFactory
,它实际上实现了 FlyWeight 模式。当需要时,会实例化一个具有我们选择的颜色的对象,然后将其存储以供以后使用:
<?php
class ShapeFactory
{
private $shapeMap = array();
public function getCircle(string $colour)
{
$circle = 'Circle' . '_' . $colour;
if (!isset($this->shapeMap[$circle])) {
echo "Creating a ".$colour." circle.";
echo "\n";
$this->shapeMap[$circle] = new Circle($colour);
}
return $this->shapeMap[$circle];
}
}
让我们在我们的index.php
文件中演示这是如何工作的。
为了使这个工作,我们创建100
个带有随机颜色的对象,放在随机位置:
require_once('Shape.php');
require_once('Circle.php');
require_once('ShapeFactory.php');
$colours = array('red', 'blue', 'green', 'black', 'white', 'orange');
$factory = new ShapeFactory();
for ($i = 0; $i < 100; $i++) {
$randomColour = $colours[array_rand($colours)];
$circle = $factory->getCircle($randomColour);
$circle->setX(rand(0, 100));
$circle->setY(rand(0, 100));
$circle->setRadius(100);
$circle->draw();
}
现在,让我们来看一下输出。您可以看到我们画了 100 个圆,但我们只需要实例化少量圆,因为我们正在缓存相同颜色的对象以供以后使用:
Creating a green circle.
Drawing circle which is green at [29, 26] of radius 100\.
Creating a black circle.
Drawing circle which is black at [17, 64] of radius 100\.
Drawing circle which is black at [81, 86] of radius 100\.
Drawing circle which is black at [0, 73] of radius 100\.
Creating a red circle.
Drawing circle which is red at [10, 15] of radius 100\.
Drawing circle which is red at [70, 79] of radius 100\.
Drawing circle which is red at [13, 78] of radius 100\.
Drawing circle which is green at [78, 27] of radius 100\.
Creating a blue circle.
Drawing circle which is blue at [38, 11] of radius 100\.
Creating a orange circle.
Drawing circle which is orange at [43, 57] of radius 100\.
Drawing circle which is blue at [58, 65] of radius 100\.
Drawing circle which is orange at [75, 67] of radius 100\.
Drawing circle which is green at [92, 59] of radius 100\.
Drawing circle which is blue at [53, 3] of radius 100\.
Drawing circle which is black at [14, 33] of radius 100\.
Creating a white circle.
Drawing circle which is white at [84, 46] of radius 100\.
Drawing circle which is green at [49, 61] of radius 100\.
Drawing circle which is orange at [57, 44] of radius 100\.
Drawing circle which is orange at [64, 33] of radius 100\.
Drawing circle which is white at [42, 74] of radius 100\.
Drawing circle which is green at [5, 91] of radius 100\.
Drawing circle which is white at [87, 36] of radius 100\.
Drawing circle which is red at [74, 94] of radius 100\.
Drawing circle which is black at [19, 6] of radius 100\.
Drawing circle which is orange at [70, 83] of radius 100\.
Drawing circle which is green at [74, 64] of radius 100\.
Drawing circle which is white at [89, 21] of radius 100\.
Drawing circle which is red at [25, 23] of radius 100\.
Drawing circle which is blue at [68, 96] of radius 100\.
Drawing circle which is green at [74, 6] of radius 100\.
您可能已经注意到了一些事情。我们正在存储我们正在重用的 FlyWeight 对象的缓存的方式是通过连接Circle_ 和颜色,例如Circle_green。显然,在这种情况下这是有效的,但有更好的方法;在 PHP 中,实际上可以为给定的对象获取唯一 ID。我们将在下一个模式中介绍这个。
组合
想象一个由单独歌曲和歌曲播放列表组成的音频系统。是的,播放列表由歌曲组成,但我们希望两者都被单独对待。两者都是音乐类型,都可以播放。
组合设计模式可以帮助我们;它允许我们忽略对象组合和单个对象之间的差异。它允许我们用相同或几乎相同的代码来处理两者。
让我们举个小例子;一首歌是我们叶子的例子,而播放列表是组合。Music
是我们对播放列表和歌曲的抽象;因此,我们可以称之为我们的组件。所有这些的客户端是我们的index.php
文件。
通过不区分叶节点和分支,我们的代码变得不那么复杂,因此也不那么容易出错。
让我们首先为我们的Music
定义一个接口:
<?php
interface Music
{
public function play();
}
现在让我们组合一些实现,首先是我们的Song
类:
<?php
class Song implements Music
{
public $id;
public $name;
public function __construct(string $name)
{
$this->id = uniqid();
$this->name = $name;
}
public function play()
{
printf("Playing song #%s, %s.\n", $this->id, $this->name);
}
}
现在我们可以开始组合我们的Playlist
类。在这个例子中,您可能注意到我使用一个名为spl_object_hash
的函数在歌曲数组中设置键。当处理对象数组时,这个函数绝对是一个祝福。
这个函数的作用是为每个对象返回一个唯一的哈希值,只要对象没有被销毁,无论类的属性如何改变,它都保持一致。它提供了一种稳定的方式来寻址任意对象。一旦对象被销毁,哈希值就可以被重用于其他对象。
这个函数不会对对象的内容进行哈希处理;它只是显示内部句柄和句柄表指针。这意味着如果您更改对象的属性,哈希值不会改变。也就是说,它并不保证唯一性。如果一个对象被销毁,然后立即创建一个相同类的对象,您将得到相同的哈希值,因为 PHP 将在第一个类被取消引用和销毁后重用相同的内部句柄。
这将是真的,因为 PHP 可以使用内部句柄:
var_dump(spl_object_hash(new stdClass()) === spl_object_hash(new stdClass()));
然而,这将是错误的,因为 PHP 必须创建一个新的句柄:
$object = new StdClass();
var_dump(spl_object_hash($object) === spl_object_hash(new stdClass()));
现在让我们回到我们的Playlist
类。让我们用它实现我们的Music
接口;所以,这是类:
<?php
class Playlist implements Music
{
private $songs = array();
public function addSong(Music $content): bool
{
$this->songs[spl_object_hash($content)] = $content;
return true;
}
public function removeItem(Music $content): bool
{
unset($this->songs[spl_object_hash($content)]);
return true;
}
public function play()
{
foreach ($this->songs as $content) {
$content->play();
}
}
}
现在让我们把这一切放在我们的index.php
文件中。我们在这里所做的是创建一些歌曲对象,其中一些我们将使用它们的addSong
函数分配给一个播放列表。
因为播放列表的实现方式与歌曲相同,我们甚至可以使用addSong
函数与其他播放列表一起使用(在这种情况下,最好将addSong
函数重命名为addMusic
)。
然后我们播放父播放列表。这将播放子播放列表,然后播放这些播放列表中的所有歌曲:
<?php
require_once('Music.php');
require_once('Playlist.php');
require_once('Song.php');
$songOne = new Song('Lost In Stereo');
$songTwo = new Song('Running From Lions');
$songThree = new Song('Guts');
$playlistOne = new Playlist();
$playlistTwo = new Playlist();
$playlistThree = new Playlist();
$playlistTwo->addSong($songOne);
$playlistTwo->addSong($songTwo);
$playlistThree->addSong($songThree);
$playlistOne->addSong($playlistTwo);
$playlistOne->addSong($playlistThree);
$playlistOne->play();
当我们运行这个脚本时,我们可以看到预期的输出:
Playing song #57106d5adb364, Lost In Stereo.
Playing song #57106d5adb63a, Running From Lions.
Playing song #57106d5adb654, Guts.
桥接
桥接模式可能非常简单;它有效地允许我们将抽象与实现解耦,以便两者可以独立变化。
当类经常变化时,通过桥接接口和具体类,开发人员可以更轻松地变化他们的类。
让我们提出一个通用的信使接口,具有发送某种形式消息的能力,Messenger.php
:
<?php
interface Messenger
{
public function send($body);
}
这个接口的一个具体实现是一个InstantMessenger
应用程序,InstantMessenger.php
:
<?php
class InstantMessenger implements Messenger
{
public function send($body)
{
echo "InstantMessenger: " . $body;
}
}
同样,我们可以用一个SMS
应用程序SMS.php
来做同样的事情:
<?php
class SMS implements Messenger
{
public function send($body)
{
echo "SMS: " . $body;
}
}
我们现在可以为物理设备,即发射器,创建一个接口,Transmitter.php
:
<?php
interface Transmitter
{
public function setSender(Messenger $sender);
public function send($body);
}
我们可以通过使用Device
类将实现其方法的设备与发射器解耦。Device
类将Transmitter
接口桥接到物理设备,Device.php
:
<?php
abstract class Device implements Transmitter
{
protected $sender;
public function setSender(Messenger $sender)
{
$this->sender = $sender;
}
}
所以让我们组合一个具体的类来表示手机,Phone.php
:
<?php
class Phone extends Device
{
public function send($body)
{
$body .= "\n\n Sent from a phone.";
return $this->sender->send($body);
}
}
让我们对Tablet
做同样的事情。Tablet.php
是:
<?php
class Tablet extends Device
{
public function send($body)
{
$body .= "\n\n Sent from a Tablet.";
return $this->sender->send($body);
}
}
最后,让我们把这一切都包装在一个index.php
文件中:
<?php
require_once('Transmitter.php');
require_once('Device.php');
require_once('Phone.php');
require_once('Tablet.php');
require_once('Messenger.php');
require_once('SMS.php');
require_once('InstantMessenger.php');
$phone = new Phone();
$phone->setSender(new SMS());
$phone->send("Hello there!");
这个输出如下:
SMS: Hello there!
Sent from a phone.
代理模式
代理是一个仅仅是与其他东西接口的类。它可以是任何东西的接口;从网络连接、文件、内存中的大对象,或者其他太难复制的资源。
在我们的例子中,我们将简单地创建一个简单的代理,根据代理的实例化方式转发到两个对象中的一个。
访问一个简单的代理类允许客户端从一个对象中访问猫和狗的喂食器,具体取决于它是否已被实例化。
让我们首先定义一个AnimalFeeder
的接口:
<?php
namespace IcyApril\PetShop;
interface AnimalFeeder
{
public function __construct(string $petName);
public function dropFood(int $hungerLevel, bool $water = false): string;
public function displayFood(int $hungerLevel): string;
}
然后我们可以为猫和狗定义两个动物喂食器:
<?php
namespace IcyApril\PetShop\AnimalFeeders;
use IcyApril\PetShop\AnimalFeeder;
class Cat implements AnimalFeeder
{
public function __construct(string $petName)
{
$this->petName = $petName;
}
public function dropFood(int $hungerLevel, bool $water = false): string
{
return $this->selectFood($hungerLevel) . ($water ? ' with water' : '');
}
public function displayFood(int $hungerLevel): string
{
return $this->selectFood($hungerLevel);
}
protected function selectFood(int $hungerLevel): string
{
switch ($hungerLevel) {
case 0:
return 'lamb';
break;
case 1:
return 'chicken';
break;
case 3:
return 'tuna';
break;
}
}
}
这是我们的AnimalFeeder
:
<?php
namespace IcyApril\PetShop\AnimalFeeders;
class Dog
{
public function __construct(string $petName)
{
if (strlen($petName) > 10) {
throw new \Exception('Name too long.');
}
$this->petName = $petName;
}
public function dropFood(int $hungerLevel, bool $water = false): string
{
return $this->selectFood($hungerLevel) . ($water ? ' with water' : '');
}
public function displayFood(int $hungerLevel): string
{
return $this->selectFood($hungerLevel);
}
protected function selectFood(int $hungerLevel): string
{
if ($hungerLevel == 3) {
return "chicken and vegetables";
} elseif (date('H') < 10) {
return "turkey and beef";
} else {
return "chicken and rice";
}
}
}
有了这个定义,我们现在可以创建我们的代理类,一个基本上使用构造函数来解密需要实例化的类,然后将所有函数调用重定向到这个类。为了重定向函数调用,使用__call magic
方法。
看起来像这样:
<?php
namespace IcyApril\PetShop;
class AnimalFeederProxy
{
protected $instance;
public function __construct(string $feeder, string $name)
{
$class = __NAMESPACE__ . '\\AnimalFeeders' . $feeder;
$this->instance = new $class($name);
}
public function __call($name, $arguments)
{
return call_user_func_array([$this->instance, $name], $arguments);
}
}
你可能已经注意到,我们必须在构造函数中手动创建带有命名空间的类。我们使用__NAMESPACE__ magic
常量来找到当前命名空间,然后将其连接到类所在的特定子命名空间。请注意,我们必须使用另一个\
来转义\
,以便允许我们指定命名空间,而不让 PHP 将\
解释为转义字符。
让我们构建我们的index.php
文件,并利用代理类来构建对象:
<?php
require_once('AnimalFeeder.php');
require_once('AnimalFeederProxy.php');
require_once('AnimalFeeders/Cat.php');
$felix = new \IcyApril\PetShop\AnimalFeederProxy('Cat', 'Felix');
echo $felix->displayFood(1);
echo "\n";
echo $felix->dropFood(1, true);
echo "\n";
require_once('AnimalFeeders/Dog.php');
$brian = new \IcyApril\PetShop\AnimalFeederProxy('Dog', 'Brian');
echo $brian->displayFood(1);
echo "\n";
echo $brian->dropFood(1, true);
输出如下:
chicken
chicken with water
turkey and beef
turkey and beef with water
那么你如何在现实中使用它呢?假设你从数据库中得到了一个包含动物类型和名称的对象的记录;你可以将这个对象传递给代理类的构造函数,并将其作为创建你的类的机制。
在实践中,当支持资源密集型对象时,这是一个很好的用例,除非客户端真正需要它们,否则你不一定想要实例化它们;对于资源密集型网络连接和其他类型的资源也是如此。
外观
外观(也称为Façade)设计模式是一件奇妙的事情;它们本质上是一个复杂系统的简单接口。外观设计模式通过提供一个单一的类来工作,这个类本身实例化其他类并提供一个简单的接口来使用这些函数。
使用这种模式时的一个警告是,由于类是在外观中实例化的,你本质上是将它所使用的类紧密耦合在一起。有些情况下你希望这样做,但也有些情况下你不希望。在你不希望这种行为的情况下,最好使用依赖注入。
我发现这在将一组糟糕的 API 封装成一个统一的 API 时非常有用。它减少了外部依赖,允许复杂性内部化;这个过程可以使你的代码更易读。
我将在一个粗糙的例子中演示这种模式,但这将使机制变得明显。
让我提议三个玩具工厂的类。
制造商(制造玩具的工厂)是一个简单的类,它根据一次制造多少个玩具来实例化:
<?php
class Manufacturer
{
private $capacity;
public function __construct(int $capacity)
{
$this->capacity = $capacity;
}
public function build(): string
{
return uniqid();
}
}
Post 类(运输快递员)是一个简单的函数,用于从工厂发货玩具:
<?php
class Post
{
private $sender;
public function __construct(string $sender)
{
$this->sender = $sender;
}
public function dispatch(string $item, string $to): bool
{
if (strlen($item) !== 13) {
return false;
}
if (empty($to)) {
return false;
}
return true;
}
}
一个SMS
类通知客户他们的玩具已经从工厂发货:
<?php
class SMS
{
private $from;
public function __construct(string $from)
{
$this->from = $from;
}
public function send(string $to, string $message): bool
{
if (empty($to)) {
return false;
}
if (strlen($message) === 0) {
return false;
}
echo $to . " received message: " . $message;
return true;
}
}
这是我们的ToyFactory
类,它充当一个外观,将所有这些类连接在一起,并允许操作按顺序发生:
<?php
class ToyShop
{
private $courier;
private $manufacturer;
private $sms;
public function __construct(String $factoryAdress, String $contactNumber, int $capacity)
{
$this->courier = new Post($factoryAdress);
$this->sms = new SMS($contactNumber);
$this->manufacturer = new Manufacturer($capacity);
}
public function processOrder(string $address, $phone)
{
$item = $this->manufacturer->build();
$this->courier->dispatch($item, $address);
$this->sms->send($phone, "Your order has been shipped.");
}
}
最后,我们可以将所有这些内容包装在我们的index.php
文件中:
<?php
require_once('Manufacturer.php');
require_once('Post.php');
require_once('SMS.php');
require_once('ToyShop.php');
$childrensToyFactory = new ToyShop('1 Factory Lane, Oxfordshire', '07999999999', 5);
$childrensToyFactory->processOrder('8 Midsummer Boulevard', '07123456789');
一旦我们运行这段代码,我们会看到来自我们的SMS
类的消息显示出短信已发送:
在其他情况下,当各种类之间耦合较松时,我们可能会发现最好使用依赖注入。通过将执行各种操作的对象注入到ToyFactory
类中,我们可以通过能够注入ToyFactory
类可以操作的假类来使测试变得更容易。
就我个人而言,我非常相信尽可能使代码易于测试;这也是为什么我不喜欢这种方法的原因。
总结
本章通过引入结构设计模式扩展了我们在上一章开始学习的设计模式。
因此,我们学会了一些关键的模式来简化软件设计过程;这些模式确定了实现不同实体之间关系的简单方式:
-
我们学习了装饰器,如何包装类以向它们添加额外的行为,并且关键是,我们学会了这如何帮助我们遵守单一职责原则。
-
我们学习了类和对象适配器,以及它们之间的区别。这里的关键是为什么我们可能会选择组合而不是继承的论点。
-
我们复习了享元设计模式,它可以帮助我们以节省内存的方式执行某些过程。
-
我们学会了组合设计模式如何帮助我们将对象的组合与单个对象一样对待。
-
我们介绍了桥接设计模式,它让我们将抽象与实现解耦,使两者能够独立变化。
-
我们介绍了代理设计模式如何作为另一个类的接口,并且我们可以将其用作转发代理。
-
最后,我们学会了外观设计模式如何用于为复杂系统提供简单的接口。
在下一章中,我们将通过讨论行为模式来结束我们的设计模式部分,准备涉及架构模式。
第五章:行为设计模式
行为设计模式关乎对象之间的通信。
牢记单一责任原则,类只封装一个责任是至关重要的。鉴于此,显然有必要允许对象进行通信。
通过使用行为设计模式,我们能够增加进行这些通信的灵活性。
在本章中,我们将介绍以下模式:
-
观察者模式(SplObserver/SplSubject)
-
迭代器
-
PHP 的许多迭代器
-
生成器
-
模板模式
-
责任链模式
-
策略模式
-
规范模式
-
定时任务模式
热情程序员的个性特征
在我们开始讨论行为设计模式之前,让我们先谈谈你作为开发人员的行为。在本书的早些时候,我已经谈到开发失败经常是由于糟糕的管理实践而出现的。
让我们想象两种情景:
-
一家公司引入 Scrum 作为一种方法论(或者另一种缺乏技术知识的“敏捷”方法论),而他们的代码并不足够灵活以承受代码。在这些情况下,当代码被添加时,它经常被拼凑在一起,几乎可以肯定的是,代码的实现时间比没有技术债务时要长得多。这导致开发速度缓慢。
-
或者,一个公司遵循严格预定义的流程,而这种方法论是一成不变的。这些流程通常是不合理的,但开发人员经常遵循它们,因为他们没有接受更好流程的教育,不想卷入官僚纠纷来改变它们,甚至可能因试图改进流程而担心受到纪律处分。
在这两种情况下,一个糟糕的流程是问题的核心。即使你没有处理遗留项目,由于财产要求的变化,这也可能成为一个问题。软件的一个好特性是能够改变,甚至改变软件本身的设计(我们将在重构的最后一章讨论这个问题)。
Alastair Cockburn 指出,软件开发人员通常不适合预定义的生产线流程。人类是不可预测的,当他们是任何给定流程中的关键行为者时,流程也变得不可预测。人类容易出错,在软件开发中有很多错误的空间,他们在预定义流程中不会完美地行事。基本上,这就是为什么人必须高于流程,正如敏捷宣言中所述。开发人员必须高于流程。
一些管理职位的人想要购买所谓的敏捷。他们会雇佣一个不了解软件开发如何真正取得成功的顾问,而是作为销售敏捷的摇钱树运营实施一个荒谬的流程。我认为 Scrum 是这种情况的最糟糕的例子(部分原因是因为不准确的课程和伪资格的数量),但毫无疑问其他敏捷流程也可以被用作摇钱树。
我曾多次接触到声称“Scrum 说我们应该做…”或“敏捷说我们应该做…”的经理或 Scrum 大师。这在心理上是不合逻辑的,应该避免。当你说这句话时,你基本上没有理解敏捷方法论是基于灵活性原则的,因此,人必须高于流程。
让我们再次回顾第一个情景。请注意,争议主要是由于开发质量的缺乏而不是项目管理流程。Scrum 未能实施开发流程,因此,通过 Scrum 尝试的项目往往会失败。
极限编程(XP)包含这些开发规则,Scrum 缺乏这些规则。以下是一些例子:
-
编码标准(在 PHP 中,你可以选择我们在前几章讨论过的 PSR 标准)
-
首先编写单元测试,然后编写代码使其通过测试
-
所有的生产代码都是成对编程的
-
一个专用的集成服务器一次只集成一对代码,代码被频繁地集成
-
使用集体所有权;代码库的任何部分都不会对其他开发人员限制
这一切都是在修复 XP 的背景下完成的,使改进过程成为开发的常规部分。
引入技术标准和开发规则需要对开发有先验知识并对学习有热情;因此,逻辑和以证据为基础的思维过程至关重要。这些都是成为优秀软件工程师的关键要素。
配对编程不能成为辅导的一种努力,也不能成为学生和老师之间的关系;两个开发人员都必须愿意提出想法并接受这些想法的批评。事实上,能够互相学习是至关重要的。
在敏捷关系中,每个人都必须愿意理解和贡献规划过程,因此沟通是一项至关重要的技能。同样,彼此尊重是关键;从客户到开发人员,每个人都应该受到尊重。开发人员在许多方面都必须勇敢,尤其是在关于进展和估计的真实性方面,同时也必须适应变化。我们必须在处理或拒绝反馈之前努力理解我们收到的反馈。
这些技能不仅仅是开关,它们是开放式的技能和知识基础,我们必须努力维护和运用。事情会出错;通过使用反馈,我们能够确保我们的代码在部署之前具有足够高的质量。
观察者模式(SplObserver/SplSubject)
观察者设计模式本质上允许一个对象(主题)维护一个观察者列表,当该对象的状态发生变化时,这些观察者会自动收到通知。
这种模式应用于对象之间的一对多依赖关系;总是有一个主题更新多个观察者。
四人帮最初确定这种模式特别适用于抽象有两个方面,其中一个依赖于另一个的情况。除此之外,当对象的更改需要对其他对象进行更改,而你不知道需要更改多少其他对象时,这种模式也非常有用。最后,当一个对象应该通知其他对象而不做出关于这些对象是什么的假设时,这种模式也非常有用,因此这种模式非常适用于松散耦合的关系。
PHP 提供了一个非常有用的接口,称为SplObserver
和SplSubject
。这些接口提供了实现观察者设计模式的模板,但实际上并没有实现任何功能。
实质上,当我们实现这种模式时,我们允许无限数量的对象观察主题中的事件。
通过在subject
对象中调用attach
方法,我们可以将观察者附加到主题上。当主题发生变化时,主题的notify
方法可以遍历观察者并多态地调用它们的update
方法。
我们还可以在主题中调用一个未通知的方法,这将允许我们停止一个观察者
对象观察一个主题
对象。
鉴于此,Subject
类包含了将观察者附加到自身和从自身分离的方法,该类还包含了一个notify
方法来更新正在观察它的观察者。因此,PHP 的SplSubject
接口如下:
interface SplSubject {
public function attach (SplObserver $observer);
public function detach (SplObserver $observer);
public function notify ();
}
与此相比,我们的SplObserver
接口看起来更简单;它只需要实现一个允许主题更新观察者的方法:
interface SplObserver {
public function update (SplSubject $subject);
}
现在,让我们看看如何实现这两个接口来实现这个设计模式。在这个例子中,我们将有一个新闻订阅类,它将更新正在阅读这些类的各种读者。
让我们定义我们的Feed
类,它将实现SplSubject
接口:
<?php
class Feed implements SplSubject
{
private $name;
private $observers = array();
private $content;
public function __construct($name)
{
$this->name = $name;
}
public function attach(SplObserver $observer)
{
$observerHash = spl_object_hash($observer);
$this->observers[$observerHash] = $observer;
}
public function detach(SplObserver $observer)
{
$observerHash = spl_object_hash($observer);
unset($this->observers[$observerHash]);
}
public function breakOutNews($content)
{
$this->content = $content;
$this->notify();
}
public function getContent()
{
return $this->content . " on ". $this->name . ".";
}
public function notify()
{
foreach ($this->observers as $value) {
$value->update($this);
}
}
}
我们讨论的实现总体上相当简单。请注意,它使用了我们在本书中之前探讨过的spl_object_hash
函数,以便让我们轻松地分离对象。通过使用哈希作为数组的键,我们能够快速找到给定的对象,而无需进行其他操作。
现在我们可以定义我们的Reader
类,它将实现SplObserver
接口:
<?php
class Reader implements SplObserver
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function update(SplSubject $subject)
{
echo $this->name . ' is reading the article ' . $subject->getContent() . ' ';
}
}
让我们将所有这些内容放在我们的index.php
文件中:
<?php
require_once('Feed.php');
require_once('Reader.php');
$newspaper = new Feed('Junade.com');
$allen = new Reader('Mark');
$jim = new Reader('Lily');
$linda = new Reader('Caitlin');
//add reader
$newspaper->attach($allen);
$newspaper->attach($jim);
$newspaper->attach($linda);
//remove reader
$newspaper->detach($linda);
//set break outs
$newspaper->breakOutNews('PHP Design Patterns');
在这个脚本中,我们首先用三个读者实例化一个订阅源。我们将它们全部附加,然后分离一个。最后,我们发送一个新的警报,产生以下输出:
这种设计模式的主要优势在于观察者和主题之间关系的松散耦合性。有更大的模块化,因为主题和观察者可以独立变化。除此之外,我们可以添加任意多的观察者,提供我们想要的任意多的功能。这种可扩展性和定制性通常是这种设计模式应用于应用程序视图上下文的原因,也经常在模型-视图-控制器(MVC)框架中实现。
使用这种模式的缺点在于当我们需要调试整个过程时会出现问题;流程控制可能会变得困难,因为观察者彼此之间并不知道对方的存在。除此之外,还存在更新开销,当处理特别大的观察者时,可能会使内存管理变得困难。
请记住,这种设计模式仅用于一个程序内部,不适用于进程间通信或消息系统。本书后面,我们将介绍如何使用消息模式来描述消息解析系统的不同部分如何相互连接,当我们想要允许不同进程之间的互通,而不仅仅是一个进程内的不同类时。
迭代器
迭代器设计模式是使用迭代器遍历容器的地方。在 PHP 中,如果最终继承了可遍历接口,类就可以使用foreach
构造进行遍历。不幸的是,这是一个抽象基础接口,你不能单独实现它(除非你是在 PHP 核心中编写)。相反,你必须实现称为Iterator
或IteratorAggregate
的接口。通过实现这些接口中的任何一个,你可以使一个类可迭代,并可以使用foreach
进行遍历。
Iterator
和IteratorAggregate
接口非常相似,除了IteratorAggregate
接口创建一个外部迭代器。IteratorAggregate
作为一个接口只需要定义一个方法getIterator
。这个方法必须返回ArrayIterator
接口的一个实例。
IteratorAggregate
假设我们想要创建一个实现这个接口的实现,它将遍历各种时间。
首先,让我们从一个IternatorAggregate
类的基本实现开始,以了解它是如何工作的:
<?php
class timeIterator implements IteratorAggregate {
public function getIterator()
{
return new ArrayIterator(array(
'property1' => 1,
'property2' => 2,
'property4' => 3
));
}
}
我们可以按照以下方式遍历这个类:
<?php
$time = new timeIterator;
foreach($time as $key => $value) {
var_dump($key, $value);
echo "n";
}
这个输出如下:
我修改了这个脚本,使它接受一个time
值,并计算两侧的各种值,并使它们可迭代:
<?php
class timeIterator implements IteratorAggregate
{
public function __construct(int $time)
{
$this->weekAgo = $time - 604800;
$this->yesterday = $time - 86400;
$this->now = $time;
$this->tomorrow = $time + 86400;
$this->nextWeek = $time + 604800;
}
public function getIterator()
{
return new ArrayIterator($this);
}
}
$time = new timeIterator(time());
foreach ($time as $key => $value) {
var_dump($key, $value);
echo "n";
}
此脚本的输出如下:
迭代器
假设我们想要创建一个实现这个接口的实现,它将遍历各种时间。
PHP 的许多迭代器
之前,我们已经探讨了SPL(标准 PHP 库)中的一些函数,这是一个解决常见问题的接口和类的集合。鉴于这个目标,它们与设计模式有着共同的目标,但它们都以不同的方式解决这些问题。构建这个扩展和在 PHP 7 中编译不需要外部库;事实上,你甚至不能禁用它。
作为这个库的一部分,在 SPL 中有很多迭代器。您可以在文档中找到它们的列表php.net/manual/en/spl.iterators.php
。
以下是一些这些迭代器的列表,以便让您了解您可以利用它们的用途:
-
追加迭代器
-
数组迭代器
-
缓存迭代器
-
回调过滤迭代器
-
目录迭代器
-
空迭代器
-
文件系统迭代器
-
过滤迭代器
-
Glob 迭代器
-
无限迭代器
-
迭代器迭代器
-
限制迭代器
-
多重迭代器
-
无需倒带迭代器
-
父迭代器
-
递归数组迭代器
-
递归缓存迭代器
-
递归回调过滤迭代器
-
递归目录迭代器
-
递归过滤迭代器
-
递归迭代器迭代器
-
递归正则表达式迭代器
-
递归树迭代器
-
正则表达式迭代器
生成器
PHP 有一个很好的机制来以紧凑的方式创建迭代器。这种类型的迭代器有一些严重的限制;它们只能向前,不能倒带。事实上,即使只是从头开始一个迭代器,你也必须重新构建生成器。本质上,这是一个只能向前的迭代器。
一个使用yield
关键字而不是return
关键字的函数。这将像return
语句一样工作,但不会停止该函数的执行。生成器函数可以yield
数据,只要你愿意。
当您用值填充一个数组时,这些值必须存储在内存中,这可能导致您超出 PHP 内存限制,或者需要大量的处理时间来生成器。当您将逻辑放在生成器函数中时,这种开销就不存在了。生成器函数可能只产生它需要的结果;不需要先预先填充一个数组。
这是一个简单的生成器,将var_dump
一个声明字符串,生成器已经启动。该函数将生成前五个平方数,同时输出它们在序列中的位置。然后最后指示生成器已结束:
<?php
function squaredNumbers()
{
var_dump("Generator starts.");
for ($i = 0; $i < 5; ++$i) {
var_dump($i . " in series.");
yield pow($i, 2);
}
var_dump("Generator ends.");
}
foreach (squaredNumbers() as $number) {
var_dump($number);
}
这个脚本的第二部分循环运行这个函数,并对每个数字运行一个var_dump
字符串。这个输出如下:
让我们稍微修改这个函数。
非常重要的一点是,如果你给变量添加了返回类型,你只能声明Generator
,Iterator
或Traversable
,integer
的返回类型。
这是代码:
<?php
function squaredNumbers(int $start, int $end): Generator
{
for ($i = $start; $i <= $end; ++$i) {
yield pow($i, 2);
}
}
foreach (squaredNumbers(1, 5) as $number) {
var_dump($number);
}
这个结果如下:
如果我们想要产生一个键和一个值,那么这是相当容易的。
还有一些关于在 PHP 5 中使用生成器的事情要提及:在 PHP 5 中,当您想要同时产生一个变量并将其设置为一个变量时,必须将 yield 语句包装在括号中。这个限制在 PHP 7 中不存在。
这在 PHP 5 和 7 中有效:
**$data = (yield $value);**
这只在 PHP 7 中有效:
**$data = yield $value;**
假设我们想修改我们的生成器,使其产生一个键值结果。代码如下:
<?php
function squaredNumbers(int $start, int $end): Generator
{
for ($i = $start; $i <= $end; ++$i) {
yield $i => pow($i, 2);
}
}
foreach (squaredNumbers(1, 5) as $key => $number) {
var_dump([$key, $number]);
}
当我们测试这个时,我们将var_dump
一个包含键值存储的二维数组,这个数组包含了生成器在给定迭代中产生的任何值。
这是输出:
还有一些其他提示,一个没有变量的 yield 语句(就像在下面的命令中所示的那样)将简单地产生null
:
**yield;**
您还可以使用yield from
,它将产生任何给定生成器的内部值。
假设我们有一个包含两个值的数组:
[1, 2]
当我们使用yield from
来产生一个包含两个值的数组时,我们得到了数组的内部值。让我演示一下:
<?php
function innerGenerator()
{
yield from [1, 2];
}
foreach (innerGenerator() as $number) {
var_dump($number);
}
这将显示以下输出:
然而,现在让我们修改这个脚本,使其使用yield
而不是yield from
:
<?php
function innerGenerator()
{
yield [1, 2];
}
foreach (innerGenerator() as $number) {
var_dump($number);
}
现在我们将看到,我们不仅仅得到了数组的内部值,还得到了外部容器:
模板方法设计模式
模板方法设计模式用于创建一组必须执行类似行为的子类。
这种设计模式由模板方法组成,它是一个抽象类。具体的子类可以重写抽象类中的方法。模板方法包含算法的骨架;子类可以使用重写来改变算法的具体行为。
因此,这是一个非常简单的设计模式;它鼓励松散耦合,同时控制子类化的点。因此,它比简单的多态行为更精细。
考虑一个Pasta
类的抽象:
<?php
abstract class Pasta
{
public function __construct(bool $cheese = true)
{
$this->cheese = $cheese;
}
public function cook()
{
var_dump('Cooked pasta.');
$this->boilPasta();
$this->addSauce();
$this->addMeat();
if ($this->cheese) {
$this->addCheese();
}
}
public function boilPasta(): bool
{
return true;
}
public abstract function addSauce(): bool;
public abstract function addMeat(): bool;
public abstract function addCheese(): bool;
}
这里有一个简单的构造函数,用于确定意大利面是否应该包含奶酪,以及一个运行烹饪算法的cook
函数。
请注意,添加各种配料的函数被抽象掉了;在子类中,我们使用所需的行为来实现这些方法。
假设我们想做肉丸意大利面。我们可以按照以下方式实现这个抽象类:
<?php
class MeatballPasta extends Pasta
{
public function addSauce(): bool
{
var_dump("Added tomato sauce");
return true;
}
public function addMeat(): bool
{
var_dump("Added meatballs.");
return true;
}
public function addCheese(): bool
{
var_dump("Added cheese.");
return true;
}
}
我们可以使用以下脚本在我们的index.php
文件中对这段代码进行测试:
<?php
require_once('Pasta.php');
require_once('MeatballPasta.php');
var_dump("Meatball pasta");
$dish = new MeatballPasta(true);
$dish->cook();
感谢各种函数中的var_dump
变量显示各种状态消息,我们可以看到如下输出:
现在,假设我们想要制作一个素食食谱。我们可以在不同的上下文中利用相同的抽象。
这一次,在添加肉或奶酪时,这些函数什么也不做;它们可以返回false
或null
值:
<?php
class VeganPasta extends Pasta
{
public function addSauce(): bool
{
var_dump("Added tomato sauce");
return true;
}
public function addMeat(): bool
{
return false;
}
public function addCheese(): bool
{
return false;
}
}
让我们修改我们的index.php
文件以表示这种行为:
<?php
require_once('Pasta.php');
require_once('MeatballPasta.php');
var_dump("Meatball pasta");
$dish = new MeatballPasta(true);
$dish->cook();
var_dump("");
var_dump("Vegan pasta");
require_once('VeganPasta.php');
$dish = new VeganPasta(true);
$dish->cook();
输出如下:
这种设计模式简单易用,但基本上允许您抽象化您的算法设计,并将责任委托给您想要的子类。
责任链
假设我们有一组对象,它们一起解决问题。当一个对象无法解决问题时,我们希望对象将任务发送给链中的另一个对象。这就是责任链设计模式的用途。
为了使这个工作起来,我们需要一个处理程序,这将是我们的Chain
接口。链中的各个对象都将实现这个Chain
接口。
让我们从一个简单的例子开始;一个助理可以为少于 100 美元购买资产,一个经理可以为少于 500 美元购买东西。
我们的Purchaser
接口的抽象如下:
<?php
interface Purchaser
{
public function setNextPurchaser(Purchaser $nextPurchaser): bool;
public function buy($price): bool;
}
我们的第一个实现是Associate
类。非常简单,我们实现setNextPurchaser
函数,以便将nextPurchaser
类属性设置为链中的下一个对象。
当我们调用buy
函数时,如果价格在范围内,助理将购买它。如果不是,链中的下一个购买者将购买它:
<?php
class AssociatePurchaser implements Purchaser
{
public function setNextPurchaser(Purchaser $nextPurchaser): bool
{
$this->nextPurchaser = $nextPurchaser;
return true;
}
public function buy($price): bool
{
if ($price < 100) {
var_dump("Associate purchased");
return true;
} else {
if (isset($this->nextPurchaser)) {
reurn $this->nextPurchaser->buy($price);
} else {
var_dump("Could not buy");
return false;
}
}
}
}
我们的Manager
类完全相同;我们只允许经理购买低于 500 美元的资产。实际上,当您应用这种模式时,您不会只是复制一个类,因为您的类会有不同的逻辑;这个例子只是一个非常简单的实现。
以下是代码:
<?php
class ManagerPurchaser implements Purchaser
{
public function setNextPurchaser(Purchaser $nextPurchaser): bool
{
$this->nextPurchaser = $nextPurchaser;
return true;
}
public function buy($price): bool
{
if ($price < 500) {
var_dump("Associate purchased");
return true;
} else {
if (isset($this->nextPurchaser)) {
return $this->nextPurchaser->buy($price);
} else {
var_dump("Could not buy");
return false;
}
}
}
}
让我们在我们的index.php
文件中运行一个来自助理的基本购买。
首先,这是我们放在index.php
文件中的代码:
<?php
require_once('Purchaser.php');
require_once('AssociatePurchaser.php');
$associate = new AssociatePurchaser();
$associate->buy(50);
所有这些的输出如下:
接下来,让我们测试我们的Manager
类。我们将在我们的index.php
文件中修改购买价格,并将我们的Manager
类添加到链中。
这是我们修改后的index.php
:
<?php
require_once('Purchaser.php');
require_once('AssociatePurchaser.php');
require_once('ManagerPurchaser.php');
$associate = new AssociatePurchaser();
$manager = new ManagerPurchaser();
$associate->setNextPurchaser($manager);
$associate->buy(400);
这有以下输出:
让我们看看如果改变价格会发生什么导致购买失败。
我们在我们的index.php
文件的最后一行进行更改,使购买价格现在为 600 美元:
<?php
require_once('Purchaser.php');
require_once('AssociatePurchaser.php');
require_once('ManagerPurchaser.php');
$associate = new AssociatePurchaser();
$manager = new ManagerPurchaser();
$associate->setNextPurchaser($manager);
$associate->buy(600);
这有以下输出:
现在我们可以扩展这个脚本。让我们添加DirectorPurchaser
和BoardPurchaser
,这样我们就可以以更高的成本进行购买。
我们将创建一个DirectorPurchaser
,他可以在 10,000 美元以下购买。
这个类如下:
<?php
class DirectorPurchaser implements Purchaser
{
public function setNextPurchaser(Purchaser $nextPurchaser): bool
{
$this->nextPurchaser = $nextPurchaser;
return true;
}
public function buy($price): bool
{
if ($price < 10000) {
var_dump("Director purchased");
return true;
} else {
if (isset($this->nextPurchaser)) {
return $this->nextPurchaser->buy($price);
} else {
var_dump("Could not buy");
return false;
}
}
}
}
让我们为BoardPurchaser
类做同样的事情,他可以在 10 万美元以下购买:
<?php
class BoardPurchaser implements Purchaser
{
public function setNextPurchaser(Purchaser $nextPurchaser): bool
{
$this->nextPurchaser = $nextPurchaser;
return true;
}
public function buy($price): bool
{
if ($price < 100000) {
var_dump("Board purchased");
return true;
} else {
if (isset($this->nextPurchaser)) {
return $this->nextPurchaser->buy($price);
} else {
var_dump("Could not buy");
return false;
}
}
}
}
现在我们可以更新我们的index.php
脚本,需要新的类,实例化它们,然后将所有内容绑定在一起。最后,我们将尝试通过调用链中的第一个来运行购买。
以下是脚本:
<?php
require_once('Purchaser.php');
require_once('AssociatePurchaser.php');
require_once('ManagerPurchaser.php');
require_once('DirectorPurchaser.php');
require_once('BoardPurchaser.php');
$associate = new AssociatePurchaser();
$manager = new ManagerPurchaser();
$director = new DirectorPurchaser();
$board = new BoardPurchaser();
$associate->setNextPurchaser($manager);
$manager->setNextPurchaser($director);
$director->setNextPurchaser($board);
$associate->buy(11000);
以下是此脚本的输出:
这使我们能够遍历一系列对象来处理数据。当处理树数据结构(例如,XML 树)时,这是特别有用的。这可以以启动并离开的方式工作,我们可以降低处理遍历链的开销。
此外,链是松散耦合的,数据通过链传递直到被处理。任何对象都可以链接到任何其他对象,任何顺序。
策略设计模式
策略设计模式存在是为了允许我们在运行时改变对象的行为。
假设我们有一个类,将一个数字提高到一个幂,但在运行时我们想要改变是否平方或立方一个数字。
让我们首先定义一个接口,一个将数字提高到给定幂的函数:
<?php
interface Power
{
public function raise(int $number): int;
}
我们可以相应地定义Square
和Cube
一个给定数字的类,通过实现接口。
这是我们的Square
类:
<?php
class Square implements Power
{
public function raise(int $number): int
{
return pow($number, 2);
}
}
让我们定义我们的Cube
类:
<?php
class Cube implements Power
{
public function raise(int $number): int
{
return pow($number, 3);
}
}
我们现在可以构建一个类,它将基本上使用其中一个这些类来处理一个数字。
这是这个类:
<?php
class RaiseNumber
{
public function __construct(Power $strategy)
{
$this->strategy = $strategy;
}
public function raise(int $number)
{
return $this->strategy->raise($number);
}
}
现在我们可以使用index.php
文件来演示整个设置:
<?php
require_once('Power.php');
require_once('Square.php');
require_once('Cube.php');
require_once('RaiseNumber.php');
$processor = new RaiseNumber(new Square());
var_dump($processor->raise(5));
输出如预期,5²是25
。
以下是输出:
我们可以在我们的index.php
文件中用Cube
对象替换Square
对象:
<?php
require_once('Power.php');
require_once('Square.php');
require_once('Cube.php');
require_once('RaiseNumber.php');
$processor = new RaiseNumber(new Cube());
var_dump($processor->raise(5));
以下是更新脚本的输出:
到目前为止一切顺利;但之所以伟大的原因是我们可以动态添加实际改变类操作的逻辑。
以下是所有这些的一个相当粗糙的演示:
<?php
require_once('Power.php');
require_once('Square.php');
require_once('Cube.php');
require_once('RaiseNumber.php');
if (isset($_GET['n'])) {
$number = $_GET['n'];
} else {
$number = 0;
}
if ($number < 5) {
$power = new Cube();
} else {
$power = new Square();
}
$processor = new RaiseNumber($power);
var_dump($processor->raise($number));
所以为了演示这一点,让我们运行脚本,将nGET
变量设置为4
,这应该将数字4
立方,得到一个输出64
:
现在如果我们通过数字6
,我们期望脚本将数字6
平方,得到一个输出36
:
在这种设计模式中,我们已经做了很多:
-
我们定义了一系列算法,它们都有一个共同的接口
-
这些算法是可以互换的;它们可以在不影响客户端实现的情况下进行交换
-
我们在一个类中封装了每个算法
现在我们可以独立于使用它的客户端来变化算法。
规范设计模式
规范设计模式非常强大。在这里,我将尝试对其进行高层概述,但还有很多可以探索;如果您有兴趣了解更多,我强烈推荐Eric Evans和Martin Fowler的论文Specifications。
这种设计模式用于编码关于对象的业务规则。它们告诉我们一个对象是否满足某些业务标准。
我们可以以以下方式使用它们:
-
对于验证一个对象,我们可以做出断言
-
从给定集合中获取选择的对象
-
为了指定如何通过按订单制造来创建对象
在这个例子中,我们将构建规范来查询
让我们看看以下对象:
<?php
$workers = array();
$workers['A'] = new StdClass();
$workers['A']->title = "Developer";
$workers['A']->department = "Engineering";
$workers['A']->salary = 50000;
$workers['B'] = new StdClass();
$workers['B']->title = "Data Analyst";
$workers['B']->department = "Engineering";
$workers['B']->salary = 30000;
$workers['C'] = new StdClass();
$workers['C']->title = "Personal Assistant";
$workers['C']->department = "CEO";
$workers['C']->salary = 25000;
The workers array will look like this if we var_dump it:
array(3) {
["A"]=>
object(stdClass)#1 (3) {
["title"]=>
string(9) "Developer"
["department"]=>
string(11) "Engineering"
["salary"]=>
int(50000)
}
["B"]=>
object(stdClass)#2 (3) {
["title"]=>
string(12) "Data Analyst"
["department"]=>
string(11) "Engineering"
["salary"]=>
int(30000)
}
["C"]=>
object(stdClass)#3 (3) {
["title"]=>
string(18) "Personal Assistant"
["department"]=>
string(3) "CEO"
["salary"]=>
int(25000)
}
}
让我们以一个EmployeeSpecification
接口开始;这是我们所有规范都需要实现的接口。确保用您处理的对象类型(例如,员工,或您从实例化对象的类的名称)替换StdClass
。
这是代码:
<?php
interface EmployeeSpecification
{
public function isSatisfiedBy(StdClass $customer): bool;
}
现在是时候编写一个名为EmployeeIsEngineer
的实现了:
<?php
class EmployeeIsEngineer implements EmployeeSpecification
{
public function isSatisfiedBy(StdClass $customer): bool
{
if ($customer->department === "Engineering") {
return true;
}
return false;
}
}
然后,我们可以遍历我们的工作人员,检查哪些符合我们制定的标准:
$isEngineer = new EmployeeIsEngineer();
foreach ($workers as $id => $worker) {
if ($isEngineer->isSatisfiedBy($worker)) {
var_dump($id);
}
}
让我们把这一切放在我们的index.php
文件中:
<?php
require_once('EmployeeSpecification.php');
require_once('EmployeeIsEngineer.php');
$workers = array();
$workers['A'] = new StdClass();
$workers['A']->title = "Developer";
$workers['A']->department = "Engineering";
$workers['A']->salary = 50000;
$workers['B'] = new StdClass();
$workers['B']->title = "Data Analyst";
$workers['B']->department = "Engineering";
$workers['B']->salary = 30000;
$workers['C'] = new StdClass();
$workers['C']->title = "Personal Assistant";
$workers['C']->department = "CEO";
$workers['C']->salary = 25000;
$isEngineer = new EmployeeIsEngineer();
foreach ($workers as $id => $worker) {
if ($isEngineer->isSatisfiedBy($worker)) {
var_dump($id);
}
}
这是此脚本的输出:
组合规范允许您组合规范。通过使用AND
、NOT
、OR
和NOR
运算符,您可以将它们的各自功能构建到不同的规范类中。
同样,您也可以使用规范来获取对象。
随着代码的进一步复杂化,这段代码变得更加复杂,但是您理解了要点。事实上,我在本节开头提到的 Eric Evans 和 Martin Fowler 的论文涉及了一些更加复杂的安排。
无论如何,这种设计模式基本上允许我们封装业务逻辑以陈述关于对象的某些事情。这是一种非常强大的设计模式,我强烈鼓励更深入地研究它。
定期任务模式
定期任务基本上由三个部分组成:任务本身,通过定义任务运行的时间和允许运行的时间来进行调度的作业,最后是执行此作业的作业注册表。
通常,这些是通过在 Linux 服务器上使用 cron 来实现的。您可以使用以下配置语法向“配置”文件添加一行:
**# ┌───────────── min (0 - 59)
# │ ┌────────────── hour (0 - 23)
# │ │ ┌─────────────── day of month (1 - 31)
# │ │ │ ┌──────────────── month (1 - 12)
# │ │ │ │ ┌───────────────── day of week (0 - 6) (0 to 6 are Sunday to
# │ │ │ │ │ Saturday, or use names; 7 is also Sunday)
# │ │ │ │ │
# │ │ │ │ │
# * * * * * command to execute**
通常可以通过在命令行中运行crontab -e
来编辑cron
文件。您可以使用此模式安排任何 Linux 命令。以下是一个 cron 作业,将在每天 20:00(晚上 8 点)运行一个 PHP 脚本:
**0 20 * * * /usr/bin/php /opt/test.php**
这些实现起来非常简单,但是在创建它们时,以下是一些指导方针可以帮助您:
-
不要将您的 cron 作业暴露给互联网。
-
当运行任务时,任务不应检查是否需要运行的标准。这个测试应该在任务之外。
-
任务应该只执行其预期执行的计划活动,而不执行任何其他目的。
-
谨防我们在第七章中讨论的数据库作为 IPC 模式,重构。
您可以在任务中放入任何您想要的东西(在合理范围内)。您可能会发现异步执行是最佳路线。Icicle 是一个执行异步行为的出色的 PHP 库。您可以在icicle.io/
上找到在线文档。
当我们的任务需要按特定顺序完成几项任务时,您可能会从使用我们在结构设计模式部分讨论的组合设计模式中受益,并调用使用此模式调用其他任务的单个任务。
总结
在本章中,我们涵盖了一些识别对象之间常见通信模式的模式。
我们讨论了观察者模式如何用于更新观察者关于给定主题状态的。此外,我们还了解了标准 PHP 库包含的功能可以帮助我们实现这一点。
然后,我们继续讨论了如何在 PHP 中以许多不同的方式实现迭代器,使用 PHP 核心中的各种接口以及利用生成器函数。
我们继续讨论了模板模式如何定义算法骨架,我们可以以比标准多态性更严格的方式动态调整它。我们讨论了责任链模式,它允许我们将对象链接在一起以执行各种功能。策略模式教会了我们如何在运行时改变代码的行为。然后我介绍了规范模式的基础知识以及其中的高级功能。最后,我们复习了定期任务模式以及如何使用 Linux 上的 cron 来实现它。
这些设计模式对开发人员来说是一些最关键的设计模式。对象之间的通信在许多项目中至关重要,而这些模式确实可以帮助我们进行这种通信。
在下一章中,我们将讨论架构模式以及这些模式如何帮助您处理出现的软件架构任务,以及如何帮助您解决可能面临的更广泛的软件工程挑战(尽管它们在技术上可能不被认为是设计模式本身)。
第六章:架构模式
架构模式,有时被称为架构风格,为软件架构中的重复问题提供解决方案。
尽管与软件设计模式类似,但其范围更广,涉及软件工程中的各种问题,而不仅仅是软件本身的开发。
在本章中,我们将涵盖以下主题:
-
模型-视图-控制器(MVC)
-
面向服务的架构
-
微服务
-
异步排队
-
消息队列模式
模型-视图-控制器(MVC)
MVC 是 PHP 开发人员遇到的最常见类型的架构模式。基本上,MVC 是一种用于实现用户界面的架构模式。
它主要围绕以下方法论展开:
-
模型:为应用程序提供数据,无论是来自 MySQL 数据库还是其他任何数据存储。
-
控制器:控制器基本上是业务逻辑所在。控制器处理视图提供的任何查询,使用模型来协助其进行此行为。
-
视图:提供给最终用户的实际内容。这通常是一个 HTML 模板。
一个交互的业务逻辑并不严格分离于另一个交互。应用程序的不同类之间没有正式的分离。
需要考虑的关键是 MVC 模式主要是一种 UI 模式,因此在整个应用程序中无法很好地扩展。也就是说,UI 的呈现越来越多地通过 JavaScript 应用程序完成,即一个简单消耗 RESTful API 的单页面 JavaScript HTML 应用程序。
如果您使用 JavaScript,可以使用诸如 Backbone.js(模型-视图-控制器)、React.js 或 Angular 等框架与后端 API 进行通信,尽管这当然需要一个启用 JavaScript 的 Web 浏览器,这对我们的一些用户来说可能是理所当然的。
如果您处于无法使用 JavaScript 应用程序且必须提供渲染的 HTML 的环境中,对于您的 MVC 应用程序来说,将其简单地消耗 REST API 通常是一个好主意。REST API 执行所有业务逻辑,但标记的呈现是在 MVC 应用程序中完成的。尽管这增加了复杂性,但它提供了更大的责任分离,因此您不会将 HTML 与核心业务逻辑合并。也就是说,即使在这个 REST API 中,您也需要某种形式的关注点分离,您需要能够将标记的呈现与实际业务逻辑分开。
选择适合应用程序的架构模式的关键因素是复杂性是否适合应用程序的规模。因此,选择 MVC 框架也应基于应用程序本身的复杂性及其后续预期的复杂性。
鉴于基础设施即代码的增长,可以以完全编排的方式部署多个 Web 服务的基础设施。事实上,使用诸如 Docker 之类的容器化技术,可以以很小的开销(无需为每个服务启动新服务器)部署多个架构(例如具有单独 API 服务的 MVC 应用程序)。
在开发出色的架构时,关注点分离是一个重要特征,其中包括将 UI 与业务逻辑分离。
当以 MVC 模式思考时,重要的是要记住以下交互:
-
模型存储数据,根据模型提出的查询检索数据,并由视图显示
-
视图根据模型的更改生成输出
-
控制器发送命令以更新模型的状态;它还可以更新与之关联的视图,以改变给定模型的呈现方式
或者,通常使用以下图表表示:
不要仅仅为了使用而使用 MVC 框架,要理解它们存在的原因以及它们在特定用例中的适用性。记住,当你使用一个功能繁多的框架时,你要负责维护整个框架的运行。
根据需要引入组件(即通过 Composer)是开发具有相当多业务逻辑的软件的更实际的方法。
面向服务的架构
面向服务的架构主要由与数据存储库通信的服务中的业务逻辑组成。
这些服务可以以不同的形式衍生出来构建应用程序。这些应用程序以不同的格式采用这些服务来构建各种应用程序。将这些服务视为可以组合在一起以构建特定格式应用程序的乐高积木。
这个描述相当粗糙,让我进一步澄清:
-
服务的边界是明确的(它们可以将不同域上的 Web 服务分开,等等。)
-
服务可以使用共同的通信协议进行相互通信(例如都使用 RESTful API)
-
服务是自治的(它们是解耦的,与其他服务没有任何关联)
-
消息处理机制和架构对每个微服务都是可理解的(因此通常是相同的),但编程环境可以是不同的。
面向服务的架构本质上是分布式的,因此其初始复杂性可能比其他架构更高。
微服务
微服务架构可以被认为是面向服务的架构的一个子集。
基本上,微服务通过由小型独立进程组成的复杂应用程序,这些进程通过语言无关的 API 进行相互通信,使每个服务都可以相互访问。微服务可以作为单独的服务进行部署。
在微服务中,业务逻辑被分离成独立的、松耦合的服务。微服务的一个关键原则是每个数据库都应该有自己的数据库,这对确保微服务不会彼此紧密耦合至关重要。
通过减少单个服务的复杂性,我们可以减少该服务可能出现故障的点。理论上,通过使单个服务符合单一职责原则,我们可以更容易地调试和减少整个应用程序中出现故障的机会。
在计算机科学中,CAP 定理规定在给定的分布式计算机系统中不可能同时保证一致性、可用性和分区容错性。
想象一下,有两个分布式数据库都包含用户的电子邮件地址。如果我们想要更新这个电子邮件地址,没有办法可以在两个数据库中同时实时更新电子邮件地址,同时不将两个数据集重新合并。在分布式系统中,我们要么延迟访问数据以验证数据的一致性,要么呈现一个未更新的数据副本。
这使得传统的数据库事务变得困难。因此,在微服务架构中处理数据的最佳方式是使用一种最终一致的、事件驱动的架构。
每个服务在发生变化时都会发布一个事件,其他服务可以订阅此事件。当接收到事件时,数据会相应地更新。因此,应用程序能够在不需要使用分布式事务的情况下在多个服务之间保持数据一致性。
为了了解如何在微服务之间实现进程间通信的架构,请参阅本章节中的消息队列模式(使用 RabbitMQ 入门)部分。
在这种情况下,缓解这种限制的一种简单方法是通过使用时间验证系统来验证数据的一致性。因此,我们为一致性和分区容忍性而放弃了可用性。
如果您可以预见在给定的微服务架构中会出现这种问题,通常最好将需要满足 CAP 定理的服务分组到一个单一的服务中。
让我们考虑一个比萨外卖网站应用,它由以下微服务组成:
-
用户
-
优惠
-
食谱
-
购物车
-
计费
-
支付
-
餐厅
-
交付
-
比萨
-
评论
-
前端微服务
在这个例子中,我们可能会有以下用户旅程:
-
用户通过用户微服务进行身份验证。
-
用户可以使用优惠微服务选择优惠。
-
用户使用食谱微服务选择他们想要订购的比萨。
-
使用购物车微服务将所选的比萨添加到购物车中。
-
计费凭据通过计费微服务进行优化。
-
用户使用支付微服务进行支付。
-
订单通过餐厅微服务发送到餐厅。
-
当餐厅烹饪食物时,交付微服务会派遣司机去取食物并送达。
-
一旦交付微服务表明食物已经送达,用户就会被邀请使用评论微服务完成评论(评论微服务通过用户微服务通知用户)。
-
Web 前端使用前端微服务包装在一起。
前端微服务可以简单地是一个消费其他微服务并将内容呈现给 Web 前端的微服务。这个前端可以通过 REST 与其他微服务通信,可能在浏览器中实现为 JavaScript 客户端,或者仅作为其他微服务 API 的消费者的 PHP 应用。
无论哪种方式,将前端 API 消费者与后端之间放置一个网关通常是一个好主意。这使我们能够在确定与微服务的通信之前放置一些中间件;例如,我们可以使用网关查询用户微服务,以检查用户是否经过授权,然后允许访问购物车微服务。
如果您使用 JavaScript 直接与微服务通信,当您的 Web 前端尝试与不同主机名/端口上的微服务通信时,可能会遇到跨域问题;微服务网关可以通过将网关放置在与 Web 前端本身相同的源上来防止这种情况。
为了方便使用网关,您可能会感受到缺点,因为您将需要担心另一个系统和额外的响应时间(尽管您可以在网关级别添加缓存以改善性能)。
考虑到网关的添加,我们的架构现在可能看起来像这样:
在 PHP 中越来越多地出现微框架,比如 Lumen、Silex 和 Slim;这些都是面向 API 的框架,可以轻松构建支持我们应用的微服务。也就是说,您可能更好地采用更轻量级的方法,只需在需要时通过 Composer 引入所需的组件。
记住,添加另一种技术或框架会给整体情况增加额外的复杂性。不仅要考虑实施新解决方案的技术原因,还要考虑这将如何使客户和架构受益。微服务不是增加不必要复杂性的借口:保持简单,愚蠢。
异步排队
消息队列提供异步通信协议。在异步通信协议中,发送方和接收方不需要同时与消息队列交互。
另一方面,典型的 HTTP 是一种同步通信协议,这意味着客户端在操作完成之前被阻塞。
考虑一下;您给某人打电话,然后等待电话响起,您与之交谈的人立即倾听您要说的话。在通信结束时,您说“再见”,对方也会回答“再见”。这可以被认为是同步的,因为在您收到与您交流的人的响应以结束通信之前,您不会做任何事情。
但是,如果您要发送短信给某人,发送完短信后,您可以随心所欲地进行任何行为;当对方想要与您交流时,您可以收到对您发送的消息的回复。当某人正在起草要发送的回复时,您可以随心所欲地进行任何行为。虽然您不直接与发送方进行通信,但您仍然通过手机保持同步通信,当您收到新消息时通知您(或者每隔几分钟检查手机);但与对方的通信本身是异步的。双方都不需要了解对方的任何信息,他们只是在寻找自己的短信以便彼此进行通信。
消息队列模式(使用 RabbitMQ)
RabbitMQ 是一个消息代理;它接受并转发消息。在这里,让我们配置它,以便我们可以从一个 PHP 脚本发送消息到另一个脚本。
想象一下,我们正在将一个包裹交给快递员,以便他们交给客户;RabbitMQ 就是快递员,而脚本是分别接收和发送包裹的个体。
作为第一步,让我们在 Ubuntu 14.04 系统上安装 RabbitMQ;我将在此演示。
首先,我们需要将 RabbitMQ APT 存储库添加到我们的/etc/apt/sources.list.d
文件夹中。幸运的是,可以使用以下命令执行此操作:
**echo 'deb http://www.rabbitmq.com/debian/ testing main' | sudo tee /etc/apt/sources.list.d/rabbitmq.list**
请注意,存储库可能会发生变化;如果发生变化,您可以在www.rabbitmq.com/install-debian.html
找到最新的详细信息。
我们还可以选择将 RabbitMQ 公钥添加到受信任的密钥列表中,以避免在通过apt
命令安装或升级软件包时出现未签名的警告:
**wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add -**
到目前为止,一切都很好:
接下来,让我们运行apt-get update
命令,从我们包含的新存储库中获取软件包。完成后,我们可以使用apt-get install rabbitmq-server
命令安装我们需要的软件包:
在被询问时,请务必接受各种提示:
安装后,您可以运行rabbitmqctl status
来检查应用程序的状态,以确保它正常运行:
让我们简化一下生活。我们可以使用 Web GUI 来管理 RabbitMQ;只需运行以下命令:
**rabbitmq-plugins enable rabbitmq_management**
我们现在可以在<您的服务器 IP 地址>:15672
看到管理界面:
但在我们登录之前,我们需要创建一些登录凭据。为了做到这一点,我们需要回到命令行。
首先,我们需要设置一个新帐户,用户名为junade
,密码为insecurepassword
:
**rabbitmqctl add_user junade insecurepassword**
然后我们可以添加一些管理员权限:
**rabbitmqctl set_user_tags junade administrator**
**rabbitmqctl set_permissions -p / junade ".*" ".*" ".*"**
返回登录页面后,我们现在可以在输入这些凭据后看到我们很酷的管理界面:
这是 RabbitMQ 服务的 Web 界面,可通过我们的 Web 浏览器访问
现在我们可以测试我们安装的东西。让我们首先为这个新项目编写一个composer.json
文件:
{
"require": {
"php-amqplib/php-amqplib": "2.5.*"
}
}
RabbitMQ 使用高级消息队列协议(AMQP),这就是为什么我们正在安装一个 PHP 库,它基本上将帮助我们通过这个协议与它进行通信。
接下来,我们可以编写一些代码来使用我们刚刚安装的 RabbitMQ 消息代理发送消息:
这假设端口是5672
,安装在localhost
上,这可能会根据您的情况而改变。
让我们写一个小的 PHP 脚本来利用这个:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword');
$channel = $connection->channel();
$channel->queue_declare(
'sayHello', // queue name
false, // passive
true, // durable
false, // exclusive
false // autodelete
);
$msg = new AMQPMessage("Hello world!");
$channel->basic_publish(
$msg, // message
'', // exchange
'sayHello' // routing key
);
$channel->close();
$connection->close();
echo "Sent hello world message." . PHP_EOL;
所以让我们来详细分析一下。在前几行中,我们只是从 Composer 的autoload
中包含库,并且state
了我们要使用的命名空间。当我们实例化AMQPStreamConnection
对象时,我们实际上连接到了消息代理;然后我们可以创建一个新的通道对象,然后用它来声明一个新的队列。我们通过调用queue_declare
消息来声明一个队列。持久选项允许消息在 RabbitMQ 重新启动时存活。最后,我们只需发送出我们的消息。
现在让我们运行这个脚本:
**php send.php**
这个输出看起来像这样:
如果现在转到 RabbitMQ 的 Web 界面,点击队列选项卡并切换到获取消息对话框;您应该能够拉取我们刚刚发送到代理的消息:
在界面中使用这个网页,我们可以从队列中提取消息,这样我们就可以查看它们的内容
当然,这只是故事的一半。我们现在需要使用另一个应用程序实际检索这条消息。
让我们写一个receive.php
脚本:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword');
$channel = $connection->channel();
$channel->queue_declare(
'sayHello', // queue name
false, // passive
false, // durable
false, // exclusive
false // autodelete
);
$callback = function ($msg) {
echo "Received: " . $msg->body . PHP_EOL;
};
$channel->basic_consume(
'sayHello', // queue
'', // consumer tag
false, // no local
true, // no ack
false, // exclusive
false, // no wait
$callback // callback
);
while (count($channel->callbacks)) {
$channel->wait();
}
请注意,前几行与我们的发送脚本是相同的;我们甚至重新声明队列,以防在运行send.php
脚本之前运行此接收脚本。
让我们运行我们的receive.php
脚本:
在另一个 bash 终端中,让我们运行send.php
脚本几次:
因此,在receive.php
终端选项卡中,我们现在可以看到我们已经收到了我们发送的消息:
RabbitMQ 文档使用以下图表来描述消息的基本接受和转发:
发布-订阅模式
发布-订阅模式(或简称 Pub/Sub)是一种设计模式,其中消息不是直接从发布者发送到订阅者;相反,发布者在没有任何知识的情况下推送消息。
在 RabbitMQ 中,生产者从不直接发送任何消息到队列。生产者甚至经常不知道消息是否最终会进入队列。相反,生产者必须将消息发送到交换机。它从生产者那里接收消息,然后将它们推送到队列。
消费者是将接收消息的应用程序。
必须告诉交换机如何处理给定的消息,以及应该将其附加到哪个队列。这些规则由交换类型定义。
RabbitMQ 文档描述了发布-订阅关系(连接发布者、交换机、队列和消费者)如下:
直接交换类型根据路由键传递消息。它可以用于一对一和一对多形式的路由,但最适合一对一的关系。
扇出交换类型将消息路由到绑定到它的所有队列,并且路由键完全被忽略。实际上,您无法区分消息将基于路由键分发到哪些工作者。
主题 交换类型通过根据消息路由队列和用于将队列绑定到交换的模式来将消息路由到一个或多个队列。当有多个消费者/应用程序想要选择他们想要接收的消息类型时,这种交换有潜力很好地工作,通常是多对多的关系。
headers 交换类型通常用于根据消息头中更好地表达的一组属性进行路由。与使用路由键不同,路由的属性基于头属性。
为了测试发布/订阅队列,我们将使用以下脚本。它们与之前的示例类似,只是我修改了它们以便它们使用交换。这是我们的 send.php
文件:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword');
$channel = $connection->channel();
$channel->exchange_declare(
'helloHello', // exchange
'fanout', // exchange type
false, // passive
false, // durable
false // auto-delete
);
$msg = new AMQPMessage("Hello world!");
$channel->basic_publish(
$msg, // message
'helloHello' // exchange
);
$channel->close();
$connection->close();
echo "Sent hello world message." . PHP_EOL;
这是我们的 receive.php
文件。和之前一样,我修改了这个脚本,以便它也使用交换:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword');
$channel = $connection->channel();
$channel->exchange_declare(
'helloHello', // exchange
'fanout', // exchange type
false, // passive
false, // durable
false // auto-delete
);
$callback = function ($msg) {
echo "Received: " . $msg->body . PHP_EOL;
};
list($queueName, ,) = $channel->queue_declare("", false, false, true, false);
$channel->queue_bind($queueName, 'helloHello');
$channel->basic_consume($queueName, '', false, true, false, false, $callback);
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
现在,让我们测试这些脚本。我们首先需要运行我们的 receive.php
脚本,然后我们可以使用我们的 send.php
脚本发送消息。
首先,让我们触发我们的 receive.php
脚本,以便它开始运行:
发布-订阅模式 图片
完成后,我们可以通过运行我们的 send.php
脚本来发送消息:
发布-订阅模式 图片
这将在运行 receive.php
的终端中填充以下信息:
发布-订阅模式 图片
总结
在本章中,我们学习了架构模式。从 MVC 开始,我们学习了使用 UI 框架的好处和挑战,并讨论了如何以更严格的方式将我们的 UI 与业务逻辑解耦。
然后,我们转向了 SOA,并学习了它与微服务的比较,以及在分布式系统提出挑战的情况下,这样的架构在哪些情况下是合理的。
最后,我们深入了解了队列系统,它们适用的情况以及如何在 RabbitMQ 中实现它们。
在接下来的章节中,我们将介绍架构模式的最佳实践使用条件。
第七章:重构
在本书中,我主要关注使用设计模式来解决你编写的新代码;这是至关重要的,开发人员在批评他人的代码之前,必须首先改进自己的代码。开发人员必须首先努力理解如何编写代码,然后才能有效地重构代码。
本章将主要基于 Martin Fowler 等人的《重构:改善既有代码的设计》以及 Joshua Kerievsky 的《重构到模式》。如果您对此主题感兴趣,我强烈推荐阅读这些书籍。
什么是重构?
重构代码的一个关键主题是解决代码内部结构的问题,同时不改变被重构程序的外部行为。在某些情况下,这可能意味着在先前没有意图或考虑的地方引入内部结构。
重构作为一个过程,在编写代码后改进代码的设计。虽然设计是软件工程过程中的关键阶段,但通常被忽视(尤其是在 PHP 中);除此之外,长期维护代码结构需要对软件设计的持续理解。如果开发人员在不了解原始设计的情况下接手项目,他们可能会以非常粗糙的方式进行开发。
在极限编程(XP)中,使用了一个被称为“无情重构”的短语,这是不言而喻的。在 XP 中,重构被提议作为保持软件设计尽可能简单并避免不必要复杂性的机制。正如 XP 的规则中所述:“确保一切只表达一次。最终,制作一个精心打理的系统需要更少的时间。”
重构的一个关键原则是将软件设计视为一种要发现而不是事先创建的东西。在开发系统时,我们可以使用开发作为找到良好设计解决方案的机制。通过使用重构,我们能够确保系统在开发过程中保持良好,从而我们能够降低技术债务。
重构并非总是可能的,您可能偶尔会遇到无法更改的“黑盒”系统,甚至可能需要封装系统以进行重写。然而,有许多情况下,我们可以简单地重构代码以改进设计。
测试,测试,再测试
没有办法绕过这一点,为了重构代码,您需要一套可靠的测试。重构代码可能会减少引入错误的机会,但改变代码的设计会引入大量引入新错误的机会。
在重构过程中会出现意外的副作用,当类紧密耦合时,您可能会发现对一个函数进行微小更改会导致完全不同类中的负面副作用。
良好的重构效果需要良好的测试。这是无法绕过的。
除此之外,从更政治的角度来看,一些公司遇到了重复糟糕的重构努力所带来的不良影响,可能会不愿意重构代码;确保有良好的测试可以确保重构不会破坏功能。
在本章中,我将展示重构工作,这应该伴随着使用单元测试的测试工作,而在本书的下一章(也是最后一章)中,我将讨论行为测试(用于 BDD)。单元测试是开发人员测试给定代码单元的最佳机制;单元测试补充了代码结构,证明方法是否按照预期工作,并测试代码单元之间的交互;在这个意义上,它们是开发人员在重构工作中最好的测试形式。然而,行为测试是用来测试代码行为的,因此在演示应用程序能够成功完成给定形式的行为方面是有用的。
每个经验丰富的开发人员都会记得痛苦的调试任务;有时候会持续到深夜。让我们想想大多数开发人员日常工作的方式。他们并不总是编写代码,他们的一些时间花在设计代码上,而相当多的时间花在调试他们已经编写的代码上。拥有自我测试的代码可以迅速减轻这种负担。
测试驱动开发围绕着在编写功能之前编写测试的方法论,确实,代码应该与测试相匹配。
在测试类时,确保测试类的public
接口;确实,PHPUnit 不允许您在普通用法下测试private
或protected
方法。
代码异味
代码异味本质上是一些不良实践,使您的代码变得不必要地难以理解,可以使用本章中介绍的技术来重构不良代码。代码异味通常违反了一些基本的软件设计原则,因此可能会对整体代码的设计质量产生负面影响。
Martin Fowler 通过以下方式定义了代码异味:
“代码异味是通常对系统中更深层次问题的表面指示。”
在本书的开头,我们讨论了技术债务这个术语,在这个意义上,代码异味可以作为技术债务的一部分。
代码异味可能不一定构成错误,它不会阻止程序的执行,但它可以帮助在以后引入错误的过程中,并使代码重构到适当的设计变得更加困难。
让我们来看看在处理传统 PHP 项目时可能遇到的一些基本代码异味。
我们将讨论一些代码异味以及如何以相当简单的方式解决它们,但现在让我们考虑一些稍微重要的、重复出现的模式,以及如何通过应用设计模式来简化代码的维护。
在这里,我们将具体讨论重构到模式,有些情况下,当简化代码设计时,您可能会从模式重构到模式。本章的重复主题围绕代码设计如何在代码的开发生命周期中存在,它不仅仅在任意设计阶段之后被丢弃。
模式可以用来传达意图,它们可以作为开发人员之间的语言;这就是为什么了解并继续使用大量模式在软件工程师的职业生涯中至关重要。
在书籍重构到模式中还有更多这样的方法,我在这里挑选了对 PHP 开发人员最合适的方法。
长方法和重复的代码
重复的代码是非常常见的代码异味。开发人员经常会复制和粘贴代码,而不是使用适当的控制结构来进行应用程序。如果相同的控制结构出现在多个地方,将两个结构合并成一个将使您的代码受益。
如果重复的代码是相同的,你可以使用提取方法。那么什么是提取方法?实质上,提取方法只是将长函数中的业务逻辑提取到更小的函数中。
假设有一个dice
类,一旦掷骰子,它将以罗马数字返回 1 到 6 之间的随机数。
Legacy
类可以是这样的:
class LegacyDice
{
public function roll(): string
{
$rand = rand(1, 6);
// Switch statement to convert a number between 1 and 6 to a Roman Numeral.
switch ($rand) {
case 5:
$randString = "V";
break;
case 6:
$randString = "VI";
break;
default:
$randString = str_repeat("I", $rand);
break;
}
return $randString;
}
}
让我们提取一个方法,将随机数转换为罗马数字,并将其放入一个单独的函数中:
class Dice
{
/**
* Roll the dice.
* @return string
*/
public function roll(): string
{
$rand = rand(1, 6);
return $this->numberToRomanNumeral($rand);
}
/**
* Convert a number between 1 and 6 to a Roman Numeral.
*
* @param int $number
*
* @return string
* @throws Exception
*/
public function numberToRomanNumeral(int $number): string
{
if (($number < 1) || ($number > 6)) {
throw new Exception('Number out of range.');
}
switch ($number) {
case 5:
$randString = "V";
break;
case 6:
$randString = "VI";
break;
default:
$randString = str_repeat("I", $number);
break;
}
return $randString;
}
}
我们对原始代码块只进行了两个更改,我们将执行罗马数字转换的函数分离出来,并将其放入一个单独的函数中。我们用函数本身的 DocBlock 替换了内联注释。
如果重复存在于多个地方(且相同),则可以使用此方法进行复制,我们只需调用一个函数,而不是在多个地方重复代码。
如果代码在不相关的类中,看看它在逻辑上适合哪里(在这两个类中的任何一个或一个单独的类中),并将其提取到那里。
在本书的前面,我们已经讨论了保持函数小的必要性。这对于确保您的代码在长期内可读性非常重要。
我经常看到开发人员在函数内部注释代码块;相反,为什么不将这些方法拆分为它们自己的函数?通过 DocBlocks 可以添加可读的文档。因此,我们在这里使用的提取方法可以更简单地使用;拆分长方法。
处理较小的方法时,解决各种业务问题要容易得多。
大类
大类经常违反单一职责原则。在特定时间点上,您正在处理的类是否只有一个更改的原因?一个类应该只对功能的一个部分负责,而且该类应该完全封装该责任。
通过提取不严格符合单一职责的方法将类分成多个类,这是一种简单而有效的方法,可以帮助减轻这种代码异味。
用多态性或策略模式替换复杂的逻辑语句和 switch 语句
通过使用多态行为,可以大大减少 switch 语句(或者说无休止的大型 if 语句);我在本书的早期章节中已经描述了多态性,并且它提供了一种比使用 switch 语句更优雅地处理计算问题的方式。
假设您正在根据国家代码进行切换;美国或英国,而不是以这种方式切换,通过使用多态性,您可以运行相同的方法。
在不可能进行多态行为的情况下(例如,没有共同的接口的情况下),在某些情况下,通过用策略替换类型代码甚至可能会受益;实际上,您可以将多个 switch 语句合并为仅将类注入到客户端的构造函数中,该类将处理与各个类的关系。
例如;假设我们有一个 Output 接口,这个接口由包含load
方法的各种其他类实现。这个load
方法允许我们注入一个数组,并且我们以所请求的格式获取一些数据。这些类是该行为的极其粗糙的实现:
interface Output
{
public function load(array $data);
}
class Serial implements Output
{
public function load(array $data)
{
return serialize($data);
}
}
class JSON implements Output
{
public function load(array $data)
{
return json_encode($data);
}
}
class XML implements Output
{
public function load(array $data)
{
return xmlrpc_encode($data);
}
}
注意
在撰写本文时,PHP 仍然认为xmlrpc_encode
函数是实验性的,因此,我建议不要在生产中使用它。这里纯粹是为了演示目的(为了保持代码简洁)。
一个极其粗糙的实现,带有switch
语句,可能如下所示:
$client = "JSON";
switch ($client) {
case "Serial":
$client = new Serial();
break;
case "JSON":
$client = new JSON();
break;
case "XML":
$client = new XML();
break;
}
echo $client->load(array(1, 2));
但显然,我们可以通过实现一个允许我们将Output
类注入到Client
中的客户端来做很多事情,并相应地允许我们接收输出。这样的类可能是这样的:
class OutputClient
{
private $output;
public function __construct(Output $outputType)
{
$this->output = $outputType;
}
public function loadOutput(array $data)
{
return $this->output->load($data);
}
}
现在我们可以非常简单地使用这个客户端:
**$client = new OutputClient(new JSON());
echo $client->loadOutput(array(1, 2));**
在单一控制结构后复制代码
我不会在这里重申模板设计模式的工作原理,但我想解释的是,它可以用来帮助消除重复的代码。
我在本书中展示的模板设计模式有效地将程序的结构抽象化,然后我们只是填充了特定于实现的方法。这可以帮助我们通过避免一遍又一遍地重复单个控制结构来减少代码重复。
长参数列表和原始类型过度使用
原始类型过度使用是指开发人员过度使用原始数据类型而不是使用对象。
PHP 支持八种原始类型;这组可以进一步细分为标量类型、复合类型和特殊类型。
标量类型是保存单个值的数据类型。如果你问自己“这个值可以在一个范围内吗?”你可以识别它们。数字可以在X到Y的范围内,布尔值可以在 false 到 true 的范围内。以下是一些标量类型的例子:
-
布尔
-
整数
-
浮点数
-
字符串
复合类型由一组标量值组成:
-
数组
-
对象
特殊类型如下:
-
资源(引用外部资源)
-
NULL
假设我们有一个简单的Salary
计算器类,它接受员工的基本工资、佣金率和养老金率;在发送了这些数据之后,可以使用calculate
方法输入他们的销售额来计算他们的总工资:
class Salary
{
private $baseSalary;
private $commission = 0;
private $pension = 0;
public function __construct(float $baseSalary, float $commission, float $pension)
{
$this->baseSalary = $baseSalary;
$this->commission = $commission;
$this->pension = $pension;
}
public function calculate(float $sales): float
{
$base = $this->baseSalary;
$commission = $this->commission * $sales;
$deducation = $base * $this->pension;
return $commission + $base - $deducation;
}
}
注意构造函数有多长。是的,我们可以使用生成器模式来创建一个对象,然后将其注入到构造函数中,但在这种情况下,我们能够特别地将复杂的信息抽象化。在这种情况下,如果我们将员工信息移到一个单独的类中,我们可以确保更好地遵守单一职责原则。
第一步是分离类的职责,以便我们可以分离类的职责:
class Employee
{
private $name;
private $baseSalary;
private $commission = 0;
private $pension = 0;
public function __construct(string $name, float $baseSalary)
{
$this->name = $name;
$this->baseSalary = $baseSalary;
}
public function getBaseSalary(): float
{
return $this->baseSalary;
}
public function setCommission(float $percentage)
{
$this->commission = $percentage;
}
public function getCommission(): float
{
return $this->commission;
}
public function setPension(float $rate)
{
$this->pension = $rate;
}
public function getPension(): float
{
return $this->commission;
}
}
从这一点上,我们可以简化Salary
类的构造函数,以便它只需要输入Employee
对象,我们就能够使用该类:
class Salary
{
private $employee;
public function __construct(Employee $employee)
{
$this->employee = $employee;
}
public function calculate(float $sales): float
{
$base = $this->employee->getBaseSalary();
$commission = $this->employee->getCommission() * $sales;
$deducation = $base * $this->employee->getPension();
return $commission + $base - $deducation;
}
}
不当暴露
假设我们有一个Human
类如下:
class Human
{
public $name;
public $dateOfBirth;
public $height;
public $weight;
}
我们可以随心所欲地设置值,没有验证,也没有统一的获取信息的方式。这有什么问题吗?嗯,在面向对象编程中,封装的原则至关重要;我们隐藏数据。换句话说,我们的数据不应该在没有拥有对象知道的情况下被公开。
相反,我们用private
数据变量替换所有public
数据变量。除此之外,我们还添加了适当的方法来获取和设置数据:
class Human
{
private $name;
private $dateOfBirth;
private $height;
private $weight;
public function __construct(string $name, double $dateOfBirth)
{
$this->name = $name;
$this->dateOfBirth = $dateOfBirth;
}
public function setWeight(double $weight)
{
$this->weight = $weight;
}
public function getWeight(): double
{
return $this->weight;
}
public function setHeight(double $height)
{
$this->height = $height;
}
public function getHeight(): double
{
return $this->height;
}
}
确保 setter 和 getter 是合乎逻辑的,不仅仅是因为类属性存在。完成后,您需要检查应用程序,并替换任何对变量的直接访问,以便它们首先通过适当的方法。
然而,这现在暴露了另一个代码异味;特征嫉妒。
特征嫉妒
松散地说,特征嫉妒是指我们不让一个对象计算自己的属性,而是将其偏移到另一个类。
所以在前面的例子中,我们有我们自己的Salary
计算器类,如下:
class Salary
{
private $employee;
public function __construct(Employee $employee)
{
$this->employee = $employee;
}
public function calculate(float $sales): float
{
$base = $this->employee->getBaseSalary();
$commission = $this->employee->getCommission() * $sales;
$deducation = $base * $this->employee->getPension();
return $commission + $base - $deducation;
}
}
相反,让我们看看将这个函数实现到Employee
类本身中,结果我们也可以忽略不必要的 getter 并将属性合理地内部化:
class Employee
{
private $name;
private $baseSalary;
private $commission = 0;
private $pension = 0;
public function __construct(string $name, float $baseSalary)
{
$this->name = $name;
$this->baseSalary = $baseSalary;
}
public function setCommission(float $percentage)
{
$this->commission = $percentage;
}
public function setPension(float $rate)
{
$this->pension = $rate;
}
public function calculate(float $sales): float
{
$base = $this->baseSalary;
$commission = $this->commission * $sales;
$deducation = $base * $this->pension;
return $commission + $base - $deducation;
}
}
不当亲密关系
这在继承中经常发生;Martin Fowler 优雅地表达如下:
“子类总是会比父类更了解他们的父类。”
更一般地说;当一个字段在另一个类中的使用比在类本身中更多时,我们可以使用移动字段方法在新类中创建一个字段,然后将该字段的用户重定向到新类。
我们可以将这与移动方法结合起来,将一个函数放在最常使用它的类中,并从原始类中删除它,如果这不可能,我们可以简单地在新类中引用该函数。
深度嵌套的语句
嵌套的 if 语句很混乱且丑陋。这会导致难以理解的意大利面逻辑;而是使用内联函数调用。
从最内部的代码块开始,试图将该代码提取到自己的函数中,让它可以幸福地存在。在第一章中,我们讨论了如何通过示例实现这一点,但如果您经常进行重构,您可能希望考虑投资一种可以帮助您的工具。
这里有一个提示,对于我们中的 PHPStorm 用户:在重构菜单中有一个很好的小选项,可以自动为您执行此操作。只需高亮显示您希望提取的代码块,转到菜单栏中的重构,然后单击提取>方法。然后会弹出一个对话框,允许您配置如何进行重构:
删除对参数的赋值
尽量避免在函数体内设置参数:
class Before
{
function deductTax(float $salary, float $rate): float
{
$salary = $salary * $rate;
return $salary;
}
}
这可以通过正确设置内部参数来实现:
class After
{
function deductTax(float $salary, float $rate): float
{
$netSalary = $salary * $rate;
return $netSalary;
}
}
通过这样的行为,我们能够在前进时轻松识别和提取重复的代码,此外,它还可以在以后维护这段代码时更容易地替换代码。
这是一个简单的调整,允许我们识别代码中特定的参数在做什么。
注释
注释并不是一种代码气味,很多情况下,注释是非常有益的。正如 Martin Fowler 所说:
“在我们的嗅觉类比中,注释不是一种难闻的气味;事实上,它们是一种甜美的气味。”
然而,Fowler 继续演示了注释如何被用作遮盖代码气味的除臭剂。当你发现自己在函数内部注释代码块时,你可以找到一个很好的机会使用提取方法。
如果注释隐藏了一种难闻的气味,重构掉这种气味,很快你就会发现原始注释变得多余了。这并不是不需要对函数进行 DocBlock 或不必要地寻找代码注释的借口,但重要的是要记住,当您重构设计变得更简单时,特定的注释可能变得无用。
用构建器封装组合
正如本书前面讨论的那样,构建器设计模式可以通过我们将一长串参数转换为一个单一对象来工作,然后我们可以将其抛入另一个类的构造函数中。
例如,我们有一个名为APIBuilder
的类,这个构建器类可以用 API 的密钥和密钥本身来实例化,但一旦它被实例化为一个对象,我们就可以简单地将整个对象传递给另一个类的构造函数。
到目前为止,一切顺利;但我们可以使用这个构建器模式来封装组合模式。我们实际上只需创建一个构建器来创建我们的项目。通过这样做,我们可以更好地控制一个类,为我们提供了一个机会来导航和修改组合家族的整个树结构。
用观察者替换硬编码的通知
硬编码的通知通常是两个类紧密耦合在一起,以便一个能够通知另一个。相反,通过使用SplObserver
和SplSubject
接口,观察者可以使用更加可插拔的方式更新主题。在观察者中实现update
方法后,主题只需要实现Subject
接口:
SplSubject {
/* Methods */
abstract public void attach ( SplObserver $observer )
abstract public void detach ( SplObserver $observer )
abstract public void notify ( void )
}
结果的架构是一个更加可插拔的通知系统,不再紧密耦合。
用组合替换一个/多个区别
当我们有单独的逻辑来处理个体到组的情况时,我们可以使用组合模式来 consolide 这些情况。这是本书早些时候介绍过的一种模式;为了将其合并到这种模式中,开发人员只需要修改他们的代码,使一个类可以处理两种形式的数据。
为了实现这一点,我们必须首先确保这两个区别实现了相同的接口。
当我最初演示这个模式时,我写了关于如何使用这个模式来处理将单个歌曲和播放列表视为一个的情况。假设我们的Music
接口纯粹是以下内容:
interface Music
{
public function play();
}
关键任务就是确保这个接口对于单个和多个区分都得到遵守。你的Song
类和Playlist
类都必须实现Music
接口。这基本上是让我们能够对待它们的行为。
使用适配器分离版本
我不会在这本书中长篇大论地讨论适配器,因为我之前已经非常详细地介绍过它们,但我只是想让你考虑一下,它们可以用来支持不同版本的 API。
确保不要将多个 API 版本的代码放在同一个类中,而是可以将这些版本之间的差异抽象到一个适配器中。在使用这种方法时,我建议你最初尝试使用封装方法,而不是基于继承的方法,因为这样可以为未来提供更大的自由度。
我应该告诉我的经理什么?
重构然后添加功能往往比仅仅添加功能更快,同时也为现有代码库增加了价值。许多了解软件及其开发方式的优秀经理都会理解这一点。
当然,有些经理对软件的实际情况一无所知,他们往往只受到最后期限的驱使,可能不愿意更多地了解自己的专业领域。我在本书中之前提到过的那些可怕的开发人员就是这样。有时,Scrum Master也会有这种情况,因为他们可能无法理解整个软件开发生命周期。
正如 Martin Fowler 所说:
“当然,很多人说他们追求质量,但更多的是追求进度。在这些情况下,我给出了更具争议性的建议:不要说!”
不了解技术流程的经理可能会急于基于软件能够快速生产的基础上交付;重构可能是帮助生产软件最快速的方式。它提供了一种高效而彻底的方式来快速了解项目,并允许我们平稳地注入新功能的过程。
我们将在本书的下一章讨论管理以及项目如何有效地进行管理。
总结
在本章中,我们讨论了一些重构代码的方法,以确保设计始终保持良好的质量。通过重构代码,我们可以更好地理解我们的代码库,并为我们添加到软件中的额外功能未来做好准备。
简化和分解你面临的问题是重构代码时可以使用的两个最基本的工具。
如果你正在使用 CI 环境,让 PHP Mess Detector(PHPMD)在该环境中运行也可以帮助你编写更好的代码。
在下一章中,我将讨论如何适当地使用设计模式,首先快速介绍在网络环境中开发 API 的方法。
第八章:如何编写更好的代码
这是本书的最后一章。我们已经讨论了很多模式,但在本章中,我希望我们讨论一下如何应用这些模式。
我希望我们在这里讨论我们的代码如何相互配合的大局观,以及我们撰写优秀代码的关键要点。
除此之外,我想讨论模式在开发阶段适用于我们的应用程序的地方。
在本章中,我们将涵盖以下几点:
-
HTTP 请求的性质
-
RESTful API 设计
-
保持简单愚蠢
-
软件开发生命周期和工程实践
-
测试的重要性
-
对 BDD 的简要介绍
HTTP 请求的性质
许多开发人员发现 HTTP 请求被抽象化了;事实上,许多 PHP 开发人员永远不需要了解 HTTP 请求在幕后实际是如何工作的。
PHP 开发人员在开发时经常与 HTTP 网络一起工作。事实上,PHP 包含一些核心功能,非常适合在处理 HTTP 通信时使用。
让我们使用一个名为curl的工具,从高层次上看一下 HTTP 请求。curl 本质上是一个命令行工具,允许我们模拟网络请求。它允许您使用各种协议模拟数据传输。
注意
- cURL 的名称最初代表查看 URL*。
curl 项目同时产生libcurl
和curl
命令行工具。Libcurl 是 PHP 支持的库,允许您在 PHP 中连接和通信多种协议,前提是您的安装中已经安装了它。
然而,在这种情况下,我们将使用命令行工具来模拟请求。
让我们从对给定网站进行简单的curl
请求开始,如下所示:
**curl https://junade.com**
根据您在命令中查询的站点,您会注意到终端输出为空:
这里发生了什么?为了找出,我们需要深入挖掘一下。
您可以在curl
命令中使用-v
参数,以便查看正在进行的详细输出:
**curl -v http://junade.com**
这个输出实际上是截然不同的:
通过这个输出,我们可以看到发送的标头和接收的标头。
以星号*
开头的块表示正在建立连接。我们可以看到 curl 如何重建 URL,使其正确(包含末尾的斜杠),然后解析服务器的 IP 地址(在我的情况下是 IPv6 地址),最后建立与 Web 服务器的连接:
* Rebuilt URL to: http://junade.com/
* Trying 2400:cb00:2048:1::6810:f005...
* Connected to junade.com (::1) port 80 (#0)
主机名通过查询 DNS 服务器转换为 IP 地址;我们稍后会更详细地讨论这一点。但在这一点上,重要的是要记住,在这一点之后,使用 IP 地址建立与服务器的连接。
如果我们去掉末尾的斜杠,我们实际上可以看到在第一行中,重建 URL 将消失,因为在我们发出请求之前,它已经以正确的格式存在:
接下来让我们看看后续行中的星号。我们看到了大于号>
中的出站标头。
这些标头看起来像这样:
> GET / HTTP/1.1
> Host: junade.com
> User-Agent: curl/7.43.0
> Accept: */*
>
因此,我们看到的第一条消息是请求方法GET
,然后是端点/
和协议HTTP/1.1
。
接下来,我们看到了Host
标头,它告诉我们服务器的域名,也可以包含服务器正在监听的 TCP 端口号,但如果端口是所请求服务的标准端口,则通常会被修改。但是,为什么需要这个呢?假设服务器包含许多虚拟主机;这实际上是允许服务器使用标头区分虚拟主机的内容。虚拟主机本质上允许服务器托管多个域名。为了做到这一点,我们需要这个标头;当服务器看到 HTTP 请求进来时,它们不会看到这个标头。
还记得我说过连接是使用 IP 地址建立的吗?这个Host
头部允许我们通过发送主机名变量来指示 IP 地址是什么。
接下来,我们看到了User-Agent
头部,指示客户端使用的浏览器;在这个请求中,我们的User-Agent
头部表示我们正在使用 curl 命令发送我们的 HTTP 请求。记住不要相信来自客户端的任何 HTTP 头部,因为它们可以被操纵以包含恶意对手想要放入其中的任何数据。它们可以包含从伪造的浏览器标识符到 SQL 注入的一切。
最后,Accept
头部指示了响应可接受的Content-Type
头部。在这里,我们看到了通配符接受,表示我们愿意接收服务器发送给我们的任何内容。在其他情况下,我们可以使用Accept: text/plain
来表示我们想要看到纯文本,或者Accept:application/json
来表示 JSON。我们甚至可以通过使用Accept: image/jpg
来指定是否要接收 JPG 图像。
还有各种参数也可以通过Accept
头部发送;例如,我们可以使用Accept: text/html; charset=UTF-8
来请求使用 UTF-8 字符集的 HTML。
在基本级别上,这个头部中允许的语法看起来像这样:
top-level type name / subtype name [ ; parameters ]
服务器可以使用响应中的Content-Type
头部指示返回给用户的内容类型。因此,服务器可以向最终用户发送一个头部,如下所示:
Content-Type: text/html; charset=utf-8
关于响应的话题,让我们来看看响应。这些都是以<:为前缀的。
< HTTP/1.1 301 Moved Permanently
< Date: Sun, 10 Jul 2016 18:23:22 GMT
< Transfer-Encoding: chunked
< Connection: keep-alive
< Set-Cookie: __cfduid=d45c9e013b12286fe4e443702f3ec15f31468175002; expires=Mon, 10-Jul-17 18:23:22 GMT; path=/; domain=.junade.com; HttpOnly
< Location: https://junade.com/
< Server: cloudflare-nginx
< CF-RAY: 2c060be42065346a-LHR
<
因此,我们在响应中首先得到的是格式和状态码。HTTP/1.1 表示我们正在接收一个HTTP/1.1
响应,而301 Moved Permanently
消息表示永久重定向。因此,我们还收到了一个Location: https://junade.com/
头部,告诉我们接下来去哪里。
Server
头部指示了提供我们请求的网络服务器的签名。它可以是 Apache 或 Nginx;在这种情况下,它是 CloudFlare 用于他们的网络的修改版本的 Nginx。
Set-Cookie
头部用于指示浏览器应该设置哪些 cookie;这方面的标准在一份名为 RFC 6265 的文档中。
RFC代表请求评论;有许多类型的 RFC。标准跟踪 RFC 是那些打算成为互联网标准(STDs)的 RFC,而信息性 RFC 可以是任何东西。还有许多其他类型的 RFC,比如实验性的,最佳当前实践,历史性的,甚至是未知的 RFC 类型,用于那些如果今天发布的话状态不清楚的 RFC。
Transfer-Encoding
头部指示了用于将实体传输给用户的编码,可以是任何东西,从分块甚至到像 gzip 这样的压缩实体。
有趣的是,2015 年 5 月发布的 RFC 7540 实际上允许头部压缩。如今,我们发送的头部数据比创建HTTP/1
协议时原始传输的数据更多(原始的HTTP
协议甚至没有Host
头部!)。
Connection
头部提供了连接的控制选项。它允许发送者指定当前连接所需的选项。最后,Date
头部指示了消息发送的日期和时间。
考虑一下:一个 HTTP 请求/响应中是否可以包含多个相同名称的头部?
是的,这在一些头部中特别有用,比如Link
头部。这个头部用于执行HTTP/2
服务器推送;服务器推送允许服务器在被请求之前向客户端推送请求。每个头部可以指定一个资源;因此,需要多个头部来推送多个资源。
这是我们在 PHP 中可以做的事情。在 PHP 中,使用以下header
函数调用:
header("Link: <{$uri}>; rel=preload; as=image", false);
虽然第一个参数是我们发送的实际标头的字符串,但第二个参数(false
)表示我们不希望替换同样的先前标头,而是希望发送这个标头,但不替换它。通过将此标志设置为true
,我们反而声明要覆盖先前的标头;如果根本没有指定标志,则这是默认选项。
最后,当请求关闭时,您将看到最终的星号,表示连接已关闭:
* Connection #0 to host junade.com left intact
通常,如果有主体,它将出现在主体下面。在此请求中,由于只是重定向,所以没有主体。
现在,我将使用以下命令向Location
标头指向的位置发出curl
请求:
**curl -v https://junade.com/**
您现在会注意到,连接关闭消息出现在 HTML 主体结束后:
现在让我们尝试探索一些 HTTP 方法。在 REST API 中,您经常会使用GET
、POST
、PUT
和DELETE
;但首先,我们将先探索另外两种方法,HEAD
和OPTIONS
。
HTTP OPTIONS
请求详细说明了您可以在给定端点上使用哪些请求方法。它提供了有关特定端点可用的通信选项的信息。
让我演示一下。我将使用一个名为HTTPBin
的服务,它允许我通过 curl 向真实服务器发出请求并获得一些响应。
这是我使用 curl 发出的OPTIONS
请求:
**curl -v -X OPTIONS https://httpbin.org/get**
-X
选项允许我们指定特定的 HTTP 请求类型,而不仅仅是默认的 curl。
让我们看看执行后的样子:
首先,您会注意到,由于请求是通过 HTTP 进行的,您将在星号中看到一些额外的信息;这些信息包含用于加密连接的 TLS 证书信息。
看看以下一行:
TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS 1.2
表示我们正在处理的传输层安全版本;第二部分,即TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
,表示连接的密码套件。
密码套件首先详细说明了我们正在处理的“TLS”。 ECDHE_RSA 表示密钥交换是使用椭圆曲线 Diffie-Hellman 完成的。密钥交换基本上允许安全地传输加密密钥。通过使用椭圆曲线密码学,可以共享特定的密钥,然后可以用于以后加密数据。 ECDHE_RSA
表示我们使用椭圆曲线 Diffie-Hellman 来共享基于服务器获取的 RSA 密钥的密钥。还有许多其他密钥交换算法;例如,ECDH_ECDSA
使用带有 ECDSA 签名证书的固定 ECDH。
以 access-control 为前缀的标头用于一种称为 CORS 的机制,它基本上允许 JavaScript 进行跨源 API 请求;让我们不在这里担心这个。
我们需要担心的OPTIONS
请求的标头是Allow
标头。这详细说明了我们被允许向特定端点提交哪些请求方法。
因此,这是我们查询/get
端点时收到的请求:
**< Allow: HEAD, OPTIONS, GET**
请注意,我在此处使用的端点使用了/get
端点。相反,让我们使用以下curl
请求向/post
端点发出另一个OPTIONS
请求:
**curl -v -X OPTIONS https://httpbin.org/post**
这是我们收到的回复:
您会注意到,Allow
标头现在包含POST
和OPTIONS
。还要注意,HEAD
选项已经消失。
您很快会发现,HEAD
请求与GET
请求非常相似,只是没有消息主体。它仅返回 HTTP 请求的标头,而不是请求的主体。因此,它允许您获取有关实体的元信息,而无需获取完整的响应。
让我们向/get
端点发出 HEAD 请求:
**curl -I -X HEAD https://httpbin.org/get**
在这个请求中,我没有使用-v
(冗长)选项,而是使用了-I
选项,它只会获取HTTP
头。这非常适合使用HEAD
选项进行 HTTP 请求:
正如您所看到的,我们在Content-Type
头部中得到了响应的类型。除此之外,您还将在Content-Length
头部中得到请求的长度。长度以八位字节(8 位)来衡量;您可能认为这与字节相同,但在所有架构上,字节并不一定是 8 位。
还有许多其他头部可以发送以表达元信息。这可能包括标准头部或非标准头部,以表达您无法在标准化的 RFC 支持的头部中表达的其他信息。
HTTP ETags(实体标签)是一种提供缓存验证的机制。您可以在 RESTful API 的上下文中使用它们进行乐观并发控制;这基本上允许多个请求完成而无需相互干预。这是一个非常先进的 API 概念,所以我在这里不会详细介绍。
请注意,在我们的HTTP HEAD
和OPTIONS
请求中,我们都收到了200 OK
头消息。200
状态代码表示成功的 HTTP 请求。
有许多不同类型的状态代码。它们被分类如下:
-
1xx 消息:信息
-
2xx 消息:成功
-
3xx 消息:重定向
-
4xx 消息:客户端错误
-
5xx 消息:服务器错误
信息头可能是101
响应,表示客户端正在切换协议,服务器已同意这样做。如果您正在开发 RESTful API,您可能不会遇到信息头消息;这些最有可能是由 Web 服务器发送的,这对于您作为开发人员来说是抽象的。
使用其他 HTTP 状态代码的正确方式对于正确开发 API 至关重要,特别是对于 RESTful API。
成功状态代码不仅限于200 OK
消息;201 Created 表示已满足已创建新资源的请求。当使用PUT
请求创建新资源或使用POST
创建子资源时,这是特别有用的。202 Accepted
表示已接受请求进行处理,但处理尚未完成,这在分布式系统中非常有用。204 No Content
表示服务器已处理请求并且不返回任何信息;205 Reset Content
头部也是如此,但要求请求者重置其文档视图。这只是一些 200 的消息;显然还有许多其他消息。
重定向消息包括301 Moved Permanently
,这是我们在第一个curl
示例中展示的,而302 Found
可以用于更临时的重定向。同样,还有其他消息代码。
客户端错误代码包括臭名昭著的404 Not Found
消息,当找不到资源时。除此之外,我们还有401 Unauthorized
,表示需要身份验证但未提供,403 Forbidden
表示服务器拒绝响应请求(例如,权限不正确)。405 Method Not Allowed
允许我们基于使用无效请求方法提交请求来拒绝请求,这对于 RESTful API 非常有用。405 Not Acceptable
是一个响应,其中服务器无法根据发送给它的Accept
头部生成响应。同样,还有许多其他 4xx 的 HTTP 代码。
注意
HTTP 代码 451 表示由于法律原因请求不可用。选择的代码是华氏 451 度,一部以 451 华氏度为书名的小说,作者声称 451 华氏度是纸张的自燃温度。
最后,服务器错误
允许服务器指示他们未能满足明显有效的请求。这些消息包括500 Internal Server Error
,这是在遇到意外条件时给出的通用错误消息。
现在让我们来看一下如何进行GET
请求。默认情况下,curl
会发出一个GET
请求,如果我们没有指定要发送的数据或特定的方法:
**curl -v https://httpbin.org/get**
我们也可以指定我们想要一个GET
请求:
**curl -v -X GET https://httpbin.org/get**
这个输出如下:
在这里,你可以看到我们得到了与HEAD
请求中相同的标头,另外还有一个主体;一些 JSON 数据,无论我们试图访问的资源是什么。
在这里我们得到了一个200 Success
的消息,但让我们向一个不存在的端点发出 HTTP 请求,这样我们就可以触发一个 404 消息:
正如你所看到的,我们得到了一个标头,上面写着404 NOT FOUND
,而不是我们通常的200 OK
消息。
HTTP 404
响应也可以没有主体:
虽然GET
请求只是显示一个现有的资源,POST
请求允许我们修改和更新一个资源。相反,PUT
请求允许我们创建一个新资源或覆盖一个资源,但是特定于给定的端点。
有什么区别?PUT
是幂等的,而POST
不是幂等的。PUT
就像设置一个变量,$x = 3
。你可以一遍又一遍地做,但输出是一样的,$x
是3
。
POST
就像运行$x++
一样;它会引起一个不是幂等的变化,就像$x++
不能一遍又一遍地重复以得到完全相同的变量一样。POST
更新一个资源,添加一个辅助资源,或者引起一个变化。当你知道要创建的 URL 时,就会使用PUT
。
当你知道创建资源的工厂的 URL 时,可以使用POST
来创建。
因此,例如,如果端点/用户想要生成一个具有唯一 ID 的用户帐户,我们将使用这个:
**POST /user**
但是,如果我们想要在特定的端点创建一个用户帐户,我们将使用PUT
:
**PUT /user/tom**
同样,如果我们想要在给定的端点上覆盖tom
,我们可以在那里放置另一个PUT
请求:
**PUT /user/tom**
但假设我们不知道 Tom 的端点;相反,我们只想向一个带有用户 ID 参数的端点发送PUT
请求,并且一些信息将被更新:
**POST /user**
希望这是有意义的!
现在让我们来看一个给定的HTTP POST
请求。
我们可以使用 URL 编码的数据创建一个请求:
**curl --data "user=tom&manager=bob" https://httpbin.org/post**
请注意,如果我们在curl
中指定了数据但没有指定请求类型,它将默认为POST
。
如果我们执行这个,你会看到Content-Type
是x-www-form-urlencoded
:
然而,如果 API 允许我们并接受这种格式,我们也可以向端点提交 JSON 数据:
**curl -H "Content-Type: application/json" -X POST -d '{"user":"tom","manager":"bob"}' https://httpbin.org/post**
这提供了以下输出,注意Content-Type
现在是 JSON,而不是之前的x-www-form-urlencoded
表单:
现在我们可以通过向/put
端点发送相同的数据来进行PUT
的 HTTP 请求:
**curl -H "Content-Type: application/json" -X PUT -d '{"user":"tom","manager":"bob"}' https://httpbin.org/put**
让我们把请求类型改成PUT
:
让我们使用以下curl
请求向DELETE
端点发送相同的请求(在这个例子中,我们将提交数据):
**curl -H "Content-Type: application/json" -X DELETE -d '{"user":"tom"}' https://httpbin.org/delete**
这有以下输出:
在现实世界中,你可能并不一定需要提交与我们刚刚删除一个资源相关的任何信息(这就是DELETE
的作用)。相反,我们可能只想提交一个204 No Content
消息。通常,我不会传回消息。
HTTP/2
在高层次上维护了这个请求结构。请记住,大多数HTTP/2
实现都需要 TLS(h2
),而大多数浏览器不支持明文传输的HTTP/2
(h2c
),尽管在 RFC 标准中实际上是可能的。如果使用HTTP/2
,你实际上需要在请求上使用 TLS 加密。
哇!这真是一大堆,但这就是你需要了解的关于 HTTP 请求的一切,从一个非常高的层次来看。我们没有深入网络细节,但这种理解对于 API 架构是必要的。
现在我们对 HTTP 请求和 HTTP 通信中使用的方法有了很好的理解,我们可以继续了解什么使 API 成为 RESTful。
RESTful API 设计
许多开发人员在不了解何为 RESTful 的情况下使用和构建 REST API。那么REpresentational State Transfer到底是什么?此外,为什么 API 是RESTful很重要?
API 成为 RESTful 的一些关键架构约束,其中第一个是其无状态性质。
无状态性质
RESTful API 是无状态的;客户端的上下文在请求之间不会存储在服务器上。
假设您创建了一个具有登录功能的基本 PHP 应用程序。在验证放入登录表单的用户凭据之后,您可以使用会话来存储已登录用户的状态,因为他们继续进行下一个状态以执行下一个任务。
这在 REST API 中是不可接受的;REST 是一种无状态协议。REST 中的ST代表State Transfer;请求的状态应该被传输而不仅仅存储在服务器上。通过传输会话而不是存储它们,您可以避免具有粘性会话或会话亲和性。
为了很好地实现这一点,HTTP 请求在完全隔离的情况下发生。服务器需要执行GET
,POST
,PUT
或DELETE
请求的所有内容都在 HTTP 请求本身中。服务器从不依赖于先前请求的信息。
这样做的好处是什么?首先,它的扩展性更好;最明显的好处是您根本不需要在服务器上存储会话。这还带来了额外的功能,当您将 API Web 服务器放在负载均衡器后面时。
集群是困难的;使用状态对 Web 服务器进行集群意味着您需要具有粘性负载平衡,或者在会话方面需要具有共同的存储。
版本控制
对 API 进行版本控制,您需要进行更改,而不希望它们破坏客户端的实现。这可以通过标头或 URL 本身来完成。例如,可以使用/api/resource.json
而不是/api/v1/resource.json
这样的版本标签。
您还可以实现HTTP Accept
标头来执行此行为,甚至可以设置自己的标头。客户端可以发送一个带有API-Version
标头设置为2
的请求,服务器将知道使用 API 的第 2 个版本与客户端进行通信。
过滤
使用参数查询,我们可以使用参数来过滤给定的内容。如果我们在/orders
端点上处理订单系统,那么实现基本过滤就相当容易。
在这里,我们使用state
参数来过滤未完成的订单:
**GET /orders?state=open**
排序
我们还可以添加一个sort
参数来按字段排序。sort
字段反过来包含一个逗号分隔的列列表,以便进行排序;列表中的第一个是最高的排序优先级。为了进行负排序,您可以在列前加上负号-
-
GET /tickets?sort=-amount
:按金额降序排序订单(最高优先)。 -
GET /tickets?sort=-amount,created_at
:按金额降序排序订单(最高优先)。在这些金额中(具有相同金额的订单),较早的订单首先列出。
搜索
然后,我们可以使用一个简单的参数进行搜索查询,然后可以通过搜索服务(例如 ElasticSearch)路由该查询。
假设我们想要搜索包含“refund”短语的订单,我们可以为搜索查询定义一个字段:
**GET /orders?q=refund**
限制字段
此外,使用fields
参数,我们可以查询特定字段:
**GET /orders?fields=amount,created_at,customer_name,shipping_address**
返回新字段
PUT,POST 或 PATCH 可以更改我们更新的字段以外的其他条件。这可能是新的时间戳或新生成的 ID。因此,我们应该在更新时返回新的资源表示。
在创建资源的POST
请求中,您可以发送一个HTTP 201 CREATED
的消息,以及一个指向该资源的Location
头。
当有疑问时-保持简单
KISS是保持简单,愚蠢的缩写。
KISS 原则指出,大多数系统最好保持简单而不是复杂。在整个编程过程中,牢记这一原则至关重要。
决定使用一些预定义的设计模式来编写程序通常是一个不好的主意。代码永远不应该被强制进入模式中。虽然为设计模式编写代码可能对于“Hello World”演示模式有效,但通常情况下效果并不好。
设计模式存在是为了解决代码中常见的重复问题。重要的是它们被用来解决问题,而不是在没有这样的问题存在的地方实施。通过尽可能简化代码并减少整个程序的复杂性,您可以减少失败的机会。
英国计算机协会发布了一份名为《IT 项目高级管理》的建议,表明项目、人员、利益、复杂性和进展都必须得到充分理解;除此之外,项目的全面理解也是至关重要的。为什么要完成这个项目?有哪些风险?如果项目出现偏离,有什么恢复机制?
复杂系统必须能够优雅地处理错误才能够健壮。冗余必须与复杂性平衡。
软件开发生命周期
这张图表是一个开源图表,描述了软件开发的步骤:
有许多不同类型的软件生产流程,但所有流程都必须包含图表中显示的步骤,因为它们对软件工程流程至关重要。
虽然现在几乎普遍认为瀑布式软件工程方法已不再适用,但替代它的敏捷方法仍需要一些设计(尽管规模较小且更迭代),以及强大的测试实践。
重要的是,软件开发不应该被视为显微镜下的东西,而应该在软件工程的更广泛视野中看待。
关于 Scrum 和真正的敏捷
Scrum 是一种迭代的软件开发框架,它声称是敏捷的,基于 Scrum 联盟发布的流程。它的图表如下:
我们许多人看到了认证 Scrum 大师在软件开发团队中留下的灾难,他们大多将敏捷作为一个噱头,提供一些简直愚蠢的软件编写流程。
敏捷宣言以“个体和互动优于流程和工具”开始。Scrum 是一个流程,而且是一个严格定义的流程。Scrum 经常以开发过程凸显于团队的方式实施。如果这一部分有一个要点,那就是记住“人胜于流程”这个短语。如果你选择实施 Scrum,你必须愿意适应和改变其流程以应对变化。
敏捷的整个意义在于灵活;我们希望能够迅速适应不断变化的需求。我们需要灵活性,不希望受到严格定义的流程的限制,这会阻碍我们迅速适应不断变化的需求。
填写时间表、采购订单和处理官僚治理流程并不能帮助将软件交到客户手中,因此如果不能交付,就必须尽量简化。
时间表是完全浪费的完美想法。它们只是用来监控开发人员的表现,尽管在一些管理层中,他们会假装它们有一些神奇的敏捷好处。无论如何,它们肯定不会帮助你做出更好的软件估算;敏捷环境应该寻求使用预测而不是预测。
我见过一些 Scrum Master 不断重复这句话:“没有一个战斗计划能在与敌人接触后生存下来”;同时又强制执行严格的预测方案。
在现实世界中,准确的预测是个矛盾。你无法对不确定的事情进行准确预测,而且在几乎所有情况下,开发人员都不会充分了解他们正在处理的系统。此外,他们也不知道自己的个人效率从一天到另一天的变化;这是无法准确预测的。
我甚至遇到过这样的环境,严格的预测(通常甚至不是由开发人员自己制定的)是通过严格的纪律程序强制执行的。
通过将问题分解并以小块的方式解决问题来减少复杂性是很好的做法;将庞大的程序员团队分成小团队也是很好的做法。
在这些小团队(通常被称为部落)中开发的系统之间,通常需要系统架构师来确保团队之间保持一致性。
Spotify 使用部落架构来开发软件;事实上,我强烈建议阅读 Henrik Kniberg 和 Anders Ivarsson 的论文Scaling Agile @ Spotify with Tribes, Squads, Chapters & Guilds。
这位系统架构师确保所有构建的不同服务之间保持一致性。
转向具体的 Scrum,Scrum 是一种敏捷流程。Scrum 指南(是的,它甚至是一个商标)在一份 16 页的文件中定义了 Scrum 的规则。
敏捷方法包含许多不同的流程以及许多其他方法论;敏捷是一个非常广泛的知识库。
Scrum Master 喜欢假装敏捷发生在开发团队的孤立环境中。这与事实相去甚远;整个组织结构都影响 Scrum。
极限编程(XP)是一个非常广泛的流程,人们在很大程度上理解这些流程之间的互动。通过挑选这些流程,你最终得到的是一个无效的流程;这就是为什么 Scrum 会遇到困难。
需求会变化;这包括它们在 Sprint 进行中发生变化。当 Scrum Master 坚持在 Sprint 开始后不进行任何更改时,这会使团队更无法有效地应对真正的变化。
在敏捷机制中开发时,我们必须记住我们的软件必须足够弹性以应对不断变化的需求(导致软件设计不断变化)。你的软件架构必须能够应对变化的压力。因此,开发人员也必须理解并参与到实现足够弹性的软件所需的技术流程中。
不能灵活应对变化的公司比能够灵活应对变化的公司效率低;因此,他们在商业世界中具有重要优势。在选择公司时,它们的敏捷性不仅仅关乎你的工作质量,也关乎你的工作安全。
我的观点很简单;在实施流程时要认真对待技术实践,并且不要盲目遵循荒谬的流程,因为这可能会损害整个业务。
开发人员不应该被像孩子一样对待。如果他们不能编码或者编写糟糕的代码,他们就不能继续作为开发人员被雇佣。
实质上,为了管理风险,最好查看你的积压工作并利用历史进展来创建关于项目进展的预测。经理的角色应该是消除阻碍开发人员工作的障碍。
最后,如果你在一个 Scrum Master 对软件开发(甚至对敏捷)理解很差的团队中,要坚决提醒他们,人必须高于流程,真正的敏捷性是由能够经受变化压力的代码所支持的。
Scrum Master 有时会争辩说敏捷意味着没有预先设计。这是不正确的,敏捷意味着没有大量的预先设计。
有时候你需要解雇人
我曾在开发环境中工作过,那里的经理们太害怕解雇员工,要么就是通过对开发人员进行惩罚来折磨他们,因为他们显然无法胜任工作,要么就是让他们在开发过程中肆意破坏。
有才华的开发人员对糟糕的代码或不公平的技能基础感到失望。其他开发人员在被迫进行维护时,往往会陷入维护噩梦。面对维护噩梦的前景(或很可能是不断加剧的维护噩梦),他们会辞职。
另一方面,为了弥补糟糕的开发人员而施加的限制性工作条件会让有才华的开发人员感到失望。厌倦了被当作白痴对待(因为其他开发人员是白痴),他们会接受更好的公司提供的工作机会,那里有更好的职业前景,更好的工作环境和更快乐、更有才华的员工。他们接受这个工作机会,因为他们要去的公司很可能也有更好的业务前景和更好的补偿,同时还有更快乐的工程师和更好的工作环境。
在这种情况下还有一个更极端的情况;企业声誉受损,无法雇佣永久开发人员;他们会支付昂贵的合同开发人员费用,同时冒险使用他们的技能。在支付合同开发人员的费用时,企业可能会选择任何愿意参与这些项目的人。这些开发人员的面试官可能没有问对问题,导致对被雇佣的承包商的质量进行了大赌注。公司减少了雇佣优秀永久员工的机会,企业陷入了恶性循环,公司的衰落变得更加严重。我曾多次见到这种情况;每次公司都面临着缓慢而痛苦的衰退。如果你曾被邀请加入类似的公司,我强烈建议你寻找其他地方,除非你真的相信你能够为这样的组织带来改革。
如果你在这样的组织中担任管理工作,确保你有能力进行有意义的改变,有权雇用合适的人并解雇错误的人。如果没有,你在这样的组织中的任期只会是在试图转移责任,同时遭受高员工流失率的困扰。
有才华的员工是值得信任的;那些对自己的工作充满热情的人不需要限制来防止他们偷懒。
如果有才华的员工无法履行职责,那么你的开发人员很可能不只是懒惰;你需要消除对开发的限制性官僚流程。
强迫执行对将软件交付给用户没有任何价值的仪式是对开发团队没有任何帮助的。
精益项目管理
精益项目管理使您能够定期交付业务价值,而不是基于需求、功能和功能列表。
《改变世界的机器》一书是基于麻省理工学院对汽车工业进行的 500 万美元、5 年的研究,使精益生产这个术语世界闻名。
这本书提出了精益的以下原则:
-
确定客户并明确价值
-
确定和映射价值流
-
通过消除浪费来创造流程
-
响应客户需求
-
追求完美
基于这一点,软件开发的精益原则主要基于精益生产的制造原则:
-
消除浪费
-
加强学习
-
尽量晚做决定
-
尽快交付
-
激发团队的力量
-
建立完整性
-
看整体
通过可重用的组件、自动化部署和良好的架构,可以帮助实现这一目标。
YAGNI 和推迟决策
你不会需要它 - 你不需要添加功能,直到有必要。只添加对项目成功至关重要的东西。你可能不需要很多功能来完成你的 Web 应用的第一个版本;最好推迟到必要时再添加。
通过推迟不必要的功能,你可以保持软件设计尽可能简单。这有助于你应对变化的速度。在软件开发过程的后期,你将更加了解需求,更重要的是,你的客户将对他们想要产品发展的方向有更精确的预测。
当你在以后做软件决策时,你会有更多的数据和更多的教育。有些决策必须提前做出,但如果你能推迟它们,那通常是一个好主意。
监控
随着规模的扩大,监控系统变得至关重要。有效的监控可以极大地简化服务的维护。
在这一领域与多位专家交谈后,这是我收集到的建议:
-
小心选择你的关键统计数据。用户不在乎你的机器 CPU 是否低,但他们在乎你的 API 是否慢。
-
使用聚合器;考虑服务,而不是机器。如果你有超过几台机器,你应该将它们视为一个无定形的块。
-
避免图表墙。它们很慢,对人类来说信息过载。每个仪表板应该有五个图表,每个图表不超过五条线。
-
分位数不可聚合,很难得到有意义的信息。然而,平均数更容易理解。第一四分位的响应时间为 10 毫秒并不是真正有用的信息,但平均响应时间为 400 毫秒显示出一个明显的需要解决的问题。
-
此外,平均数比分位数更容易计算。它们在计算上很容易,并且在需要扩展监控系统时特别有用。
-
监控是有成本的。要考虑资源是否真的值得。1 秒的监控频率真的比 10 秒的监控更好吗?成本是否值得?监控不是免费的,它有计算成本。
-
也就是说,Nyquist-Shannon 采样定理表明,如果你每 20 秒采样一次,就无法重建 10 秒间隔的模式。假设有一个服务每 10 秒就崩溃或减慢你的计算机系统的速度-这是无法检测到的。在数据分析过程中要牢记这一点。
-
相关性不等于因果关系-小心确认偏见。在采取任何激烈行动之前,一定要确保建立起导致特定问题的正式关系。
-
日志和指标都很好。日志让你了解细节,指标让你了解高层次。
-
有一种方法来处理非关键警报。你在 Web 服务器日志文件中的所有 404 错误该怎么办?
-
记住之前提到的 KISS 原则;尽可能保持你的监控简单。
测试对抗遗留
自动化测试是对抗遗留代码的最佳工具。
通过拥有自动化测试,如单元测试或行为测试,你能够有信心有效地重构遗留代码,几乎不会破坏。
糟糕的系统通常由紧密耦合的函数组成。一个类中的函数的更改很可能会破坏完全不同类中的函数,导致更多类被破坏,直到整个应用程序被破坏。
为了解耦类并遵循单一职责原则等实践,必须进行重构。任何重构工作都必须确保不会破坏应用程序中的其他代码。
这就引出了测试覆盖率的话题:这是一个真正有意义的数字吗?
阿尔贝托·萨沃亚在 artima.com 上发布了一个有趣的轶事,最好地回答了这个问题;让我们来看一下:
清晨,一位程序员问大师:“我准备写一些单元测试。我应该追求什么代码覆盖率呢?”
大师回答道:“不要担心覆盖率,只是写一些好的测试。”
程序员微笑着鞠躬离开了。
...
当天晚些时候,第二位程序员问了同样的问题。大师指着一锅开水说:“我应该往锅里放多少粒米?”
程序员困惑地回答道:“我怎么可能告诉你呢?这取决于你需要喂多少人,他们有多饿,你还提供了什么其他食物,你有多少大米可用,等等。”
“没关系,”大师说。
第二位程序员微笑着鞠躬离开了。
...
一天结束时,第三位程序员也问了同样关于代码覆盖率的问题。
“80%以上,不可少!”大师用严厉的声音回答,一边拍着桌子。
第三位程序员微笑着鞠躬离开了。
...
在这之后,一位年轻的学徒走向了大师:
“大师,今天我听到您对同一个问题给出了三个不同的答案。为什么呢?”
大师站起来,说:“跟我一起喝杯新茶,我们谈谈这个问题。”
在他们的杯子里倒满了冒着热气的绿茶后,大师开始回答:“第一位程序员是新手,刚刚开始测试。现在他有很多代码但没有测试。他还有很长的路要走;此时专注于代码覆盖率会令人沮丧且毫无用处。他最好只是习惯写一些测试并运行。他以后可以担心覆盖率。”
“另一方面,第二位程序员在编程和测试方面都非常有经验。当我回答她应该往锅里放多少粒米时,我帮助她意识到测试的必要程度取决于许多因素,而她比我更了解这些因素——毕竟那是她的代码。没有单一简单的答案,她足够聪明去接受事实并与之共事。”
“我明白了,”年轻的学徒说,“但如果没有单一简单的答案,那您为什么对第三位程序员说‘80%以上’呢?”
大师笑得很大声,他的肚子上下翻动,这是他喝了不止绿茶的证据。
“第三位程序员只想要简单的答案——即使没有简单的答案……然后也不遵循。”
年轻的学徒和古老的大师在沉思的沉默中喝完了他们的茶。
阿尔贝托传达了一个简单的信息:专注于拥有尽可能多的业务逻辑和功能是前进的最佳方式。测试覆盖率不是应该追求任意数字的东西。
有些东西是有道理不进行测试的,即使是已经经过测试的代码也有不同的逻辑路径。
此外,在分布式系统中,API 或系统之间的通信可能会破坏系统。在分布式架构中,仅仅测试代码可能是不够的。强大的监控系统变得至关重要。基础设施即代码可以确保一致的部署和升级。此外,实现松散耦合的服务和适当的进程间通信对整体架构更有益,而不是一些单元测试。
测试驱动开发(TDD)有一种替代方法。行为驱动开发(BDD)为我们提供了一种不同的测试代码的机制;让我们讨论一下。
行为驱动开发
BDD 通过使用人类可读的故事来实现测试。
黄瓜是一种工具,通过使用用简单英语语言编写的人类可读的特性文件来实现 BDD 工作流程,例如:
Feature: Log in to site.
In order to see my profile
As a user
I need to log-in to the site.
Scenario: Logs in to the site
Given I am on "/"
When I follow "Log In"
And I fill in "Username" with "admin"
And I fill in "Password" with "test"
And I press "Log in"
Then I should see "Log out"
And I should see "My account"
现在,这一部分将是对 Behat 的非常简单的探索,以激发你的好奇心。如果你想了解更多,请访问www.behat.org
。
Behat 指南中包含了ls
命令的用户故事的示例。这是一个相当体面的例子,所以在这里:
Feature: ls
In order to see the directory structure
As a UNIX user
I need to be able to list the current directory's contents
Scenario: List 2 files in a directory
Given I am in a directory "test"
And I have a file named "foo"
And I have a file named "bar"
When I run "ls"
Then I should get:
"""
bar
foo
"""
为了安装 Behat,你可以修改你的composer.json
文件,以便在开发环境中需要它:
{
"require-dev": {
"behat/behat": "~2.5"
},
"config": {
"bin-dir": "bin/"
}
}
这将安装 Behat 版本 2.5,还有 Behat 版本 3,其中包含了一整套新功能,而且没有失去太多向后兼容性。也就是说,很多项目仍在使用 Behat 2。
然后你可以使用以下命令运行 Behat:
**bin/behat**
我们得到以下输出:
通过使用init
标志,我们可以创建一个包含一些基本信息的特性目录,让我们开始:
因此,让我们编写我们的feature/ls.feature
文件,包括以下功能和场景,如下所示:
如果我们现在运行 Behat,我们会得到以下输出:
因此,Behat 返回一些代码片段,以便我们可以实现未定义的步骤:
/**
* @Given /^I am in a directory "([^"]*)"$/
*/
public function iAmInADirectory($arg1)
{
throw new PendingException();
}
/**
* @Given /^I have a file named "([^"]*)"$/
*/
public function iHaveAFileNamed($arg1)
{
throw new PendingException();
}
/**
* @When /^I run "([^"]*)"$/
*/
public function iRun($arg1)
{
throw new PendingException();
}
/**
* @Then /^I should get:$/
*/
public function iShouldGet(PyStringNode $string)
{
throw new PendingException();
}
现在,在为我们创建的特性目录中有一个包含FeatureContext.php
文件的引导文件夹。在这个文件中,你将能够找到你的类的主体:
你可能已经注意到了类主体中的这个块。我们可以把生成的方法放在这里:
//
// Place your definition and hook methods here:
//
// /**
// * @Given /^I have done something with "([^"]*)"$/
// */
// public function iHaveDoneSomethingWith($argument)
// {
// doSomethingWith($argument);
// }
//
我已经这样做了:
你可能会注意到代码中充满了PendingException
消息。我们需要用实际的功能替换这些代码块;幸运的是,Behat 文档中包含了这些方法的函数:
/** @Given /^I am in a directory "([^"]*)"$/ */
public function iAmInADirectory($dir)
{
if (!file_exists($dir)) {
mkdir($dir);
}
chdir($dir);
}
/** @Given /^I have a file named "([^"]*)"$/ */
public function iHaveAFileNamed($file)
{
touch($file);
}
/** @When /^I run "([^"]*)"$/ */
public function iRun($command)
{
exec($command, $output);
$this->output = trim(implode("\n", $output));
}
/** @Then /^I should get:$/ */
public function iShouldGet(PyStringNode $string)
{
if ((string) $string !== $this->output) {
throw new Exception(
"Actual output is:\n" . $this->output
);
}
}
现在我们可以运行 Behat,我们应该看到我们的场景及其各种步骤已经完成:
通过使用 Mink 和 Behat,我们能够相应地使用 Selenium 来运行浏览器测试。Selenium 将使用 Mink 启动浏览器,然后我们可以在浏览器中运行 Behat 测试。
总结
在这一章中,我试图解决一些问题。我们通过学习 HTTP 来讨论了一些网络开发的方面。除此之外,我们还学习了如何有效地设计 RESTful API。
这本书现在要结束了;让我们重新审视一些使我们的代码变得伟大的核心价值观:
-
优先使用组合而不是继承
-
避免重复编码(DRY 原则意味着不要重复自己)
-
保持简单,傻瓜
-
不要仅仅为了使用设计模式而使用设计模式,当你发现它们可以解决重复出现的问题时引入设计模式
-
抽象很棒,接口帮助你抽象
-
按照良好的标准编写代码
-
在你的代码中分离责任
-
使用依赖管理和依赖注入;Composer 现在可用
-
测试可以节省开发时间;它们对于任何重构工作都是至关重要的,并且可以减少故障
感谢你读完了这本书;这本书是我对软件开发的一系列抱怨;在经历了非常多样化的职业生涯后,我学到了很多教训,也不得不重构了很多令人眼花缭乱的代码。我见过一些最糟糕的,但也参与了一些最激动人心的 PHP 项目。我希望在这本书中能够分享一些我在这个领域的经验。
开发人员很容易把自己藏起来,远离开发的现实;很少有人知道在软件设计和架构方面的最佳实践,而且其中很少有人选择 PHP 作为他们的开发语言。
对于我们许多人来说,我们所创造的代码不仅仅是一种爱好或工作,它是我们作为软件工程师表达的极限。因此,以诗意、表达力和持久的方式编写代码是我们的责任。
想想你希望维护的代码;那就是你有责任创造的代码。极简主义、减少复杂性和分离关注点是实现这一目标的关键。
计算机科学可能根植于数学和定理,但我们的代码超越了这一点。通过利用图灵完备语言的基础,我们能够编写创造性和功能性的代码。
这使得软件工程处于与许多其他学科相比的奇特真空中;虽然它非常度量化,但也必须吸引人类。我希望这本书能帮助你实现这些目标。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)