最近在一次App版本接口迭代时,把一个维护了两年的旧项目接口进行了重构,下面简单用这一篇文章交代一下重构的过程:
为什么要进行重构
首先谈为什么要进行重构吧,毕竟已经维护了两年了,大大小小也经历了很多次迭代开发,为何这次会进行重构呢?
这要先交代一下我们这个接口项目是咋回事。
一般一个App项目如果业务比较复杂的话,可能会将接口划分到多个项目中,当然,我们的App也是这样,而我重构的这个项目,也是比较新的一个模块的接口,由于刚开始比较简单,所以没有选择现在成熟的框架,而是用PHP自己了写了一个简单的MVC框架,其实项目的目录结构大体如下:
controlers #控制器层
models #模型类层
common #公用函数
config #配置文件目录
libs #基础类库
ext #第三扩展
index.php #入口文件
复制代码从代码的项目结构看得出来,最主要的是控制器层和模型层:
控制器层(controllers)
负责接收请求参数,并且也堆积很多业务逻辑,同时调用模型类完成业务逻辑,返回数据,在controllers目录下,由于要为不同的App版本提供接口,所以子目录格式为v1.x.x,以些来区别不同版本的接口。
controllers
v1.0.0
v1.0.1
...
v5.0.0
复制代码模型层(models):
业务逻辑层,也包含对数据进行CURD,调用第三方,参数校验等功能,几乎所有业务功能都在这里,不同版本的模型层相互调用,所以这一层也是功能最混乱的层,因此也是最需要重构的分层,models目录的版本划分与controllers类似,不同版本的model类有大量重复的代码。
models
v1.0.0
v1.0.1
...
v5.0.0
复制代码通过上面的介绍,其实你会发现,这个是一个简单到不能再简单的项目,但再简单的项目,随着业务的推进以及多个迭代开发,代码的代码极其混乱,总结起来大概是以下几个问题:
没有良好的目录分层,大体上只是简单分了控制器层和模型层,所有的业务逻辑都堆积在控制器层和模型层,完全没有任何扩展性可言。
很多配置都直接写死在代码里,虽然项目中有专门存放配置的目录,但还是有很多的配置直接写死在代码,比如连接redis的代码。
没有编码规范,无论变量名还是常量或是类名,命名都很随意。
代码里还充斥着大量的魔法数值,如何有新人接手,会完全搞不清楚到底这些数值到底什么意思。
所有的代码都没有输出日志,出现BUG时,很难定位问题。
业务代码直接堆积在控制层和模型层,没有抽离公共代码,新增一个版本接口时,需要完全复制上个版本的代码。
完全没有使用PHP的命名空间,不能很好划分不同的类。
简而言之,这是一个项目结构极其简单,但由于经过太多人维护而代码变得极其混乱且没有规范的应用,虽然我接手后一直有重构的项目,奈何之前只是个别接口的迭代,而在最近一次App迭代时,几乎所有接口都是新的版本,因此重构是不可避免的,哪怕时间不够,为了之后的版本。
重构过程
上面列出项目的几个问题,其实重构的过程,就是把上面提出问题的优化吧。
重构要达到的目的
重新划分项目结构,使项目结构层级更加清晰。
进行版本迭代开发时,不需要复制一份同样的代码,提升开发效率。
增加代码的可维护性。
重构的原则
单一职责原则:每个类的功能要单一,每个方法的作用要单一,避免代码臃肿。
变量命名规范
避免在代码中直接写魔法数值
迭代接口,新增一个接口版本时,不需要完全复制上一个版本接口的代码。
重新划分的项目结构
controllers # 控制器层,只能将参数传递给services层。
v1_0_0
IndexController.php
v1_0_1
......
v5_0_0
services #具体业务逻辑层,可以调用manager层或models来实现业务逻辑
v1_0_0
v1_0_1
......
v5_0_0
handlers # 扩展层,对services的补充
models # 模型,针对数据表的CURD代码
entity #实体类
managers #业务封装层
libs #类库
ext #第三方类库
common #公用函数
index.php #入口文件
复制代码下面的项目的目录结构的截图:
一个controller类的示例:
namespace app\controllers\v1_0_0;
use app\services\v1_0_0\IndexService;
class IndexController {
private $service = null;
public function __construct(){
$this->service = new IndexService();
}
public function actionIndex(){
//接收参数
....
调用IndexService的业务方法,并返回值给客户端
return $this->service->index();
}
}
一个service类型的示例:
#不同版本的service之间通用代码抽取到Service类,作为基类被继承
namespace app\services;
class IndexService{
public function __construct(){
}
//业务方法
public function index($page = 1){
//具体业务逻辑
$list = $this->getArticle();
return [
'list' => $list,
'total' => 100
];
}
protected function getArticle(){
}
}
在重构过程中,之所以设计如上所示的目录结构,一个重要的考虑点就是如果在新增加一个版本接口时,最大限度地复用上一个版本的逻辑,这里的新增接口的意思是现在首页接口的版本为v1.0.0,但由于版本迭代,会把接口升级为v1.0.1,也就是接口升级。
一个接口的升级,无非两个原因:
接口的业务处理逻辑发生改变,因此需要升级接口。
接口的数据结构发生改变,比如新增数据或数据类型发生改变,因此需要升级接口。
比如说,index接口从v1.0.0升级v1.0.1时,getArticle()方法数据结构或者业务逻辑发生改变,这时候继承父类接口,并覆盖getArticle()方法即可。
namespace app\services\v1_0_1;
use app\services\IndexService IndexBaseService;
class IndexService extends IndexBaseService{
public function __construct(){
}
//重写覆盖父类逻辑
pulic function getArticle(){
}
}
但这时候,你会发现在getArticle()方法重写的逻辑,只在v1.0.1这个版本中,如果这个重写的逻辑在后续版本也是一样的,那不是每个版本都要重写?
这时候,可以将这段逻辑抽取出来,给每个需要的版本复用,如果某个有单独的处理逻辑,可以使用的的覆盖重写的方法,而抽取出来的逻辑,放在handlers目录结果中,handlers目录是对services中需要重写覆盖并会在多个版本复用逻辑的抽取层。
所以我们把getArticle()方法抽取出来,如下所示:
namesapce app\handlers;
trait getArticle{
public function getArticle(){
}
}
这时候v3.0.1的接口,加载上面handlers的方法,完全逻辑复用。
namespace app\services\v3_0_1;
use app\services\IndexService IndexBaseService;
use app\handlers\getArticle;
class IndexService extends IndexBaseService{
use getArticle;
public function __construct(){
}
}
重构的结果
老实说,项目开发时间不够,而我个人能力也有限,因此我只能在本次的开发中,尽自己的能力去完善整个项目架构,很多不完善的地方,只能之后的开发中优化了。
重构后的优点
相比原先全部业务堆积在models层,划分后的架构,每个层级只负责自己的事情,因此逻辑比较清晰,代码可维护性强,每个类或方法的职责单一,降低了开发难度。
重构后的缺点
当然,原来的代码非常简单,就是controllers层直接调用models层,或者有时候,所有的业务逻辑直接写在controller层,重构后,代码的复杂性也会相应增加。
为什么是重构而不是重新开发一个新项目呢?
可能很多人会问,这么简单的项目,直接重做不就好了吗?其实是这样,除了上面重构中的重新分层,项目还有很多公用的代码和模块,如果重新开发一个项目的话,那么这些基础的也需要重新开始,而旧项目也需要继续维护,更加增加开发和维护的成本,因此重构是比较合适的选择。
小结
老实说,我重构的这个接口项目一点也不复杂,最主要的代码还是CURD,并没有非常复杂的业务逻辑,但由于从一开始就没有进行严格的代码架构分层,在开始过程也没有一致的代码规范,导致经过两三年业务的发展与代码迭代开发,造成了代码混乱和重复业务逻辑堆积。
所以,对于任何项目来说,良好的代码分层和开发规范,是保证项目可维护性的根本。
最后
想获取面试资料 在评论下方回复面试资料哦!
作者:张君鸿
出处链接:https://juejin.im/post/5e021a04e51d45582b2a427b