17611538698
webmaster@21cto.com

PHP语法糖的隐藏架构成本

编程语言 0 38 1天前
图片

21CTO导读:PHP8中有不少语法糖,但却也存在着隐藏的架构成本。

先说一下啥是语法糖(Syntactic Sugar),它是指编程语言中添加的某种语法结构,这种结构并没有引入新的功能,但能让代码写起来更简洁、读起来更直观、用起来更顺手。

一句话概括是:逻辑没变,只是“语法变甜了”,程序员的敲击键盘的次数变少、工作效率烃高,并提高了代码可读性。

它还会帮助程序员掩盖遗留代码库中的“异味”。

你是否曾经处理过极其复杂的遗留代码库?当没有明确的界限时,这项工作通常很快就会失控。

最近,在 PHPVerse 的一次讨论中,Larry Garfield 强调构造函数提升属性是过去十年 PHP 最优秀的特性之一,因为它极大地简化了依赖注入。

他的观点可说是完全正确。

但语法糖是一把双刃剑。它虽然能巧妙地消除代码摩擦,但在庞大的音乐平台遗留代码库中,这种摩擦本身往往是糟糕设计的天然阻碍。当编程语言不再惩罚糟糕的架构时,责任就完全转移到了我们自身的专业领域。

代码太长不看


我们都喜欢现代的 PHP 语法,但消除输入繁琐可能掩盖了严重的代码异味,例如“上帝对象”和作用域泄漏。这里可以使用架构边界和严格的静态分析来保持应用程序的可读性和可维护性。

构造函数提升属性与对象体操


先来了解一下静态分析的一个概念。如果你实践过对象体操(Object Calisthenics,一套旨在强制执行良好面向对象设计的规则),你可能熟悉这条规则:保持类的简洁,并限制实例变量的数量。过多的依赖关系是类违反单一职责原则(SRP)的明显标志。

在旧系统中,单一职责原则 (SRP) 违规会导致 Spotify 发布逻辑出现 bug,进而破坏音频标准化流程。在 PHP 8.0 之前,SRP 违规会带来严重的后果。

这个 22 行的构造函数写起来很麻烦。这种繁琐的操作几乎是在逼你重构代码并拆分职责。

// PHP 7.4: The pain of boilerplate signals a code smell.class AlbumIngestionService {    private TrackRepository $trackRepo;    private AudioNormalizer $normalizer;    private CoverArtResizer $artResizer;    private SpotifyApiGateway $spotifyGateway;    private MetadataValidator $validator;    public function __construct(        TrackRepository $trackRepo,        AudioNormalizer $normalizer,        CoverArtResizer $artResizer,        SpotifyApiGateway $spotifyGateway,        MetadataValidator $validator    ) {        $this->trackRepo = $trackRepo;        $this->normalizer = $normalizer;        $this->artResizer = $artResizer;        $this->spotifyGateway = $spotifyGateway;        $this->validator = $validator;    }}

现在,让我们来看看现代 PHP 中同样的架构缺陷:

// PHP 8+: The code smell is masked by elegance.class AlbumIngestionService {    public function __construct(        private TrackRepository $trackRepo,        private AudioNormalizer $normalizer,        private CoverArtResizer $artResizer,        private SpotifyApiGateway $spotifyGateway,        private MetadataValidator $validator,    ) {}}

它美观、简洁,编写起来也只需几秒钟。但这恰恰是危险所在。

这种“糖衣炮弹”让臃肿、中心化的上帝对象看起来赏心悦目,却在悄无声息中加深了遗留问题的复杂性。你可能在不经意间就添加了第六个甚至第七个依赖项!

短箭头功能和瞄准镜陷阱


现在让我们来关注经常遇到的一个最常见问题:数组迭代和作用域。我们已经了解了如何通过对部分代码进行作用域划分来处理长脚本,现代 PHP 引入了短箭头函数(fn() => ...)。

在对遗留代码库进行现代化改造时,严格控制变更范围至关重要。你需要确保变量和状态不会泄露到不应该泄露的地方。对于标准的匿名函数,PHP 强制我们使用 ` use@` 关键字显式地指定哪些局部变量跨越了作用域边界。

短箭头函数消除了这一要求。但是,关于对象上下文,存在一个隐藏的陷阱:$this

在标准的 PHP 闭包中,$this除非显式声明,否则对象会自动绑定static function()。对于短箭头函数也是如此。由于语法非常简洁,开发者经常fn()在任何地方使用闭包,而忽略闭包是否真的需要访问对象实例。

class AlbumTrackProcessor {    public function formatDurations(array $tracks): array     {        // $this is bound here, even though it's never used!        return array_map(fn($track) => gmdate("i:s"$track->getDurationSeconds()), $tracks);    }}

这也就是为什么这在大型遗留应用程序中会造成一些影响。

如果该闭包被返回或传递给长时间运行的后台工作进程(例如异步音频转码队列),隐式绑定$this会阻止AlbumTrackProcessor实例被垃圾回收。这会悄无声息地导致内存泄漏😞。

从技术上讲,正确的做法是写成这样static fn($track) => ...,但视觉上的笨拙感让许多开发者失去了使用“短”箭头函数的意义。

PHP 内部开发社区正在持续讨论未来的优化方案,例如自动静态短箭头函数。引擎可以自动将$this未使用的闭包编译为静态闭包。虽然这巧妙地解决了内存泄漏问题,无需额外的按键操作,但也凸显了一个正在日益增长的趋势:我们为了便捷而牺牲了显式性。在隐藏状态被视为敌人的遗留应用程序中,依赖引擎的“魔法”来管理作用域边界可能是一种冒险的做法。

结语


我们来总结一下:

  • 语法糖总体来说是有益的。没人愿意回到过去那种为了播放一张专辑而编写 30 行构造函数赋值语句的时代。
  • 摩擦是一种反馈。随着语言逐渐消除过去用来警示我们设计缺陷的摩擦,我们必须更加依赖其他质量控制机制。
  • 工具必不可少。在庞大的遗留代码库中,我们不能再仅仅依靠手动输入来判断类是否执行了过多操作或是否存在作用域泄漏。严格的静态分析(例如PHPStan)和自动化的架构边界(例如DeptracPHPArkitect)现在相当重要。


按键操作从来都不是真正的敌人,架构的复杂性才是。而PHP 8的复杂性正在呈现出令人惊艳的美感。

作者:托马斯·杜特里恩

编译:聆听音乐的鱼

来源:

https://medium.com/believe-tech/the-hidden-architectural-cost-of-phps-syntactic-sugar-f0e2bd0ef0e8

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信