机械重复的事,就让机器来做吧

前言

机器的诞生就是为了减轻人类的工作,科学的进步往往带来大量的失业,但这也意味着效率的提高,工厂的流水线作业总有一天会被机器人取代,而那些进厂打工的人就得重新寻找谋生的工作了,不论这种辛酸是否值得惋惜,但是使用机械取代人类代表着可以节约大量人力,腾出的人力就可以去做更多的事情了。

以码农为例,重复的代码好比流水线作业,日复一日重复着简单的增删改查,繁重的工作会让人饱受精神折磨,并且对技术的提升毫无作用,所以,如何避免重复性劳动成为重中之重。

查询参数的优化

码农的日常工作无非就是写一些增删改查,写一个后台 WEB 系统就需要有用户管理、分类管理、文章管理……诸如此类,然后我们就得一个个写控制器,查询就得要有查询参数,增删改操作还得判断是否允许增删改,这些都是十分琐碎的事情,全部堆积起来就是恐怖的事情,例如以优雅著称的 Laravel 在遇到查询参数的时候也是“不优雅”了,如下图,这是一个常见的列表数据:

列表数据

顶部有一个搜索栏,用来搜索指定名称、邮箱或者注册 IP 的用户,因而就需要用到查询参数,所以在 Laravel 中,获取用户列表的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function getList(): \Hyperf\Contract\LengthAwarePaginatorInterface
{
$query = User::query();
$params = $this->request->all();

if (isset($params['name'])) {
$query->where('name', 'like', '%' . $params['name'] . '%');
}

if (isset($params['email'])) {
$query->where('email', 'like', '%' . $params['email'] . '%');
}

if (isset($params['register_ip'])) {
$query->where('register_ip', 'like', '%' . $params['register_ip'] . '%');
}

return $query->paginate(10);
}

每有一个查询条件就得用 ifisset 判断一下,上面的代码除了参数不同,其他地方一模一样,这就属于机械重复劳动,因此可以使用一个循环来代替从而节省工作量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getList(): \Hyperf\Contract\LengthAwarePaginatorInterface
{
$query = User::query();
$params = $this->request->all();
$queryFields = ['name', 'email', 'register_ip'];

foreach ($queryFields as $field) {
if (isset($params[$field])) {
$query->where($field, 'like', '%' . $params[$field] . '%');
}
}

return $query->paginate(10);
}

现在看起来是好多了,但是如果接下来要再添加一个分类管理和文章管理,下面循环体的部分又会成为「重复代码」:

1
2
3
4
5
foreach ($queryFields as $field) {
if (isset($params[$field])) {
$query->where($field, 'like', '%' . $params[$field] . '%');
}
}

ORM 的设计者们也发现到了这个问题,于是可以使用下面的方式进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getList(): \Hyperf\Contract\LengthAwarePaginatorInterface
{
$request = $this->request;
return (new User())
->when($request->has('name'), function ($query) use ($request) {
$query->where('name', 'like', '%' . $request->input('name') . '%');
})
->when(!$request->has('email'), function ($query) use ($request) {
$query->where('email', 'like', '%' . $request->input('email') . '%');
})->when(!$request->has('register_ip'), function ($query) use ($request) {
$query->where('register_ip', 'like', '%' . $request->input('register_ip') . '%');
})
->paginate(10);
}

这种做法也就是将 if 条件优化了一下而已,治标不治本,这样一坨代码看着就头大,甚至还不如我上面用循环体的方式处理来得简洁。每有一个列表需要展示,我们就得再重复写一次这个循环体,那为什么不能封装起来呢?上一篇文章写到协程调度器,那为什么查询就不能写一个「查询解析器」呢?类的封装就是为了复用代码,先分析一下现在的需求:

1、查询参数有多种查询模式,例如:等于、Like 等
2、判断查询参数是否存在,如果存在就给 SQL 附加查询条件

捋清楚思路之后就简单了,首先创建一个用来保存查询类型的枚举类 QueryConstant

1
2
3
4
5
enum QueryConstant
{
case EQUAL;
case LIKE;
}

接着再将上面演示的循环体封装成一个类进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace App\Query;

use Hyperf\Database\Model\Builder;

class QueryHandler
{
protected array $rules;

public function __construct(array $rules)
{
$this->rules = $rules;
}

public function query(string $modelClass): Builder
{
$query = make($modelClass)->query();
$params = request()->all();

foreach ($this->rules as $key => $val) {

if (!array_key_exists($key, $params)) continue;

switch ($val) {
case QueryConstant::EQUAL:
$query->where($key, $params[$key]);
break;
case QueryConstant::LIKE:
$query->where($key, 'like', '%' . $params[$key] . '%');
break;
}
}

return $query;
}
}

现在我们将循环体封装成了「查询参数解析器」,接着修改控制器的获取列表代码:

1
2
3
4
5
6
7
8
9
10
public function getList(): \Hyperf\Contract\LengthAwarePaginatorInterface
{
$handler = new QueryHandler([
'name' => QueryConstant::LIKE,
'email' => QueryConstant::LIKE,
'register_ip' => QueryConstant::LIKE,
]);

return $handler->query($this->model)->paginate(10);
}

这样查询解析器就完成了,以后不管有多少查询列表的需求,如果需要用到查询参数就可以直接使用查询解析器来处理,代码更加简洁,而且重复的代码也全部去除了,心情愉♂悦,现在只需要传入一个数组类型的 rules 变量,就可以自动查询这些参数了。

资源控制器

解决了查询参数繁琐重复的问题,可后台的功能无外乎增删改查,有用户管理、分类管理、文章管理……每一个控制器都需要写增删改查,在 Laravel 中可以使用命令一键生成资源控制器:

1
php artisan make:controller PhotoController --resource

该命令将在您的控制器目录中创建一个 PhotoController.php 文件,并将自动创建 7 种方法 index ,show,create,store,edit,update,destroy。 所有这些方法都是空的,您必须为每个操作添加逻辑。

上述为 Laravel 文档的说明,也就是说可以通过命令节省了你创建文件的功夫,但每个控制器的方法还是得自己手撸,同样是治标不治本。当删除一个数据的时候,我们得判断一下是不是允许被删除,当创建一个数据的时候,我们也得判断新数据是否跟数据库其他数据有重复,这些同样是「重复性劳动」。

机器之所以能够取代人类部分工作,乃是因为可以将「重复」、「规律性」的行为交给机器去干,毕竟机器没有 AI,它只能进行机械的重复工作,换句话说只要找到规律就可以让机器去干重复的劳动。

以删除数据为例,它们有着共通点:

1、判断是否允许删除
2、执行删除
3、删除完成之后的操作

想删除一个商品的订单,那么首先应该判断这个数据是否还存在,并且是否可以被删除,比如用户下单之后,商家很快就发货了,订单处于发货状态,那这个订单就不能删掉了,所以在删除前需要进行两个判断:①订单数据是否还在(而不是已被删除了)②订单状态是否允许被删除,当满足了上面两个条件,就开始执行删除操作,删除完成之后也需要再执行一次回调,比如通知卖家该订单已删除,无需发货。

这其实就是前面一篇文章提到的面向切面编程相似的原理,也就是生命周期的概念:删除前、删除、删除后……诸如此类,参照 Laravel 资源控制器,我们可以知道有哪些常规的增删改查方法,那直接提取出来即可,我们来创建一个简单的资源控制器父类,一个资源控制器无非就是显示视图以及提供 API 给前端调用,控制器管理的是一个模型,比如用户控制器就管理 user 表的数据,而管理 MYSQL 数据可以使用 Model 类来处理,因此一个控制器对应一个 Model,而所有的视图为了规范我们也会放在同一个路径,因而提取出两个参数:modelpath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<?php
/**
* @author FireRabbit
* @date 2023/2/20 01:44
*/


namespace App\Controller;

use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\Contract\ViewInterface;
use Psr\Http\Message\ResponseInterface;
use function Hyperf\ViewEngine\view;

abstract class ResourceController extends AbstractAdminController
{
/**
* 当前资源的模型类名称
* @var string
*/
protected string $model;

/**
* 当前返回的基础视图路径
* @var string
*/
protected string $path;

public function index()
{
$items = $this->getList();
return $this->template('index', compact('items'));
}

public function edit()
{
$item = make($this->model)->find($this->request->input('id'));
$initParams = $this->initViewParams();
$action = '编辑';

return $this->template('edit', array_merge(compact('item', 'action'), $initParams));
}

public function create()
{
$initParams = $this->initViewParams();
$action = '创建';
return $this->template('edit', array_merge(compact('action'), $initParams));
}

public function store()
{
$res = $this->checkEnableCreate();
if (!is_bool($res)) {
return $res;
}

return $this->onStore();
}

public function update()
{
$res = $this->checkEnableCreate();
if (!is_bool($res)) {
return $res;
}

return $this->onUpdate();
}

public function delete()
{
$res = $this->checkEnableDelete();
if (!is_bool($res)) {
return $res;
}

return $this->onDelete();
}

/**
* create和edit方法传递给模板的数据(例如更新文章时的分类数据)
* @return array
*/
protected function initViewParams(): array
{
return [];
}

/**
* 返回HTML模板
* @param $filename
* @param array $params
* @return FactoryInterface|ViewInterface
*/
protected function template($filename, array $params = [])
{
return view('admin.' . $this->path . '.' . $filename, $params);
}

protected abstract function getList(): LengthAwarePaginatorInterface;

protected abstract function onStore(): ResponseInterface;

protected abstract function onUpdate(): ResponseInterface;

protected abstract function onDelete(): ResponseInterface;

/**
* 在创建数据时的条件判断
* @return bool|ResponseInterface
*/
protected function checkEnableCreate(): bool|ResponseInterface
{
return true;
}

/**
* 判断是否允许更新数据
* @return bool|ResponseInterface
*/
protected function checkEnableUpdate(): bool|ResponseInterface
{
return true;
}

/**
* 在删除数据之前判断是否允许
* @return bool|ResponseInterface
*/
protected function checkEnableDelete(): bool|ResponseInterface
{
return true;
}
}

显示列表、创建数据、编辑数据的视图都是统一的,我们只需要将唯一不同的地方抽取出来即可,每个资源控制器都有一个 Model 模型类,以及对应的视图路径,如果想创建一个资源控制器,只要让子类继承此父类即可,并且这个资源控制器父类是根据生命周期来进行自动化操作的,比如创建一个用户数据,因为邮箱是用来当做登录凭证的,并且也不允许用户有同名,所以在创建数据之前应该进行判断是否允许创建:

1
2
3
4
protected function checkEnableCreate(): bool|ResponseInterface
{
return true;
}

子类通过重写这个方法,如果返回的是布尔值真,则表示允许创建,如果返回的是响应,那么代表创建数据时遇到问题不能创建(名字、邮箱被其他用户占用的情况),则返回响应即可,用户管理资源控制器示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?php
/**
* @author FireRabbit
* @date 2023/2/14 23:04
*/


namespace App\Controller\Admin;

use App\Controller\ResourceController;
use App\Middleware\Admin\AuthPermission;
use App\Model\User;
use App\Query\QueryConstant;
use App\Query\QueryHandler;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Annotation\Middleware;
use Psr\Http\Message\ResponseInterface;

#[AutoController(prefix: '/admin/user')]
#[Middleware(AuthPermission::class)]
class UserController extends ResourceController
{
public function __construct()
{
$this->model = User::class;
$this->path = 'user';
}

protected function getList(): \Hyperf\Contract\LengthAwarePaginatorInterface
{
$handler = new QueryHandler([
'name' => QueryConstant::LIKE,
'email' => QueryConstant::LIKE,
'register_ip' => QueryConstant::LIKE,
]);

return $handler->query($this->model)->paginate(10);
}

protected function checkEnableCreate(): bool|\Psr\Http\Message\ResponseInterface
{
$params = $this->request->all();
$exists = User::where([
'email' => $params['email'],
'name' => $params['name'],
])->first();

if ($exists) {
$msg = $exists->name == $params['name'] ? '名称已存在' : '邮箱已存在';
return api_error($msg);
}

return true;
}

protected function onStore(): \Psr\Http\Message\ResponseInterface
{
$params = $this->request->all();
$data = [];

$fields = ['name', 'email'];
foreach ($fields as $field) {
$data[$field] = $params[$field];
}

$data['created'] = time();
User::create($data);

return api_success();
}

protected function checkEnableUpdate(): bool|ResponseInterface
{
return $this->checkEnableCreate();
}

protected function onUpdate(): ResponseInterface
{
$params = $this->request->all();
User::where('id', $params['id'])->update($params);

return api_success();
}

protected function onDelete(): ResponseInterface
{
$params = $this->request->all();
User::where('id', $params['id'])->delete();

return api_success();
}
}

删除和更新的原理也是一样,基于生命周期的理念实现这种资源控制器可以大大节省人工判断的流程,毕竟增删改查在后台的开发中大量存在,如果每个控制器都得手撸一遍那会是非常痛苦的事情,针对一些复杂功能的,也可以直接重写资源控制器的各种方法,但后台的功能无外乎就是对数据增删改查,上述控制器已经可以满足大多数需要了。

有的时候,我们不应该一味的撸业务代码,而应该在开发流程上多花些功夫,如果说撸业务代码是为了赶进度,那么优化开发流程就是提升效率,如果开发效率低下,倒不如优先花点时间优化开发流程,提升效率远比埋头苦干好得多,这就是后发先至。

文章作者: 火烧兔子
文章链接: http://huotuyouxi.com/2023/02/20/2023-news-2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 火兔游戏工作室