阅读 2098

明明开发时间很赶,我为什么还要重构整个项目

最近在一次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,并没有非常复杂的业务逻辑,但由于从一开始就没有进行严格的代码架构分层,在开始过程也没有一致的代码规范,导致经过两三年业务的发展与代码迭代开发,造成了代码混乱和重复业务逻辑堆积。

所以,对于任何项目来说,良好的代码分层和开发规范,是保证项目可维护性的根本。


如果你觉得文章不错,欢迎扫码关注,你的关注就是我写作的最大动力