二、反模式

这里是我们开始反模式的地方;在你满怀希望之前,我想告诉你一些惊人的事情,它们将在不使用设计模式的情况下出色地简化你的代码,我不会在这里这样做(我说过我擅长粉碎希望和梦想吗?)。简而言之,反模式是代码中不需要的东西。

说到粉碎希望和梦想,如果你有一个初级开发人员,反模式也是一种很好的教学方法,应该同样避免。学习反模式也可以提高代码审查的有效性;您可以找一个外部源来咨询代码质量,而不是根据个人意见来讨论代码质量。

反模式是解决反复出现的问题的一种可怕的方法,这种方法通常是无效的,并且有可能产生严重的反效果。然后,他们可以创造技术债务,因为开发人员以后必须努力进行重构以解决最初的问题,但希望使用更具弹性的设计模式。

我们都遇到过意大利面代码;一位与我共事的合同开发商面对高额技术债务,对一位要求越来越高的产品负责人感叹道:“意大利面太多了,我还不如开一家餐馆!”意大利面代码是一个程序的控制结构很难理解的地方,因为它是如此的混乱和过于复杂,它可以被描述为一种反模式。PHP5.3.0 中的一个主要批评是在该语言中实现 goto 运算符。事实上,那些批评他们的实现的人声称 goto 操作符将为在 PHP 中使用更多意大利面代码提供另一个借口。

goto 在 PHP 中备受争议,甚至有人将其报告为一个 bug,称:“PHP5.3 包括 goto。这是一个问题。说真的,PHP 在没有 goto 的情况下取得了如此巨大的成就,为什么要将该语言变成一种公共威胁?”

除此之外,bug 报告的提交者还列出了预期结果:“世界将终结”,而实际结果是“世界终结”。尽管如此,PHP 中的 goto 运算符受到严格限制,因此不能直接跳入和跳出函数。有些人还认为它们在有限状态机(基本上是基于多个输入的二进制输出)中很有用,但这也是有争议的;因此,我将允许你对它们作出自己的判断。

您可能已经体验过复制和粘贴编程,即在程序中复制和粘贴整个代码块;这是另一个糟糕软件设计的例子。实际上,开发人员应该设计他们的软件来创建问题的通用解决方案,而不是复制、重构和粘贴一些代码来适应情况。

我将在本章中介绍学习反模式的重要性。在本章中,我将不仅讨论传统的与反模式相关的软件设计,还将讨论与反模式相关的 web 基础架构和管理风格。除此之外,我还想讨论一些特定于 PHP 的反模式或 PHP 中的缺陷,您可能需要在自己的代码中对此进行补偿。

这本书最后有一章专门介绍重构;如果您对重构过程感兴趣,本章将帮助您为您可能想要开始思考的想法奠定基础;除此之外,特定于设计模式的章节可能会帮助您实现最终的目标代码。在专门讨论重构的一章中,我们还将介绍一些代码气味,它们可以帮助您发现正在维护的代码库中的反模式。

为什么反模式很重要

大多数程序员的背景都是采用某种形式的反模式,直到最终意识到它无法扩展或不能很好地工作。当我 17 岁的时候,我的第一份工作是作为一名学徒开发人员,我会在周一到周五被迅速带到伦敦,不知何故,我会把我的西装和我的全黑衣服压缩成一个出人意料的小行李箱,学习软件开发。在星期五,我们通常在 12:00 休息半天,但我会在下午预订公司的火车票,这样我就可以在快餐店或咖啡馆里做一些简单的项目。每周,当我回来尝试扩展其中一个解决方案时,我都会意识到新的可伸缩性问题和代码质量问题。当然,我以前也做过开发,但这些主要是处理全新的非常短的编程任务,使用预先制作的框架,或者处理架构已经完成的遗留代码(或者,正如我现在意识到的,用一把非常钝的刀子屠宰)。这个扩展我自己代码的学习过程很棒;我很快自学了如何更好地设计软件。作为人类,我们对某个主题的了解往往不够,以致于我们所知甚少(我在管理软件开发人员的人身上发现了一个难以置信的事实,但他们自己从未编写过任何代码);在牢记这一点的同时,我们应该记住,我们永远不会从自己的错误中吸取教训。虽然这非常重要,但为了从别人的错误中吸取教训,自学有文档记录的反模式也是至关重要的。

我曾经是一个开发人员的技术领导和导师,他从错误中吸取教训,受到最残酷的对待。在我与这位开发人员进行的第一次评估中,人力资源部的同事告诉我,每次他犯了错误,之前的技术和人力资源主管都会把他拖进会议室,接受正式的纪律处分。这两个人的技术知识都非常有限,而且在管理开发人员方面也完全不称职(他们是那种生活在自己的泡沫中的人,不知道人们如何在更成功的环境中工作,基本上陷入了死胡同,不知道在职业生涯中做任何有意义的事情).当他们离开的时候,这个可怜的开发者的信心已经被粉碎到了这样一个地步,以至于他在网络领域没有真正的职业抱负,也不热衷于学习。在你的职位上快乐没有什么错。正如我的一位前任老板在我告诉他一些非常私人的事情后所说的那样,“最终,重要的是你快乐。”.是的,要让世界运转需要很多人,但一旦你将自己置于指导或管理其他开发人员的位置,你就有义务保持自己在游戏中的领先地位。如果你是一名管理者,你应该知道如何有效地完成工作。我遇到过的最好的人事管理者就是那些拥有丰富的知识,掌握最新和最先进的管理方法,就像我喜欢了解 PHP 核心和社区中最新和最先进的管理方法一样。在写这本书的整个过程中,我对项目和人员管理的知识有所提高,但还有很长的路要走,而且因此,在我工作的公司,欺凌是一种有效的管理策略,我曾向部门负责人提到过这一点,部门负责人回答说“我们不是说这是最好的方式”;如果这是真的,那么为了公司的利益,肯定应该采取一些措施来解决这个问题!这不是整个公司都是真的;其他部门的态度非常不同,事实上,技术总监曾经就这个话题和知道你不知道的重要性进行过一次技术演讲。公司首席执行官启动了一个 sim 卡我和我聊天,说他怎么知道他不知道。旧的习惯很难改变,但至少他们已经开始在改变的风中航行。

所以除了咆哮(我确实喜欢一个好的咆哮),我为什么要谈论这个?我的观点是你的态度很重要。关于这个问题,我最喜欢的一句话是“如果你把你的开发人员当作白痴,他们很快就会变成白痴”。让我进一步说:

  • 学生的不良表现往往是教师不良表现的反映。
  • 每个人都会犯错误,错误失控是管理者愚蠢行为的错,而不是开发人员的错。
  • 白痴吸引白痴。如果你在你的学科知识体系中是个白痴,那么你很可能会招募更多的白痴。
  • 如果你的工作场所充满了恐惧,你就是个白痴,害怕被发现。
  • 如果你不知道自己知道的有多少,也不寻求有效地治愈自己的无知,那么你就是一个白痴。
  • 如果你像对待白痴一样对待你的开发人员,你就是白痴。

简言之,学习你知道的和成长的是多么少。听起来很残忍,但这是事实。我们都是无知的,我们不可能什么都知道。有效地利用自己的知识与他人的知识是成功的关键。认识到我们自己的无知是关键。例如,去年我认为我在基础计算机科学方面的知识不够广泛,无法满足我自己或我指导的人对它的需求;因此,我决定去攻读计算机科学的兼职硕士学位。学习过程非常棒,教会了我以前不知道的计算机科学领域。

一些软件开发人员使用其他人的工作,是的,WordPress 或 Drupal 开发可以给你带来快乐和富有成效的职业生涯,但是你会发现为自己构建和构建东西是一种很好的学习体验。在传统的工程环境中工作过,我被说服了,认为计算机科学的坚实理论背景对软件工程师非常有益。事实上,理解计算机科学基础知识所需的知识体系实际上相当容易掌握。当然,在很多方面,我是在向皈依者说教;如果你正在读这本书,你大概了解到需要更深入的理论计算机科学知识基础,但请不要读这本书,停止积极学习。继续制定一个计划来提高你的知识,寻求改善储存在我们头骨中的蛋白质中的信息。

人们常说:“在盲人的国度里,独眼人就是国王。”;小规模的开发团队在进行好的软件开发时往往缺乏基础知识(可能是出于缺乏必要性),事实上,一些被困在过去的大型开发环境可能最终也会遇到同样的情况。在这方面,知识变得更加宝贵,对开发人员来说,接受软件开发方面的教育也变得同样重要。

反模式不仅仅是你的团队可以避免的事情;好的软件开发不仅需要对编程语言有坚定的理解,而且对软件开发的理论理解也是关键。

最后,让我从一篇关于 SourceMaking 的文章中引用这句话:

“体系结构驱动的软件开发是构建系统的最有效方法。体系结构驱动的方法优于需求驱动、文档驱动和方法驱动的方法。尽管有方法论,但项目往往成功,而不是因为它。”

咆哮结束。让我们讨论一些反模式。

不是这里发明的综合征

密码学可以给我们上一堂关于软件的非常重要的课;科克霍夫斯原理尤其如此。该原则规定:

“密码系统应该是安全的,即使系统的所有内容(除了密钥)都是公共知识。”

这是克劳德·香农以香农格言的形式重新制定的:

“一个人应该在假设敌人会立即完全熟悉他们的情况下设计系统”。

用外行的话说,为了拥有一个安全的系统,它不应该仅仅因为没有人知道它是如何实现的(“通过模糊实现的安全”)。如果你想通过默默无闻来确保你的钱,你会把它埋在树下,希望没有人能找到它。然而,当你使用真正的安全机制时,比如把钱放在银行的保险箱里,你可以把安全系统的每一个细节都作为公共信息,但是如果安全系统是真正安全的,你只需要保留安全的钥匙,其他每一个细节都可以是公共知识。如果有人要找到你保险箱的钥匙,你只需要更改密码,而如果有人真的找到了你的钱埋在树下的地方,你实际上就得把钱挖出来,找其他地方放。

只有在默默无闻的情况下才能实现安全是一个坏主意(也就是说,这并不总是一个坏主意)。如您所知,当您在数据库中存储密码时,您应该使用称为哈希算法的单向加密算法,以确保如果数据库被盗,任何人都无法使用数据库中的数据来查找用户的原始密码。当然,在现实中,您不应该只是散列一个密码,您应该对它进行加密,并使用诸如 PBKDF2 或 BCrypt 之类的算法,但这本书并不是关于密码安全的。

然而,现实情况是,有时,当开发人员真的费心对密码进行散列时,他们决定创建自己的密码散列函数,这些函数很容易可逆,并且只有在不知道算法的情况下才能得到保护。这是一个完美的例子不是在这里发明的NIH综合征);他们决定创建自己的密码散列库,假装自己是一名密码学家,却不了解这样一个决定的安全含义,而不是使用精心创建的备受推崇的密码散列库。

谢天谢地,PHP 现在可以轻松地散列您的密码;password_hash函数和password_verify函数使得password_needs_rehash函数可以很容易地告诉您何时需要重新计算散列。然而,我离题了。

那么究竟什么是 NIH 综合征呢?NIH 综合症是指对组织或单个开发人员自身能力的错误自豪感导致他们构建自己的解决方案,而不是采用优秀的第三方解决方案。重新发明车轮不仅成本高、不必要,而且会增加不必要的维护费用;它也可能非常不安全。

也就是说,如果解决方案是封闭源代码和锁定的,那么避免它们可能是一个好主意。这样做还可以避免供应商锁定和对业务灵活性的限制。

NIH 综合征依赖于现有的解决方案是好的,并达到预期。使用第三方库不是不检查代码质量的借口。

致力于开源解决方案是缓解这些问题的一个好方法。现有图书馆的改进空间?分叉,提出一项修正案合并到。没有一个库可以实现您想要的功能?然后你可能想考虑编写自己的库并发布它。

在结束这一节时,我将说世界已经变得异质;人们不再寻找一个技术堆栈来回答他们所有的祈祷;人们现在追求的是最好的工作工具。值得思考的是,如何利用这一事实为自己谋利。

与 Composer 的第三方依赖关系

Composer 使管理第三方依赖关系变得非常容易。在第 1 章为什么“优秀的 PHP 开发者”不是一个矛盾修饰法,我简要描述了如何使用 Composer 进行自动加载。重要的是,自 PHP5.1.2 以来,自动加载一直作为一个核心功能受到支持,但 Composer 的优点在于,您还可以将其用于依赖关系管理。Composer 可以使用指定的版本约束有效地获取所需的依赖项。

让我们从以下composer.json文件开始:

{ 
  "autoload": { 
    "psr-4": { 
      "IcyApril\\ChapterOne": "src/" 
    } 
  } 
} 

让我们引入一个依赖项:

{ 
  "autoload": { 
    "psr-4": { 
      "IcyApril\\ChapterOne": "src/" 
    } 
  }, 
  "require": { 
    "guzzlehttp/guzzle": "^6.1" 
  } 
} 

请注意,我们所做的只是添加一个require参数,在该参数中指定需要的软件。不需要手动将文件粘贴到项目或根目录中,也不需要使用 Git 中的子模块!

在本例中,我们引入了 Guzzle,一个 PHP 的 HTTP 库。

默认情况下,Composer 从名为Packergist的中央存储库查询存储库,该存储库聚合可从其各种版本控制系统(如 GitHub、BitBucket 或其他存储库主机)安装的软件包。如果您愿意,Packergist 可以充当一种电话簿,将来自 Composer 的包请求连接到代码存储库。

这就是说,Composer 支持的不仅仅是打包库。本着开源的精神,它支持来自一系列 VCS 系统(如 Git/SVN)的存储库,无论它们位于何处。

我们来看看下面的composer.json文件:

{ 
  "autoload": { 
    "psr-4": { 
      "IcyApril\\ChapterTwo": "src/" 
    } 
  } 
} 

让我来演示如何从 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 密钥,您可以使用 runssh-keygen命令,该命令将创建一个可用于 SSH 身份验证的公钥和私钥,包括 Github 或 BitBucket。这些密钥(通常)将存储在~/.ssh目录中,注意波浪号(~代表您的主目录)。因此,要将密钥打印到终端窗口中,请运行cat ~/.ssh/id_rsa.pub命令。请注意,.pub后缀表示id_rsa.pub是您可以公开共享的公钥。您不能共享私钥,私钥通常仅命名为id_rsa。在 Windows 上,您可以使用称为PuttyGen的 GUI 工具生成公钥和私钥。

获得公钥和私钥后,只需访问 GitHub 网站,进入设置菜单中的 SSH 密钥页面,粘贴密钥并保存,即可将其放入 GitHub。

对于后续更新,composer update将更新到composer.json中定义的所有依赖项的最新版本。不过,如果你不想这样做,还有另一种选择;运行 Composerdump-autoload将仅重新生成需要包含在项目中的 PSR-0/PSR-4 类的列表(例如,添加、删除或重命名某些类)。

Composer 还支持私有存储库,允许您跨多个项目有效地管理代码重用。另一个关键好处是 Composer 如何自动生成一个锁文件,您可以在项目中提交该文件。这允许您在使用版本控制系统进行提交时,准确地管理在特定时间点安装的依赖项的精确版本。

Composer 使管理第三方依赖关系变得简单有效。通过 Composer 已经可以使用一些重要的库,例如 PHPUnit,但也有一些其他伟大的库可以让您的生活更轻松。Composer 上我最喜欢的两个数据库库是雄辩的(一个来自 Laravel 的数据库 ORM 系统,您可以在illuminate/database找到)和 Phinx(一个数据库迁移/种子植入系统,您可以在robmorgan/phinx找到)。除此之外,Packergist 还提供了一些用于各种 API 的优秀 SDK(谷歌发布了一些 SDK,还有一些更具体的 SDK,例如用于从 PHP 应用程序发送短信的 Twilio SDK)。

Composer 允许您为特定环境指定依赖项;假设您只想在您的开发环境中使用 PHPUnit……这不是问题!

上帝反对

God 对象是糟糕的软件设计和糟糕的面向对象实现的诱人结果。

本质上,一个上帝对象是一个方法太多或属性太多的对象;本质上,它是一个知道的太多或做的太多的类。God 对象很快就会与应用程序中的许多其他代码位紧密耦合(被引用)。

那么这到底是怎么回事?简言之,当您将一段代码绑定到另一段代码中时,您很快就会发现维护灾难。如果为一个用例调整 God 对象中方法的逻辑,您可能会发现它会对另一个元素产生意外的后果。

在计算机科学中,采用分而治之的策略通常是个好主意。通常,大问题只是一系列小问题。通过解决这组小问题,您可以快速解决整个问题。对象通常应该是自包含的;他们应该只知道自己的问题,也应该只解决一组问题,自己的问题。任何与这个目标无关的东西都不属于那个类别。

可以说,与物理对象相关的对象应该实例化,而与物理对象无关的对象应该是抽象类。

开发嵌入式系统时,God 对象是反模式的另一面。嵌入式系统用于处理从计算器到 LED 标牌的任何数据;它们是小型芯片,基本上是独立的计算机,成本相当低。在这个用例中,由于计算能力有限,您经常会发现编程的优雅性和可维护性成为外围问题。稍微提高性能和集中控制可能更为重要,这意味着使用 God 对象在某种程度上是明智的。幸运的是,PHP 很少用于编写嵌入式系统,因此您不太可能发现自己处于这种特殊情况。

处理这些类的最有效方法是手动将它们拆分为单独的类。

另一种被称为害怕添加类的反模式也可能在这一过程中起到作用,同时也未能缓解这种情况。这就是开发人员不愿意创建必要类的地方。

下面是一个上帝类的例子:

<?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 上遇到一个项目,你会注意到原来的开发人员留下了一个config.php文件,其中包含(最好的情况下)无用的数据库信息或(最坏的情况下)极其重要的 API 密钥。

当这些文件没有意外地进行版本控制时,它们通常会被放入一个.gitignore文件中,并附上一个示例文件,供开发人员根据需要进行修改。WordPress 就是这样一个平台。

对此有一些小的改进,比如将核心配置放在一个 XML 文件中,这个 XML 文件被隐藏在一个有大量不相关配置的模糊文档中。

我发现在 PHP 中有两种管理环境变量的好方法。第一种方法是将它们以 YML 等格式放入root文件夹的文件中,并根据需要读取这些变量。

第二种方法,我个人更喜欢,是由一个名为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 不同,它没有与LISTEN通道配对的NOTIFY命令。

还请注意,当您将作业队列与存储真实数据的数据库表合并时,每次完成作业并更新标志时,都会使缓存失效,从而使 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

您可以在了解有关 Monit 的更多信息,并了解如何配置 Monithttp://www.mmonit.com 。对于每个专注于 DevOps 的开发人员来说,这是一个非常有价值的工具。

软件代替架构

通常,开发人员会在软件开发级别上寻求纠正系统的体系结构问题。虽然这有用例,但我非常喜欢在不必要的地方避免这种做法。将问题从软件体系结构层转移到基础结构层有其优点。

例如,假设您需要将特定 URL 端点的请求代理到另一台服务器。我认为这最好在 web 服务器级别完成,而不是编写 PHP 代理脚本。Apache 和 Nginx 都可以处理反向代理,但是编写一个库来实现这一点可能意味着您会遇到一些前所未闻的问题。你认为你会处理HTTP PUT/DELETE请求吗?错误处理呢?假设你确定了你的库,那么性能呢?利用用低级系统工程语言编写的 web 服务器,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 库中数千行代码更容易维护,PHP 库模仿 ProxyPass Apache 模块中已有的东西。

我听到有人批评微服务试图将问题从软件开发层转移到基础架构层,但我们真的认为这总是一件坏事吗?

是的,软件开发人员在软件开发层做事情有着既得利益,但是经常有必要让自己了解在更高层次上可用的功能,看看这是否可以纠正您存在的任何问题。

用奥卡姆的剃须刀来思考:最短的解决方案往往是最好的,因为它被逐字翻译为“不应该使用比需要更多的东西”

接口膨胀

我遇到过很多这样的例子,人们认为他们在做伟大的架构,但结果证明他们的努力适得其反。接口膨胀是这种情况的常见后果。

有一次,当我与一位 Scrum 大师讨论在 PHP 中执行多态性时接口的重要性时,他回答说,他曾经在一个环境中工作过,那里有一位工程师花了几个月的时间开发接口,他认为自己在做出色的架构工作。不幸的是,事实证明他并没有做很好的基础设施工作,他犯了实现接口膨胀的错误。

顾名思义,接口膨胀是指接口过度膨胀的地方。接口可能过于臃肿,以至于类实际上不可能以任何其他方式实现。

接口应尽量少用;如果这个类只需要单独实现一次,那么您真的需要一个接口吗。如果是这样,您可能会考虑在这种情况下避免接口。

接口不应用作测试单元功能的手段。在这种情况下,您真的应该使用单元测试,例如,通过 PHPUnit。即使如此,单元测试也应该测试单元如何工作,而不是作为工具来确保没有人编辑您的代码。

所以,让我把你们引向一个接口膨胀的实现。让我们看看 PHEANTHOST 开源库中的 ToeT0.接口类(注意,我已经删除了注释,使其更可读):

<?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 世纪 web 环境的无情打击。有被囚禁的工作角色;你要么是一名开发人员,要么负责托管。他们有各自的预算,尽管两个部门有着明确的共同命运。

这种设置的结果是开发人员和操作技术人员从不共享知识。通过将开发和操作结合起来(DevOps,如果您愿意的话),不仅可以通过共享知识库有效地提高工作质量,还可以通过授权开发人员来提高效率。

在我给出的示例中,当托管在公司服务器上的站点遭到黑客攻击或破坏时,所有操作都是从备份中恢复。将开发工作结合到这一组合中,不仅可以修补漏洞,还可以在托管环境中采取有效措施来纠正这些问题(无论是暴力插件还是 web 应用程序防火墙)。

过度分离开发责任

过于明显地划分开发责任可能对团队有害。

有些分离是必要的。例如,使用物联网物联网平台的团队无法保持强大的电子工程知识和前端 web 开发知识。这就是说,开发人员应该学习他们遇到的其他技能,这可以通过鼓励知识共享得到帮助。拥有多学科的团队成员并不是业务上的劣势,事实上,这是一种优势。

错误抑制运算符

PHP 中的错误抑制操作符确实是一个非常危险的工具。只需在语句前面加一个 at 符号@,就可以抑制由此产生的任何错误,包括停止脚本执行的致命错误。

不幸的是,这在 PHP 中还不一定被反对;在与 PHP internals 小组的成员讨论过之后,有很多先决工作需要首先完成,因为一些 PHP 函数没有伴随的错误函数来产生 PHP 脚本执行中的错误。因此,显示不一定停止脚本执行的非致命错误的唯一方法是捕获在该特定函数的操作期间抛出的错误

不幸的是,PHP 核心本身包含了大量的技术债务。不幸的是,一个好的 PHP 开发人员应该擅长的一件事是发现 PHP 核心本身的技术债务。事实上,Facebook 试图通过重写 PHP 核心并称之为Hak来绕过这个问题;我将让你决定是否考虑采用它。

在 Go(一种由 Google 编写的系统语言)中开发的一个特性是,您可以执行多种返回类型(例如,您可以从一个函数返回两个值)。这样做的另一个好处是,您可以在单个函数调用中简单地返回任何错误,而不是使用一个将返回错误消息的伴随函数。

另外一件我喜欢围棋的事情是,所有警告都被视为错误。你分配一个变量,然后不使用它?程序将无法运行(除非您将变量分配给下划线_,该下划线是空赋值运算符,表示变量不会存储在任何位置)。将警告视为错误的结果意味着,当开发人员遇到错误时,他们知道这是严重的。

因此,是的,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!"; 
} 

顺序耦合

顺序耦合是创建一个类的地方,该类具有必须按特定顺序调用的方法。以initbeginstart开头的方法名称可能表示此行为;这可能表示反模式,具体取决于上下文。有时候,工程师用汽车来解释抽象概念,我也会这样做。

例如,以以下课程为例:

<?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 with Puppet 或 Docker,可能是一个很好的解决方案。

在使用 PHPUnit 和 Composer 进行单元测试时,您只需将其包含在composer.json文件中即可将其拉入:

{ 
  "autoload": { 
    "psr-4": { 
      "IcyApril\\Example": "src/" 
    } 
  }, 
  "require": { 
    "illuminate/database": "*", 
 "phpunit/phpunit": "*", 
    "robmorgan/phinx": "*" 
  } 
} 

除此之外,phpunit.xml文件可能也很有用,这样 PHPUnit 就可以知道测试在哪里,也可以知道 Composer autoloader 在哪里(这样它就可以继续并拉入类):

<?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); 
  } 
} 

当然,除此之外,您还可以根据需要在 autoloader 中拉入 PHP 类。

并非所有测试都需要是单元测试。编写外部测试脚本来测试 API 也可能是有益的。一种叫做的工具 http://www.seleniumhq.org 甚至可以帮助您实现浏览器自动化。

业务拆分

将您的整体拆分为小型独立、松散耦合的服务是减少技术债务的一个好方法。

大型整体式应用程序的技术债务植根于应用程序的核心,因此处理这些应用程序可能会有问题。在这样不稳定的地基上建造房屋,以后很难拆散。然而,有一个解决办法;通过建立新的功能作为独立的服务,您可以有效地建立在一个新的核心,具有稳定的基础,与您的旧的薄弱基础设施背道而驰。然后,您可以使用 RESTful 结构将其与旧的 monolith 和此类新服务相互通信。

此结构允许您在迁移到新的微服务体系结构时继续开发新功能。

Martin Fowler 提出了一个称为抽象分支的系统,它允许您以渐进的方式对系统进行大规模更改,允许您在更改仍在进行时继续发布。

第一步是捕获客户代码的一部分与其供应商之间的交互;然后,我们可以更改代码的该部分,使其通过抽象层相互通信。

然后我们与供应商进行互动。当我们这样做时,我们借此机会提高单元测试的覆盖率。一旦一个供应商根本不在使用中,我们就可以将客户端迁移到使用该供应商,并删除旧供应商。

完美的阶段性迁移

将您的整体拆分为小型独立的松散耦合服务是减少技术债务的一个很好的方法,但是在这个过程中,您显然会给体系结构级别增加额外的负担。

在迁移数据或托管环境时,您可能会在此过程中遇到困难。当部署过程不重复并且对于每个部署都是唯一的(例如在不使用持续集成的环境中)时,这一点尤其正确。

使用容器技术(如 Docker)可以让您更好地执行快速应用程序部署,让您能够更快地部署,同时提高可移植性并简化维护。有些人可能会发现其他技术,比如流浪汉,对他们更有利;无论如何,在所有这些技术中都有一个共同的因素:作为代码的基础设施。

基础设施作为代码是通过代码而不是交互配置工具来管理和配置计算基础设施的过程;然而,我们在这里追求的甚至比这更基本。我们想要的是能够在事实发生之前对任何类型的迁移进行阶段化和测试,并在执行迁移时重新运行确切的过程。

通过编写迁移脚本,您可以像编写代码一样预先测试迁移。您可以确定,当在实时服务器上而不是在临时服务器上完成时,发生任何错误的可能性都会降低。

除此之外,如果部署中的某个因素稍后会导致问题,或者可以看到决策的理由,那么迁移可以用于对流程进行反向工程。它本质上充当软件部署过程的工件。

在可能的情况下,应在该过程中提供尽可能多的资源;这包括那些部署代码的人、将项目整合在一起的开发人员,在极端情况下,还包括一名通信人员,以使客户机保持最新。这些资源允许快速调试这些问题,但至关重要的是,部署代码的个人在需要这些资源时带头协调,以防止分心。

按照预先计划好的正式例行程序进行工作,同时允许纠正任何问题,通常可以帮助尽可能轻松地进行部署。

测试驱动开发

这是对测试驱动开发TDD的开玩笑的引用。TDD 是一种软件开发策略,主要围绕使用开发测试来推动实现以满足需求。

然而,测试人员驱动的开发是需求的捷径,软件团队开始通过 bug 报告指定需求。测试人员驱动的开发也可以被称为Bug 驱动的开发,因为它本质上导致 Bug 报告被用来指定开发人员应该实现的操作和特性。

例如,开发人员构建一个工具,将数据从数据库导出到电子表格。它工作得很好,但测试人员仍然会回来,并提出一张罚单,说产品中存在缺陷;他们说它不包含导出为 PDF 的功能。如果这不在需求中,就不应该作为 bug 提出。是的,你应该有要求。

QA 团队和测试人员的存在是为了验证软件是否满足需求。它们的存在并不是为了说明需求本身。

膨胀优化

通常,开发人员可能会在试图优化其代码或设计工件到荒谬的程度时绊倒自己,通常是在他们的代码执行基本功能之前,或者甚至在创建任何代码之前。这可以快速执行生产中的问题。

在本节中,我希望讨论与此主题相关的三种反模式:

  • 分析麻痹
  • 自行车运动
  • 过早优化

分析瘫痪

简言之,这是一个策略被过度分析的地方,以至于进度减慢,甚至在极端情况下完全停止。这样的解决方案不仅会很快过时,而且会在教育程度较低的情况下产生,例如,在一次会议上,一位过度分析的老板试图在不允许其开发人员进行实际研究的情况下,提前深入挖掘细节。

过度分析问题,预先寻求完美的解决方案是行不通的;程序员应该寻求完善他们的解决方案,而不是提前提出完善的解决方案。

自行车赛

从本质上讲,这就是分析瘫痪可能发生在一些非常琐碎的决定基础上的地方,例如,登录页面的颜色。唯一需要的解决办法是不要把时间浪费在琐碎的决定上。尽可能避免由委员会进行设计,因为大多数人,无论他们认为自己的设计技能有多好,在很大程度上都不擅长设计。

过早优化

在这一部分,到目前为止,我主要殴打了项目经理;没有时间殴打开发者。通常,开发人员会试图过早地优化他们的代码,而没有经过教育的数据引导的结论来推动何时何地进行优化。

编写清晰易读的代码是您的首要任务;然后,您可以使用一些优秀的分析工具来确定瓶颈所在。XDebug 和 newrelic 只是擅长这方面的一些工具。

也就是说,在某些情况下,必须进行优化,特别是在一些长时间的计算任务上,在这些任务中,将某些时间从 O(N2)减少到 O(N)是至关重要的。也就是说,大多数简单的 PHP web 应用程序都不需要使用这种考虑。

未受过教育的经理综合征

您的经理是否自己开发过 web 应用程序?我发现这是一个经理应该具备的相当重要的特征。就像初级医生向自己经历过初级医生过程的医生报告一样,或者教师向自己经历过教师过程的班主任报告一样,软件开发人员应该向自己经历过该过程的人报告。

显然,在小型团队中(例如,一家小型设计公司,同时进行 web 开发),工程经理可能不是绝对必要的。如果管理者确实理解在必要时将决策推迟给程序员的必要性,那么这种方法很有效。然而,一旦事情扩大规模,就需要有结构。

诸如雇佣谁、解雇谁、如何解决技术债务、最需要关注哪些要素等决策需要由开发人员做出;除此之外,他们有时不能被民主地对待,因为这样做会导致委员会的设计。在这种情况下,需要一名工程经理。

在大型团队中,应该总是有一个开发人员花费超过 90%的时间不编写代码。

我会更进一步,;一个 web 工程经理不应该只有技术背景,他们应该有 web 背景。开发 Java 应用程序开发人员可能与构建 PHP web 应用程序完全不同,因此,这样的工程经理应该通过具有一些 web 经验(尽管不一定必须使用一种特定的语言)来理解这一原则。

错误的岩石地基

SensioLabs Insight 工具用于评估各个项目中的技术债务,他们评估并公布了回复。SensioLabs 在他们的博客上回应说,结果没有考虑到项目年龄或项目规模,但它确实显示了您在使用某些框架作为基础时所面临的技术债务:

Wrong rocky foundations

别误会我的意思: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 相关的,还有一些是管理层的。

从根本上说,反模式导致技术债务。通过技术债务,我们谈论的是很难扩展的代码,以至于以后很难对其进行更改。

以下是我希望您为解决此问题而执行的操作列表:

  • 在开始编码之前做好计划
  • 发表评论,并在代码目的不明显的地方添加评论
  • 确保您的代码具有结构
  • 尽量避免在一个方法中放入太多代码
  • 使用 DocBlocking
  • 对 PHP 使用常识方法

在本章中,我们了解了一些可能导致严重问题的常见设计问题;这些原则可以帮助您防止以后出现重大问题。按比例编写代码是设计的一个重要因素。其核心是,这需要理解约束。使用适当的进程间通信策略可以帮助扩展服务,而编写松散耦合的代码可以提高代码重用和调试。最后,在部署这段令人敬畏的代码时,自动化测试和完美的分阶段迁移可以确保这一切顺利进行。

在接下来的章节中,我们将继续介绍一些设计模式(大概是您一直在等待的)。

如果您有兴趣学习如何改进现有代码库的设计,您可能会发现本书中关于重构的专门章节特别有趣;但是,为了理解我们试图重构的模式,首先阅读其他设计模式是值得的。