十、PHP 框架与 FP
现在我们已经了解了如何使用函数式编程来解决常见的编程问题,现在是我们在使用框架进行开发时应用这些技术的时候了。本章将介绍几种最常见的 PHP 框架的实现方法。
在我们开始之前,有一点免责声明。我决不是我们将在这里讨论的每一个框架的专家。我在不同的层面上与他们共事过,但这并不意味着我知道他们的一切。因此,我可能不会在这里介绍最新的最佳实践,尽管在撰写本章时进行了研究。
话虽如此,本章中我们不会编写太多代码。我们将主要研究如何将现有的功能代码与框架结构连接起来,以及如何利用各种框架特性来帮助您以功能性的方式进行编写。我们还将讨论每个框架在函数式编程方面的优缺点。
在本章中,我们将了解以下框架:
- 西蒙尼
- 拉维尔
- 猪跑
- WordPress
我听到一些人在后台低声说 Drupal 和 WordPress 不是框架,而是内容管理系统。我同意你的看法,但请记住,人们都在使用它们来创建具有电子商务和其他功能的成熟应用,因此它们在这里占有一席之地。
此外,CodeIgniter框架在列表中缺失,因为我从未使用过它。然而,您可能可以将这里提供的大部分建议用于任何框架,包括 CodeIgniter。
事实上,每一部分中的大部分建议在各种情况下都是有用的。这就是为什么我强烈建议您阅读关于每个框架的章节。这将使我避免重复太多。
Symfony
Symfony 框架专注于依赖注入,非常适合编写功能代码。Symfony 开发人员习惯于以明确定义其依赖关系的方式声明其控制器和服务。
我们可以说注入整个容器有点问题。从严格意义上讲,控制器和服务仍然可以是纯粹的,但显然认知负担有点重,因为您需要阅读代码才能确切知道使用了哪个依赖项。
在这一部分中,我们将讨论 Symfony 的哪些部分非常适合函数式编程,以及在哪些方面需要谨慎。我们无法涵盖所有内容,因为 Symfony 是一个包含大量组件的完整框架,但它应该足以让您开始使用。
处理请求
原始的Request
类与我们前面提到的PSR-7 HTTP
消息接口不兼容。这意味着它并非如规范所建议的那样是不变的。然而,如果您使用的是至少 3.0.7 版本的SensioFrameworkExtraBundle
框架,那么获得请求的 PSR 版本就非常容易。您只需使用 Composer 安装所需的依赖项,并稍微更改控制器操作签名:
composer require sensio/framework-extra-bundle
composer require symfony/psr-http-message-bridge
composer require zendframework/zend-diactoros
您的控制器需要在其方法签名中使用新的ServerRequestInterface
类,而不是更传统的Request
类:
<?php
namespace AppBundle\Controller;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Zend\Diactoros\Response;
class DefaultController extends Controller
{
public function indexAction(ServerRequestInterface $request)
{
return new Response();
}
}
如果出于任何原因,您需要获得与 Symfony 接口兼容的Request
或Response
实例,您可以使用Symfony PSR-7桥接器。
如果您在新的符合 PSR-7 的 HTTP 消息的帮助下,只在控制器和服务中正确地注入所需的依赖项,那么您现在就可以为 Symfony 应用编写功能代码了。
数据库实体
编写完全纯函数代码时可能遇到的一个挑战是数据库访问。通常,开发人员在 Symfony 中使用原则,据我所知,原则还没有任何工具来帮助编写引用透明的代码。
在框架的上下文中,使用 IO monad 之类的东西也很麻烦,因为在函数的每个入口和出口点,都必须封装参数或将结果转换为框架所期望的格式。
我们将尝试使用各种技术来缓解这个问题。在这一过程中,我们还将学习如何在使用条令时利用Maybe
类型。
嵌入
尽管与函数式编程没有严格的关系,但值对象的概念可以用来实现实体的某种不变性。为了自身的利益,这也是一个值得探索的想法。
这是我们已经在第 2 章、纯函数、引用透明性和不变性中讨论过的想法。然而,我将借此机会给出一个与领域驱动设计有所不同的定义:
- 实体:具有独立于其属性的身份的东西。
- 值对象:与属性无关的事物。
一个常见的例子是有地址的人。人是一个实体,地址是一个值对象。如果您更改了一个人的姓名、地址或任何其他财产,该人仍然是同一个人。但是,如果您更改地址上的任何内容,它将成为一个完全不同的地址。
教条以Embeddedables的名义实施这一理念。该术语来源于这样一个事实,即值对象始终与实体相关或嵌入,因为它本身没有存在的意义。您可以在官方网站上找到文档 http://docs.doctrine-project.org/en/latest/tutorials/embeddables.html 。
虽然我不建议劫持这个特性,但为了实现所有关系的不变性,我强烈建议您在设计实体和尽可能使用它们时考虑嵌入。它将帮助您在功能上进行编码,并提高数据模型的质量。
避开二传手
如果您开始寻找关于使用条令和大多数 ORM 的最佳实践,有一天您会发现有人建议我们避免创建 setter 方法。这样做通常有很多好的理由。在我们的例子中,我们将只关注其中一个,我们希望不可变实体帮助我们编写纯函数代码。
在大多数情况下,摆脱二传手的建议解决方案将是从任务的角度来思考。例如,在BlogPost
类上没有setState
和setPublicationDate
方法设置器,而是有一个publish
方法,这将反过来改变这两个字段。
这是一个很好的建议,因为它允许您将大部分业务逻辑放在它所属的实体中,并且避免了对象处于某种奇怪的状态,因为开发人员没有采取所有必要的步骤。带有 setter 的传统类如下所示:
<?php
class BlogPost
{
private $status;
private $publicationDate;
public function setStatus(string $s)
{
$this->status = $s;
}
public function setPublicationDate(DateTime $d)
{
$this->publicationDate = $d;
}
}
可以将其转换为以下实现:
<?php
class BlogPost2
{
private $status;
private $publicationDate;
public function publish(DateTime $d)
{
$this->status = 'published';
$this->publicationDate = $d;
}
}
如您所见,我们在适当的位置修改了值,留下了一个副作用。我们可能天真地认为,在publish
方法中克隆当前对象并返回带有修改属性的新版本就足够了,以获得我们方法的不可变版本;遗憾的是,这一解决方案不起作用。
条令存储哪些实体由其工作单元管理,而克隆实体不处于托管状态。我们可以使用一些技巧附加实体,但我们将处于以下两种情况之一:
- 这两个实体都得到了管理,这可能导致理论本身内部元数据的内部一致性出现问题
- 只有最新的实体被管理,这意味着我们对
publish
方法的调用产生了将以前的实体与学说分离的副作用
棺材中的钉子是,目前没有 API 可用于从实体内部执行此操作。这就是为什么在撰写本文时,我不建议使用当前的条令版本(即版本 2.5.5)追求不可变的实体。
无论如何,避免在实体上创建 setter 已经是朝着引用透明的代码库方向迈出的一大步。它还将帮助您将业务逻辑保持在一个位置,而不可能让实体处于无效状态。
为什么是不可变的实体?
让我们用一个简单的例子来演示这一点,而不是长篇大论。条令使用DateTime
类的实例来表示与日期和时间相关的任何内容。由于DateTime
类是可变的,这可能导致根本不容易确定的问题:
<?php
$date = $post->getPublicationDate();
// for any reason you modify the date
$date->modify('+14 days');
var_dump($post->getPublicationDate() == $date);
// bool(true)
$entityManager->persist($post);
$entityManager->flush();
// nothing changes in the database :(
第一个问题是,实体中存储了对同一对象的引用。这意味着,如果出于任何原因,您更改了它,则日期也将在帖子中更改。这可能是你想要的,但毫无疑问,这是一个副作用。特别是如果您将$date
变量返回给潜在的调用者。如何知道修改日期将导致修改实体?
第二个问题更成问题。由于条令使用对象标识而不是其值来确定是否发生了变化,它将不知道现在的日期不同,将帖子保存回数据库将毫无意义。
GitHub(上提供了一个软件包 https://github.com/VasekPurchart/Doctrine-Date-Time-Immutable-Types )对于这个特定问题,但是,任何时候使用可变实例而不是嵌入式或任何其他类型的值对象时,都可能遇到类似的问题。帮自己一个忙,尽可能使用不变性。
Symfony 参数转换器
我们讨论了修改已经实例化的实体并将它们持久化回数据库。但是一开始就得到它们怎么样?SensioFrameworkExtraBundle
框架包含一个称为@ParamConverter
的很好的小注释,它允许我们让框架完成工作,并将从数据库获取实体的副作用保留在代码库之外。
下面是一个小示例,以便您了解如何使用此注释(如果您想了解更多信息,可以阅读 Symfony 网站上的官方文档,网址为http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html :
<?php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class PostController extends Controller
{
/**
* @Route("/blog/{id}")
* @ParamConverter("post", class="SensioBlogBundle:Post")
*/
public function showAction(Post $post)
{
// do something here
}
}
使用来自路由的信息以及定义的参数转换,框架可以直接给您Post
实例,如果无法找到,则可以生成404 error
。
使用注释,您的方法不再需要执行数据库访问,因为它将直接接收数据。我们可能会争辩说不纯代码存在于其他地方,这是真的,但 Symfony 无论如何都不应该是一个纯代码库。我们能够将杂质从我们自己的代码中剔除,这对我们来说很重要。
在这种特殊情况下,类型提示对于路由来说已经足够了。当函数签名引用实体类时,ParamConverter
注释将自动输入操作。如果您发现注释更清晰,或者您可以决定只在更复杂的情况下使用它,那么保留注释是没有坏处的。
在某些情况下,这种机制显然不够强大。我知道有些捆绑包提供了类似的功能,具有更大的灵活性;你也许能找到一个适合你需要的。如果其他方法都不起作用,您仍然可以自己执行查询,或者使用 IO monad 为您执行查询。
可能存在一个实体
该原则可以很容易地适应于返回集合实例,也可能是单子。第一步是创建一个新的存储库:
<?php
use Widmogrod\Monad\Maybe as m;
use Widmogrod\Monad\Collection;
class FunctionalEntityRepository extends EntityRepository
{
public function find($id, $lockMode = null, $lockVersion = null)
{
return m\maybeNull(parent::find($id, $lockMode, $lockVersion));
}
public function findOneBy(array $criteria, array $orderBy = null)
{
return m\maybeNull(parent::findOneBy($criteria, $orderBy));
}
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
{
return Collection::of(parent::findBy($criteria, $orderBy, $limit, $offset));
}
public function findAll()
{
return Collection::of(parent::findAll());
}
}
然后,您需要配置 Symfony 以使用这个新类作为默认存储库;通过向 YAML 配置中添加以下键,可以轻松完成此操作:
doctrine:
orm:
entity_managers:
default_em:
default_repository_class: MyBundly\MyNamespace\FunctionalEntityRepository
如果您没有使用 Symfony,您可以在一个条令Configuration
类实例上使用setDefaultRepositoryClassName
方法来实现相同的效果。
考虑到我们讨论的关于条令的所有内容,当涉及数据库时,您将无法拥有一个纯粹的功能代码,但您已经做好充分准备,可以获得大部分好处。
组织您的业务逻辑
官方的 Symfony 最佳实践包含一些关于如何以及在何处编写业务逻辑的建议。我们将对它们进行一些扩展,以便于编写函数代码。
第一个建议是避免在与框架本身相关的部分中编写任何逻辑:路由和控制器。这些应该尽可能直截了当。遵循这个建议是一个好主意,因为这样,如果您被迫在控制器中进行一些数据库访问,那么它就没有那么重要了。
我建议您在控制器内部完成所有与数据库相关的工作,这样任何有副作用的事情都会被隔离在那里,您的业务逻辑可以遵循适当的功能技术。
此外,您应该只在控制器和服务中注入所需的依赖项,而不是使用服务容器。这将大大减少认知负担,因为方法和构造函数的签名将足以确定业务逻辑的依赖关系。
我还建议您避免使用 setter 注入,因为调用 setter 将修改您的服务状态,从而破坏不变性,如果多次调用 setter,还可能导致问题。
通过决定将副作用限制在控制器上,您可以集中精力在实体和服务中编写功能代码。这将使实体和服务易于推理和测试。然后,由于控制器本身不应包含任何逻辑,因此可以使用集成和功能测试来测试最终部件,并快速获得对应用的信心。
Flash 消息、会话和其他具有副作用的 API
Flash 消息通常用于以非突兀的方式向用户传达信息。很遗憾,Symfony 提出的管理这些消息的 API 在引用上是不透明的,因为您需要在控制器上调用一个将消息添加到队列的方法。会话数据管理也是如此。
这个问题可以通过将它们整合到Response
对象中来解决。然而,这需要在框架一级进行。此类变更要么需要纳入上游,要么需要大量维护。
一种解决方案是利用服务中的 Writer 或 State monad 来保存该信息,然后将其保存在控制器中,因为我们已经决定使用它来处理与数据库相关的副作用。
但是,我不建议使用 IO monad,因为如果没有框架级别的某种支持,它会变得很复杂,特别是因为 PHP 缺少 Haskell 中可用的do 注释注释的良好替代品。它只会使代码复杂化,而没有真正的好处。
Form
API 是另一个具有大量内部状态和不纯方法的实例。然而,它是声明性的,足以使这一事实不引起很多问题。您可以将表单创建抽象为它自己的类,这也有助于考虑到它没有副作用。
我强烈建议您尽可能创建Form
类型,并尽可能将生成的Form
实例视为不可变对象。
结束语
虽然设计非常注重面向对象的设计,但 SimfOy 为我们编写功能代码提供了良好的基础。需要做出一些让步,特别是当涉及到数据库访问和框架提供的非功能性 API 时,但是您自己编写的代码自始至终都可以很好地发挥功能,除了控制器本身。
使用功能性方法的一个缺点可能是,您发现自己创建了更多的服务和类,以便在请求生命周期的单个部分中隔离所有副作用。
然而,这样做的好处是拥有一个清晰解耦的代码库,如果给予适当的关注,甚至可以在 Symfony 之外重用。您还可以更轻松地分离测试每个部件。
也没有什么可以阻止您在应用的某些部分和服务中逐渐应用功能性技术。通过缓慢地将代码迁移到功能更强大的地方,您可以立即应用这些技术,这也有助于其他人的学习。您可以使用以下资源更好地了解功能技术的应用:
拉雷维尔
正如我们已经讨论过的,来自 Laravel 的collection
API 是一个不可变数据结构的很好例子,上面有很好的函数方法。返回Collection
类实例的数据库层确实有助于简化其使用。
然而,如果您想保持功能的纯粹性,框架提出的Facade模式是不可能的。一旦您使用了一个 façade,您就使用了一个没有在函数签名中声明的外部依赖项。
不管您对这种模式的看法如何,如果您想编写引用透明的代码,就必须去掉它们。幸运的是,Laravel 为大多数常见任务提供了帮助函数,并提供了一种访问烘焙外观的容器的方法。正如我们将看到的那样,使用不同的东西并不难。
由于 Laravel 也是一个基于面向对象原则和 MVC 模式的框架,因此 Symfony 部分的所有一般建议也适用,特别是关于解耦各个部分并尝试在每个请求(通常是控制器)的唯一位置分离副作用的建议。
实现这一目标的方式可能略有不同,但不会太多;这就是为什么我鼓励您阅读上一节,如果您还没有这样做,因为这里将不再重复该建议。
数据库结果
如前所述,Laravel 有一个非常棒的不可变集合实现。所有返回多个实体的数据库查询都将返回该实体的一个实例。有一本非常好的书详细介绍了利用其功能的所有方法。您可以在作者的网站上找到它,以及屏幕广播和其他教程 https://adamwathan.me/refactoring-to-collections/ 。集合可能是不可变的,但其中的对象不是。既然有说服力的拉雷维尔 ORM 真的不同于教义,那么,我们有可能让它们如此。与使用Repository
模式和UnitOfWork
模式不同,您只需要在Model
类上扩展ActiveRecord
模式的方法。这意味着有可能实现您的方法,使您的实体保持不变,而不存在我们遇到的关于学说内部状态本身的问题:
<?php
use Illuminate\Database\Eloquent\Model;
class BlogPost extends Model
{
private $status;
private $publicationDate;
public function publish(DateTime $d)
{
$new = clone $this;
$new->status = 'published';
$new->publicationDate = $d;
return $new;
}
}
只要不修改用于主键的字段,对对象的save
方法的任何调用都将更新数据库中的当前行。但是,如果您具有非标量属性,您可能希望在对象上添加一个__clone
方法。默认情况下,PHP 只执行浅层复制,这意味着所有引用都将保持不变。这可能是你想要的,但你需要确保它。
如果您想要强制某些属性的不变性,GitHub(上提供了一个包 https://github.com/davidmpeace/immutability 就是为了做到这一点。如果您很严格,就不必这样做,但在传统和功能部件的遗留代码库中,这可能是一个很好的特性。
可能使用
与原则一样,也可以返回Maybe
的实例而不是null
。由于结构有点不同,我们首先需要创建一个新的Builder
类:
<?php
use Illuminate\Database\Eloquent\Builder as BaseBuilder;
use Widmogrod\Monad\Maybe as m;
class FunctionalBuilder extends BaseBuilder
{
public function first($columns = array('*'))
{
return m\maybeNull(parent::first($columns));
}
public function firstOrFail($columns = array('*'))
{
return $this->first($columns)->orElse(function() {
throw (new ModelNotFoundException)- >setModel(get_class($this->model));
});
}
public function findOrFail($id, $columns = array('*'))
{
return $this->find($id, $columns)->orElse(function() {
throw (new ModelNotFoundException)- >setModel(get_class($this->model)); });
}
public function pluck($column)
{
return $this->first([$column])->map(function($result) {
return $result->{$column};
});
}
}
由于使用了first
函数,现在返回的是Maybe
类型的实例,而不是 null,因此重新定义了多个方法。然而,没有必要返回Collection
的实例,因为 Laravel 版本已经是一个很好的实现,即使它不是 monad。
现在我们需要我们自己的Model
类,它将使用我们的FunctionalBuilder
方法:
{
return $this->find($id, $columns)->orElse(function() {
throw (new ModelNotFoundException)- >setModel(get_class($this->model));
});
}
public function pluck($column)
{
return $this->first([$column])->map(function($result) {
return $result->{$column};
});
}
}
这两个新类在大多数情况下都应该有效,但由于我们正在重新定义框架本身使用的方法,您可能会遇到麻烦。如果是这样的话,我很乐意听取您的意见,以便改进实现以避免问题。
您可能还希望修改集合 monad,以便在适当的情况下,在各种方法中返回 Maybe monad 的实例,而不是null
。然而,这将需要比我们迄今所做的更多的修改。据我所知,目前还没有提供此功能的软件包。
去掉立面
Facades 的概念可能有助于减少新来者的学习曲线,并有助于使用 Laravel 提出的服务。但不管你对它们的看法如何,它们一点都不起作用,因为一旦你使用它们,它们就会引入副作用。
通过在控制器和服务中注入依赖项可以很容易地消除它们,这是 Symfony 世界中的常见做法。除了允许您编写功能性代码外,停止使用 facades 还有一个隐藏的好处,即您的代码与 Laravel 的联系将更少,因此您可以更多地重用它。
Laravel 提供了一个名为自动注入的功能,这将允许您非常轻松地通过外观获得各种可用组件。它使用类型提示在类实例化时自动注入所需的依赖项。例如,它可以在多种上下文控制器、事件侦听器和中间件中工作。
获取UserRepository
类的实例非常简单,如下所示:
<?php
namespace App\Http\Controllers;
use App\Users\Repository as UserRepository;
class UserController extends Controller
{
protected $users;
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}
可通过参考文档中的表格轻松找到要使用的类型提示 https://laravel.com/docs/5.3/facades#facade-类别参考。
然而,这个漂亮的机制并没有真正将您与框架解耦,因为您需要使用正确的类型提示。另一种通过项目的bootstrap/start.php
手动注入依赖项的方法在文章中描述 http://programmingarehard.com/2014/01/11/stop-using-facades.html/ 。
HTTP 请求
与 Symfony 一样,使用 PSR-7 中定义的接口而不是框架中定义的接口非常容易。Laravel 使用与 Symfony 相同的桥来执行转换。您只需要使用 Composer 安装两个软件包:
composer require symfony/psr-http-message-bridge
composer require zendframework/zend-diactoros
然后,当您想要获取 PSR-7 版本的实例时,使用ServerRequestInterface
类作为类型提示而不是Request
方法就足够了。如果您的控制器操作返回 PSR-7 版本,Laravel 将负责将Response
方法转换为自己的格式。
结束语
Laravel 核心开发人员做出的实施决策是双向的。其中的一些,比如不可变集合实现,在函数式编程方面非常出色。其他的,如使用外墙,使我们的生活更加困难。
然而,为了使用更实用的方法,转换代码是相当简单的。您可能遇到的唯一困难是在阅读文档或教程时,这些文档或教程通常会描述我们试图避免的模式和实践。
总而言之,在编写函数代码方面,Laravel 和 Symfony 一样优秀。前面提到的关于其集合实现的书也是学习如何使用与集合 monad 实现相关的一些功能性技术的好方法。据我所知,Symfony 不存在这种资源。
德鲁帕尔
Drupal 模块直到版本 7 都依赖于钩子来执行操作。Drupal 钩子是遵循特定命名模式的函数,当响应修改生成网页的各个方面的请求时发生各种事件时,Drupal 会调用这些函数。
在理想情况下,所有钩子都将接收执行其工作所需的所有信息,修改某些内容的方法是使用返回值。对于模块 API 的某些部分来说,这基本上是正确的。遗憾的是,有些函数接收通过引用传递的参数,例如hook_block_list_alter
函数。此外,您有时需要访问全局变量,例如,以掌握当前语言。
Drupal8 转向了基于类的方法。现在应该在控制器内创建内容,以便更接近 Symfony 术语。原因是这个新版本现在使用了 Symfony 的一些核心组件。这并不意味着不可能再使用函数式编程,只是事情有点不同。
本书的作用不是详细解释从第 7 版到第 8 版的变化。有很多教程都做得很好。这里介绍的大部分内容都非常通用,对于两个 Drupal 版本都非常有用。
数据库访问
在 Drupal7 中,有多个函数可用于执行数据库查询和访问结果。通常,您从一个db_query
函数开始,该函数返回一个结果对象,使用各种方法来检查和处理数据。首选的 Drupal8 方法不是在模块或服务中注入数据库连接,而是以更面向对象的方式使用它。
这一变化并不会真正影响我们,因为在这两种情况下,都不可能以引用透明的方式查询数据库。此外,人们通常不会将 ORM 与 Drupal 一起使用;对数据库的大多数请求都是直接使用 SQL 完成的。
这就是为什么除了重复尽可能多地隔离数据库访问以使代码的其余部分能够正常工作之外,我们不会继续讨论这个问题的原因。
处理需要副作用的挂钩
最小的 Drupal 模块由两个文件组成,info
文件和module
文件。info
文件的格式从 Drupal 7 中的特殊文本文件更改为 Drupal 8 中的 YAML 文件,但该文件仍然包含有关模块的信息。module
文件是模块的主要 PHP 文件。
正如我们所看到的,一些钩子需要副作用来完成它们的工作,几乎没有办法绕过这个事实。我可以推荐使用模块文件,它将保存所有非严格功能代码,并将所有计算放在其他地方。
在 Drupal8 的情况下,一些控制器方法可能还需要有副作用。在这种情况下,我将给您与 Laravel 和 Symfony 相同的建议:将它们保留在控制器中,并使用外部服务/帮助程序执行引用透明计算。
对于前面提到的hook_block_list_alter
函数,我们将如何做到这一点?首先,这只适用于 Drupal 7,因为在下一个版本中,块是通过一个类来管理的,这样就解决了这个特殊情况下的引用透明性问题。
我的建议是在模块中创建第二个 PHP 文件,只包含纯函数。例如,该文件可能包含一个new_blocks
函数,该函数将当前块和语言作为其唯一参数。
然后,在模块文件中,可以执行以下操作:
function my_module_block_list_alter(&$blocks) {
global $language;
$blocks = new_blocks($blocks, $language);
}
这一功能显然有副作用和副作用;对此我们无能为力。然而,new_blocks
函数可以是纯函数,这意味着您可以很容易地对其进行推理,并像我们在前面章节中看到的那样进行测试。
这种方法几乎可以应用于任何事情。一旦得到副作用或副作用,就在模块文件中执行这些操作,然后使用不同的文件保存纯函数,这将进行必要的处理和计算。如果您使用的是 Drupal 8,而不是使用module
文件,那么您可以使用控制器,正如我们已经为 Symfony 和 Laravel 讨论过的那样。
钩子订单
Drupal 的魅力来自所有可用的模块。这是如此的真实,以至于一些人想出了一个苹果营销口号的变体:有一个模块用于此!。然而,这也带来了一些困难。在质量方面,并非所有模块都是平等的,对于任何给定的应用,您通常会得到一堆模块。
由此推论,您自己的钩子接收的信息可能已经被以前的模块更改了。假设您正在编写一个模块来对页面上的某些块重新排序;完全有可能您希望出现的某些块已经被移除。或者,您在关联数组中使用的密钥可能已注册或将被覆盖。
这可能会导致一些难以准确指出的问题。由于您的函数是纯函数,因此,通过显式添加测试来确保它在给定数据集上按预期工作,相对容易检测到它来自其他内容的事实。
关于这个问题的一个好建议是不要对从 Drupal 收到的任何东西中可能存在或可能不存在的东西进行假设。始终进行某种检查,以确保您收到的数据结构正确且呈现正确。
结束语
可能是出于历史原因,一些 Drupal 挂钩需要有副作用才能履行其职责。此外,并非所有信息都作为参数传递给它们,这需要我们访问全局范围来获取它们。这一事实要求我们找到解决办法,尽可能多地保持代码的纯净。
通过引入一种更面向对象的方法,再加上服务注入,Drupal8 使事情变得更简单。这相当于我们可以和 Simfor 或 Laravel 合作的经验,但是事情仍然不完美。
如果您在至少两个文件中严格区分纯函数和纯函数,那么您编写函数代码的经验会非常好。总是创建两个函数来实现一个钩子可能看起来很麻烦,但如果您想要纯函数,这是您必须付出的代价,而且在我看来,这是值得的。
正如我们所讨论的,即使您的纯代码由于调用某些钩子的顺序而被彻底测试,您仍然可以在最终页面呈现时遇到问题,但是如果您对自己的函数有信心,这些问题通常更容易发现。
Drupal 的功能开发人员体验并不完美,但已经接近完美。您必须做出一些让步,但可以将这些文件绑定到几个文件,以限制它们对代码其余部分的影响。
WordPress
WordPress 还有一个钩子系统,尽管与 Drupal 的不同。不使用特定名称创建函数,而是将函数注册到特定挂钩。不幸的是,根据定义,这些钩子中的大多数都需要有副作用。例如,我们可以借助wp_footer
钩子来实现这一点:
此钩子不提供参数。通过将函数 echo 输出到浏览器,或者让它执行后台任务,可以使用此钩子。您的函数不应该返回,也不应该接受任何参数。
没有返回值,没有参数;我们被迫创建一个具有副作用的函数。这意味着您必须围绕代码创建包装函数,甚至比我们刚才用 Drupal 演示的还要多。
幸运的是,WordPress 还允许每个插件有多个文件。因此,建议将所有不纯代码放在主文件中。从全局上下文中获取所需的所有信息,并在全局上下文中执行任何具有副作用的操作。一旦您拥有了所需的一切,就可以调用纯函数进行处理和计算。
一些 WordPress 教程将面向对象编程作为开发人员的下一个发展方向,当他们掌握了编写插件的更程序化的方法时。如果您计划使用功能性技术,这并不重要。您可以仅使用函数来组织代码,也可以将它们分组到类中。我建议你坚持你比较习惯的方法。
数据库访问
WordPress 插件中基本上有两种访问数据库的方法。您可以直接使用WP_Query
方法及其面向对象接口。或者您可以使用辅助功能,如get_posts
和get_pages
。
你可能听说过或读到过这样的消息:在使用类编写插件时最好使用WP_Query
函数,在使用函数时最好使用助手。从功能的角度来看,这无关紧要。它们中没有一个在任何方面都是透明的。你可以用你最喜欢的或更适合你需要的。
关于 WordPress 代码库中的数据库访问,没有什么可说的。这个问题与其他框架一样,目前还没有办法以纯粹的方式执行它们。
话虽如此,建议还是一样,尝试将任何带有副作用和副作用的代码分离到插件的单个文件中,然后将计算和处理委托给其他地方的纯函数。
功能性方法的好处
我在本部分的介绍中说过,使用函数还是对象来组织代码并不重要。这只是部分正确。WordPress 缺少 Symfony 或 Laravel 等框架的所有注入功能。这意味着,如果您使用的是对象,则在共享周围的实例时会遇到困难。
如果您的对象仅用于保存不使用任何内部状态的纯方法,那么这并不是一个真正的问题,但是正如我们所看到的,有时需要做出让步。如果您需要与这样一个状态共享一个实例,您唯一的解决方案是使其在全局可用。这样一个变量的问题是,它可以被重新分配给其他变量,导致以后出现问题。
相反,函数在任何地方都可用,您无法重新定义它。这将导致更健壮的代码,因为您限制了副作用的可能性。
结束语
Drupal 的第一个版本可以追溯到 2000 年,这使它成为这里介绍的最古老的工具。WordPress 出生于 2003 年。然而,Drupal 从那时起就被重写了,而 WordPress 的代码库大部分是在没有完全重写的情况下扩展的。
我为什么要告诉你这些?因为您在尝试用 WordPress 编写功能代码时遇到的大多数问题都与它的遗留代码库有关。2000 年编写软件的方式与我们现在期望的最佳实践略有不同。
为了使 WordPress 现代化,已经做了很多工作,但你能做的只有这么多。特别是当重点不是使框架功能性开发人员友好时。尽管如此,如果您愿意跳过一些障碍来隔离有副作用的部分,那么编写功能代码是可能的。
WordPress 主要基于钩子,大部分 API 由函数组成。其中一些是指代透明的,另一些则根本不透明。要将这些代码与代码的其余部分清晰地隔离开来,需要进行一些严格的操作。好处总是一样的:
- 减少认知负担
- 促进代码重用
- 更容易的测试
不利的一面是,您的主插件文件大部分由非常小的函数组成,这些函数只作为 WordPressAPI 的不纯净函数的包装器,然后调用您的引用透明函数。
如果包装器的名称与 Wordpress 的名称非常接近,那么对任何了解 Wordpress 的人来说,阅读代码并在其中导航应该是非常容易的。最后,编写尽可能多的功能代码仍然是一个好主意。
总结
正如我们前面讨论过的,没有一个主流框架的核心是功能性方法。在本章中,我们试图了解我们所学到的技术如何或多或少成功地应用于一些可用的框架和 CMSE。
正如我们所看到的,至少在某种程度上使用函数式编程总是可能的。遗憾的是,根据框架的不同,您将不得不在某个时候创建非引用透明的代码。
正如我在导言中所说的,我不是本章讨论的所有库的专家,所以对一切都持保留态度。经验更丰富的开发人员可能会做不同的事情。然而,这些示例为想要尝试函数式编程的人提供了一个很好的起点。
此外,在这样做时,请记住,这首先是一种思维方式。最重要的是你处理手头问题的方式。如果在某个时候,您需要创建非纯代码来适应外部依赖项或您正在使用的框架,那么就这样吧。这不会改变您所编写的函数代码所带来的好处。
现在我们已经了解了如何在现有框架或遗留代码库中使用函数式编程,下一章将介绍如何使用称为函数式反应式编程或 FRP 的范例设计整个应用。
版权属于:月萌API www.moonapi.com,转载请注明出处