重构的乐趣

发布时间 作者:

Fun with Refactoring image

重构不是一个贬义词,恰恰相反。重构是你在升级或普遍改进后会做的事情。如果你没有改进,重构是不可行的,当然,除非你第一次没有尝试过!

本教程将讨论重构,我们可以做些什么,以及我们如何处理一些现实世界的例子。我们将使用一个在 GitHub 上开源的实际项目,并逐步进行重构练习。我们将使用的项目是 Laravel Shift Blueprint,这是一个流行的引导 Laravel 应用程序的方法。

让我们深入研究。我已经创建了一个新的存储库分支来隔离地构建东西 - 在我的帐户中运行 GitHub 操作,这样核心库就不会被淹没。现在我已经做到了,我可以克隆我的分支并开始在本地工作。

当我打开项目时,我注意到 composer 需要设置一个所需的 PHP 级别。这不是什么大问题 - 然而,对我来说,我会把它设置为最低支持的 PHP 版本。PHP 8.0 是唯一一个有足够支持让我安心写代码的版本。让我们做第一个改变。

"require": {
"doctrine/dbal": "^3.3",
"illuminate/console": "^9.0",
"illuminate/filesystem": "^9.0",
"illuminate/support": "^9.0",
"laravel-shift/faker-registry": "^0.2.0",
"symfony/yaml": "^6.0"
},

更新依赖项并添加语言约束后,现在看起来像这样

"require": {
"php": "^8.0",
"doctrine/dbal": "^3.5",
"illuminate/console": "^9.39",
"illuminate/filesystem": "^9.39",
"illuminate/support": "^v9.39",
"laravel-shift/faker-registry": "^0.2.0",
"symfony/yaml": "^6.1"
},

我们需要在每次更改前后运行测试,以确保最新的更改没有破坏任何东西。如果它坏了,我们需要回滚或重构损坏的部分。这是重构过程中的一个关键步骤,因为你总是希望确保你是在为了项目的改进而进行重构 - 而不是仅仅因为你有不同的意见。

OK (430 tests, 2056 assertions)

到目前为止,一切都很好!现在让我们继续看看代码本身。我注意到在打开代码库中的随机文件时,它没有利用类型提示或返回类型。此外,由于我们现在已经设置了最低 PHP 版本,我们可以更新我们使用 PHP 语言的方式。让我们看一个简单的例子

class Tree
{
private $tree;
 
public function __construct(array $tree)
{
$this->tree = $tree;
 
$this->registerModels();
}
 
private function registerModels()
{
$this->models = array_merge($this->tree['cache'] ?? [], $this->tree['models'] ?? []);
}
 
// More code was there, but let's simplify
}

查看这段代码,我们知道我们可以进行一些快速的重构改进 - 只是为了达到我们现在希望设置为最低的语言级别。首先,让我们修复构造函数并删除属性。

class Tree
{
public function __construct(
private array $tree,
) {
$this->registerModels();
}
 
// ...
}

转而使用构造函数属性提升,使我们能够简化代码。让我们运行我们的测试。

OK (430 tests, 2056 assertions)

仍然很好。我们对这段代码进行了一个简单的重构,可以继续进行下一部分。让我们看看在构造函数中调用的方法。我们在类上有一个动态属性 - 这是在未来的 PHP 支持版本中会被删除的。虽然现在可能不是问题 - 但我们知道将来会出现问题。因此,我们可以提前进行重构。

class Tree
{
public function __construct(
private array $tree,
private array $models = [],
) {
$this->registerModels();
}
 
private function registerModels(): void
{
$this->models = [
...$this->tree['cache'] ?? [],
...$this->tree['models'] ?? []
];
}
 
// ...
}

我们现在在构造函数中为类设置了属性 - 并重构了数组合并以使用数组解构。这使我们的代码更容易阅读。让我们重新运行这些测试!

OK (430 tests, 2056 assertions)

让我们看看下面的方法:一个从 Tree 类中获取控制器的 getter。我们目前没有返回类型,需要知道这可能包含什么。使用“查找使用情况”快速浏览代码库,我发现别的地方我们正在动态地将它设置为 Controller 的实例。让我们看看这个方法

public function controllers()
{
return $this->tree['controllers'];
}

首先,我们想要添加返回类型,它将是一个数组。

public function controllers(): array
{
return $this->tree['controllers'];
}

到目前为止,一切都很好。所有测试都还在通过。但这够了吗?当我们可以在一个地方让我们的项目知道这一点时,我们应该在项目中添加一个动态类型设置吗?让我们更新它。

/**
* @return array<int,Controller>
*/
public function controllers(): array
{
return $this->tree['controllers'];
}

我们知道这将返回一个 Controller,所以为什么不在我们的 docblock 中将其设置为通用类型,这样我们就可以充分利用现代 PHP?现在重新运行我们的测试。

OK (430 tests, 2056 assertions)

到目前为止,我们的重构都很成功。让我们继续另一个例子。

接下来,让我们看看这个包的 ServiceProvider。像另一个例子一样,这里没有返回类型 - 所以我将快速添加它们,在这里不担心这些例子。让我们看看如何改进 register 方法。

public function register(): void
{
$this->mergeConfigFrom(
__DIR__ . '/../config/blueprint.php',
'blueprint'
);
 
File::mixin(new FileMixins());
 
$this->app->bind('command.blueprint.build', fn ($app) => new BuildCommand($app['files'], app(Builder::class)));
$this->app->bind('command.blueprint.erase', fn ($app) => new EraseCommand($app['files']));
$this->app->bind('command.blueprint.trace', fn ($app) => new TraceCommand($app['files'], app(Tracer::class)));
$this->app->bind('command.blueprint.new', fn ($app) => new NewCommand($app['files']));
$this->app->bind('command.blueprint.init', fn ($app) => new InitCommand());
$this->app->bind('command.blueprint.stubs', fn ($app) => new PublishStubsCommand());
 
$this->app->singleton(Blueprint::class, function ($app) {
$blueprint = new Blueprint();
$blueprint->registerLexer(new \Blueprint\Lexers\ConfigLexer($app));
$blueprint->registerLexer(new \Blueprint\Lexers\ModelLexer());
$blueprint->registerLexer(new \Blueprint\Lexers\SeederLexer());
$blueprint->registerLexer(new \Blueprint\Lexers\ControllerLexer(new \Blueprint\Lexers\StatementLexer()));
 
foreach (config('blueprint.generators') as $generator) {
$blueprint->registerGenerator(new $generator($app['files']));
}
 
return $blueprint;
});
 
$this->app->make('events')->listen(CommandFinished::class, function ($event) {
if ($event->command == 'stub:publish') {
$this->app->make(Kernel::class)->queue('blueprint:stubs');
}
});
 
$this->commands([
'command.blueprint.build',
'command.blueprint.erase',
'command.blueprint.trace',
'command.blueprint.new',
'command.blueprint.init',
'command.blueprint.stubs',
]);
}

首先,我们在容器中绑定了很多东西,并使用字符串常量,这是可以的 - 但是我们可以快速改进它。

$this->app->bind(BuildCommand::class, fn ($app) => new BuildCommand($app['files'], app(Builder::class)));
$this->app->bind(EraseCommand::class, fn ($app) => new EraseCommand($app['files']));
$this->app->bind(TraceCommand::class, fn ($app) => new TraceCommand($app['files'], app(Tracer::class)));
$this->app->bind(NewCommand::class, fn ($app) => new NewCommand($app['files']));
$this->app->bind(InitCommand::class, fn ($app) => new InitCommand());
$this->app->bind(PublishStubsCommand::class, fn ($app) => new PublishStubsCommand());

这是第一步;接下来,让我们添加命名参数,使它看起来更干净。

$this->app->bind(
abstract: BuildCommand::class,
concrete: fn (Application $app): BuildCommand => new BuildCommand(
filesystem: $app['files'],
builder: $app->make(
abstract: Builder::class,
),
),
);
$this->app->bind(
abstract: EraseCommand::class,
concrete: fn (Application $app): EraseCommand => new EraseCommand(
filesystem: $app['files'],
),
);
$this->app->bind(
abstract: TraceCommand::class,
concrete: fn (Application $app): TraceCommand => new TraceCommand(
filesystem: $app['files'],
tracer: $app->make(
abstract: Tracer::class,
),
),
);
$this->app->bind(
abstract: NewCommand::class,
concrete: fn (Application $app): NewCommand => new NewCommand(
filesystem: $app['files'],
),
);
$this->app->bind(
abstract: InitCommand::class,
concrete: fn (): InitCommand => new InitCommand(),
);
$this->app->bind(
abstract: PublishStubsCommand::class,
concrete: fn () => new PublishStubsCommand(),
);

至少对我来说,现在看起来干净多了。我们添加了更多的类型提示,并为清晰起见添加了返回类型。并不是每个人都会同意命名参数,但这使代码更易于阅读。

重构的目的是使代码更易于管理、更高效,或者两者兼而有之。我们在上面所做的展示了我们如何使代码更类型安全,更易于管理。Blueprint 是一个相当复杂的项目。它有很多活动部件,以确保它的正常运行。如果不花很长时间深入了解项目的具体细节及其未来目标,很难看出可以做出哪些改进。

我重构的典型方法是首先解决那些快速获胜的问题,即支持的语言级别和软件包版本,然后是类型提示和返回类型,然后继续现代化代码的各个部分。在逐步进行的过程中,务必确保测试仍在运行,这一点至关重要。

Steve McDougall photo

技术作家,任职于 Laravel 新闻,开发者倡导者,任职于 Treblle。API 专家,资深 PHP/Laravel 工程师。 YouTube 直播主.

Cube

Laravel 新闻通讯

加入 40,000 多名其他开发者,绝不错过新的提示、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和本地和生产环境中的即时反馈进行调试。

Tinkerwell
No Compromises logo

绝不妥协

Joel 和 Aaron,两位来自“绝不妥协”播客的资深开发者,现在可以为您的 Laravel 项目提供服务。 ⬧ 固定价格 7500 美元/月。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

绝不妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,让您的应用程序保持新鲜感。

Shift
Bacancy logo

Bacancy

只需每月 2500 美元,即可获得拥有 4-6 年经验的资深 Laravel 开发人员,为您的项目注入活力。获得 160 小时的专业知识和 15 天的无风险试用。立即安排通话!

Bacancy
Lucky Media logo

Lucky Media

现在就获得幸运 - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar:Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的力量带入 Laravel。

Lunar:Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 工作板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit:Laravel SaaS 启动套件

SaaSykit 是一个 Laravel SaaS 启动套件,包含运行现代 SaaS 所需的所有功能。付款、漂亮的结账、管理面板、用户仪表板、身份验证、现成的组件、统计数据、博客、文档等等。

SaaSykit:Laravel SaaS 启动套件
Rector logo

Rector

您无缝 Laravel 升级、降低成本和加速创新的合作伙伴,帮助成功企业。

Rector
MongoDB logo

MongoDB

通过将 MongoDB 和 Laravel 強力集成,增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。在使用熟悉的 Eloquent API 的同时,支持事务、搜索、分析和移动用例。探索灵活的现代数据库 MongoDB 如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel Prompts 构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包为您的 Laravel 应用程序添加评论

阅读文章