构建一个PHP工程化项目 (杂谈篇)

前言

本篇文章是开启我写作之路的第一篇技术文章,有一些东西觉得可以和大家分享

如果文中有描述不合理的地方,望大家海量指出。

本系列文章我会根据我的实际项目经验谈谈以下内容

  • 为什么要工程化
  • 如何更好的组织业务代码
  • PHP 来构建一个基于微服务 的商城API

其中 实战 一节我将放在下一篇文章中去叙述 (Show Code)

为什么工程化

本篇谈及的工程化仅在代码组织和项目组织上的一些见解

我相信大家在多年的工作中一定接手过不少项目代码

搭建项目维护项目 再到 开发新功能 这个过程中,我相信原先写代码的兄弟已经被问候多次。

我认为一个值得花时间去学习和研究的项目应该具有以下几点特征

项目结构组织明确

没有严格的按照工程层次划分目录, 随心所欲,导致冗余目录和相似功能目录增多,让人心里抵触。

我觉得一个良好的项目工程应该是经过系统的设计,并且他人从项目结构中可以轻易看出系统层次。

image-20211128155850776.png

以一个微服务中的工程标准,我一般会按以下结构组织代码

1
2
3
4
5
6
7
8
9
10
11
markdown复制代码├─app           // 服务APP
│ ├─api // API层 对外提供 HTTP/JSON RPC
│ ├─model // 模型层 存放对数据模型的定义(包括表数据模型和业务输出输入模型)
│ └─service // 服务层 业务逻辑封装管理,特定的业务逻辑实现和封装。
│ └─constants // 存放常量及消息的映射
├─deploy // 部署脚本目录
├─config // 配置目录
├─tests // 测试目录
├─Dockerfile // 构建镜像
├─composer.json // 管理第三方包的依赖
├─.env // 项目的环境配置文件

工程化组织代码

代码组织混乱最常见的几种情况

  • 变量定义不规范, 常量使用一堆幻数
  • 代码可复用性低
  • 业务核心逻辑职责不明确

下面我会通过几段代码来说明一下工程化 组织带来的好处,希望借此抛砖引玉。

统一管理常量

项目中使用大量的数字来标识状态,会使他人接手项目的理解成本上线。

将常量的定义统一放到 类似 constants 的文件夹中统一管理, 我们可以轻易从项目角度去快速理解业务的上下文。

例如我需要一个类来专门管理这些订单的状态

根据上文中组织的项目结构,我们可以在 app/constants 中建立一个 OrderStatus.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
php复制代码<?php
/**
* 订单状态常量
*/
namespace app\constants;


class OrderStatus
{
/**
* @Message('待付款')
*/
const TO_BE_PAY = 10;

/**
* @Message("待发货")
*/
const TO_BE_DELIVERY = 11;

...
}

?>
用设计模式消除重复代码

重复的代码会让系统变得臃肿,难以维护,增加接手项目人员的负担

消除重复的方式无非是 封装抽象

我们站在工程的角度说明一下如何让一个充值的业务模块更直观从而减少重复

笔者之前接手过一个项目,其中充值部分包括 充值平台币开通VIP 他们都需要支持 微信 支付宝 支付。

以下是项目中此业务的控制层代码 (部分代码经过处理)

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
php复制代码<?php
namespace app\controller;

class OrderController
{
/**
* Route('GET', '/recharge/vip')
*/
public function rechargeVip()
{
$payType = $this->request->get('pay_type');
$vipRuleId = $this->request->get('rule_id');

$vipProductInfo = (new VipService())->getVipProduct($vipRuleId);

if( $payType == PayType::WECHAT) {
$payHandler = new WechatPay();
$result = $payHandler->doPay(...);
return $this->responseSuccess($result);
} else if ($payType === PayType::ALIPAY ) {
$payHandler = new AliPay();
$payHandler->doPay(....)
return $this->responseSuccess($result);
}
...
return $this->responseFail('支付方式不存在');

}

/**
* Route('POST', '/recharge/coin')
*/
public function rechargeCoin()
{
$payType = $this->request->get('pay_type');
$coinRuleId = $this->request->get('coin_rule_id');

$coinProductInfo = (new CoinService())->getCoinProduct($coinTuleId);

// 和 rechargeVip 一样的重复代码
...
}
}
?>

我相信大家已经看出了这段代码存在重复并且不太符合 面向对象 的思想。

The goal of software architecture is to minimize the human resources required to build and maintain the required system.

译: 软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求

站在工程化的角度思考,就是我们该以何种方式去组织这一模块代码,让它 减少重复 并且符合 面向对象的思想。

通过对业务的一些抽象, 为业务定义一个规范,并且遵守它。 形成一个上下游的概念。

这样我们就可以服务好我们上游的两个C ClientController

作为 Client 我们可以只用一个 充值 的接口就能调用我们所有下游业务提供的充值服务

image.png
首先我们需要一个 RechargeInterface.php 让充值业务都按照系统的标准去实现逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
php复制代码<?php
namespace app\contract;

interface RechargeInterface
{ /* 充值方法 */
public function recharge(int $userId, string $ruleId, int $payType);
/* 完成充值后 */
public function whenFinishRecharge(NotifyResponse $response);
/* 获取充值Title */
public function getTitle(): string;
/* 获取充值完成通知的URL */
public function getNotifyUrl(): string
}
?>

接着我们根据业务建立两个类,分别是 VipRecharge.phpCoinRecharge.php

  • VIP充值实现类 VipRecharge.php
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
php复制代码<?php
namespace app\service\recharge;

class VipRecharge implements RechargeInterface
{
/**
* 充值规则的实体
* @var RechargeRuleModel
*/
protected $vipRechargeRule;

protected $vipRechargeLog;

public function recharge(int $userId, string $ruleId, int $payType)
{
$vipRuleProduct = $this->vipRuleModel->getVipRuleById($ruleId);
if($vipRuleProduct) {
return $this->vipRuleRechargeLog->create([
'rule_id' => $ruleId,
'price' => $vipRuleProduct->price,
'pay_type' => $payType,
'user_id' => $userId,
'vip_key' => $vipRuleProduct->vip_key
]);
}
// 抛出充值规则不存在异常
}
public function getTitle():string
{
// 返回充值Title
}
public function getNotifyUrl():
{
// 返回充值回调接口地址
}
public function whenFinishRecharge(NotifyRecharge $response)
{
// 处理充值完成后的内容,主要用于接收到回调后的处理
}
}
?>
  • 金币充值实现类 CoinRecharge.php
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
php复制代码<?php
namespace app\service\recharge;

class CoinRecharge implements RechargeInterface
{
protected $coinRechargeRule;

protected $coinRechargeLog;

public function recharge(int $userId, string $ruleId, int $payType)
{
$coinRuleProduct = $this->coinRechargeRule->getCoinRuleById($ruleId);
if($coinRuleProduct) {
return $this->coinRechargeLog->create([
'rule_id' => $ruleId,
'price' => $coinRuleProduct->price,
'coin' => $coinRuleProduct->coin,
'user_id' => $userId,
'pay_type' => $payType
])
}

// 抛出规则不存在异常
}

...
}
?>

最后我们需要一个充值服务对下游的业务进行管理。

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
php复制代码<?php
namespace app\service\recharge;

class RechargeService
{
/* 允许支付的方法 */
protected $allowPay = [
PayType::WECHAT,
PayType::ALIPAY
];

/* 充值服务提供者 */
protected $rechargeProviders = [
RechargeType::VIP_RECHARGE : VipRecharge::class,
RechargeType::COIN_RECHARGE: CoinRecharge::class
];

/**
* 充值接口
* @var RechargeInterface
*/
protected $rechargeHandle;

public function __construct(string $rechargeType)
{
$this->rechargeHandle = new $this->rechargeProviders[$rechargeType]();
}

public function payRecharge(int $payType, $payPrice, $payOptions)
{
// 调用支付服务(支付类型,支付价格, 支付信息)
// 返回调用支付结果
}

/**
* 充值业务入口
*/
public function doRecharge(int $userId, $ruleId, int $payType)
{
$recharge = $this->rechargeHandle->recharge($userId, $ruleId, $payType);

// 使用支付
$payOptions = [
'title' => $this->rechargeHandle->getTitle(),
'notify_url' => $this->rechargeHandle->getNotifyUrl(),
'order_num' => $recharge->order_num
]
return $this->payRecharge($payType, $recharge->price, $payOptions);
}
}
?>

抽象永远是软件工程领域中最难的命题,因为他没有规则没有标准,甚至没有对错,只分好坏,只分是否适合。

这里仅以个人实际工作经验总结出来对代码工程化的一些思考 。

如何更好的组织业务代码

Programs are meant to be read by humans and only incidentally for computers to execute
译: 代码始终是写给人看的,只是恰好能被计算机执行。

我相信,局部干净,核心逻辑简洁的代码一定是好的代码。

为什么提倡简洁? 对于我而言就是容易 单元测试 代码可控性高 试想一下一个业务逻辑里的代码冗余非常多的与该业务无关的代码,维护起来简直就是想开喷。

写到这一段时我首先需要感谢我的启蒙恩师 PIPO, 是他在我初入这行时为我提供了不少宝贵经验,其中第一课就给我指导了代码工程的重要性,所以后期除了注重框架技术的学习,也更加注重代码工程的质量。

如何组织一个业务,让他能最小化的达到快速验收的目的?

分离业务中的主线和支线

在业务代码中,每个业务的主要逻辑都是一条主线,我们在编写每个业务逻辑时,应该要突出主线,分离支线,这样按照我们日常的思维才更容易去理解代码,如果你的支线代码变多,那么就会有种喧宾夺主的感觉,让我们无法轻易了解业务的内容。

分离业务主次方面我们可以通过

  • 框架提供的中间件(Middleware)
  • 事件(Event)
  • AOP (面向切面去创建业务的连接点)

例如上面的充值服务 的充值方法中,我们就应该是 检查充值规则, 抽取支付内容发起支付 那么这个充值方法就应该只有简单的几行代码,而不应该在有诸如权限判断, 支付方式实例判断, 性能记录 等无关主线的代码。

分离业务代码和其他代码

业务代码通常是和业务逻辑相关的, 而诸如基础工具类代码, 日志记录代码, 这些应该和业务逻辑分开。

相同业务高内聚,不同业务低耦合

耦合是一种摩擦力, 太高的偶尔会使摩擦力变强,不易行走, 太低的摩擦力又无法正常行走, 所以根据你的业务控制耦合的高低也是做好业务代码组织的一种手段

就拿上面的充值模块 举例,充值服务是依赖于与支付服务的,因为我们完成充值规则的校验和支付参数装配后,我们需要调用充值支付。

这时候我们可以通过依赖注入的方式,将 支付服务 注入到 充值服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
php复制代码<?php

class RechargeService
{
/**
* @Inject
* @var payServiceInterface
*/
protected $payService;

/**
* 通过依赖来组合业务之间的关系
*/
public function payRecharge($payType, $payPrice, $payOptions)
{
reutrn $this->payService->pay($payType, $payPrice, $payOptions);
}

}
?>

这样做的好处是整个 RechargeService 我们可以轻易的做单元测试, 我们只需在单元测试的时候 Mock 支付服务类使其按要求返回结果即可。

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
php复制代码<?php
namespace ProjectTest\Services;

class RechargeServiceTest extends TestCase
{
protected $container;

protected $rechargeService;

public function setUp():void
{
$payService = Mockery::mock(PayService::class);
$payService->shoudReceive('pay')->andReturn([
'pay_type' => PayType::WECHAT,
'pay_data' => []
]);

$this->container = ApplicationContext();

// 替换RechargeService中依赖的PayService
$this->container->getDefinitionSource()->addDefinition(PayService::class , function use ($payService) {
// 我们之前Mock好的PayService
return $payService;
})

$this->rechargeService = new RechargeService();
}

public function testRechargeService()
{
$result = $this->reachargeService->doRecharge();
$this->assertIsArray($result);
}
}
?>

image.png

写在最后

一直没有时间好好输出一篇文章,之前一直考虑的问题是自己的表达能力和写作能力不到位,但是回过头来想想, 也只是输出自己的一些实际经验之谈, 算不上什么大作。

下一篇文章我会以这篇文章叙述的为基础,谈谈我是如何使用 PHP 来构建一个微服务。

PHP来谈微服务 总觉得有些格格不入, 主要是为了说明 PHP是世界上最好的语言

最好在写这篇文章的时候参考了

  • 阿里技术号关于重拾面向对象的文章
  • 腾讯技术关于业务代码实践的文章

两个业界标杆出品写的技术文章总是能让我从中学习到新的知识。

文章中的示例代码使用的是 Hyperf 框架,看了原作者做的Hyperf教程视频,受益良多。

一杯咖啡,洋洋洒洒写了上千字, 好久没这么舒畅!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%