iOS DevelopmentiOS之框架架构程序员

iOS重构实践

2018-02-07  本文已影响747人  文兴

最近完成了我司iOS项目的重构,把整体的代码架构都梳理了一遍,主要按照MVP的架构模式,并综合考虑了重构的难度和效果。在这个过程中也积累了一些代码重构方面的经验,在这里总结一下。

项目简介和MVP模式重构

项目简介

首先简单介绍一下项目情况。我们原有项目的架构是比较标准的MVC模式,也是苹果官方推荐的架构模式。Model层用来表示实体类,View层负责界面展示和传递UI事件,Controller层负责大部分的业务逻辑。除此之外,对一部分公共的可复用的逻辑,我们抽象出Service层,提供给Controller使用,另外网络层也独立出来。下图比较清楚地展示了整体架构


整体架构

MVC模式的问题

MVC架构作为苹果官方推荐的架构模式,把数据Model和展现View通过Controller层隔离开,在项目规模较小的时候是一个不错的选择。随着项目复杂性的提高,我们也渐渐感觉到MVC模式的弊端,主要体现在下面几个方面

MVP模式的重构

对于Controller层过于臃肿的问题,MVP模式则能较好地解决这个问题——既然UIViewControllerUIView是耦合的,索性把这两者都归为View层,业务逻辑则独立存在于Presenter层,Model层保持不变。下图比较清除得展示了MVP模式的结构

MVP模式简介
我们来看一下MVP模式能否解决MVC模式存在的问题

MVC模式改进——Router模式

这里主要是考虑界面间跳转的代码如何重构,这一点我在之前的文章里已经有提到了,这里给个链接iOS重构之面向协议编程实践,另外附图一张

Router模式

实例分析

前面我们提到,MVP模式虽然能解决许多MVC模式下存在的问题,但对于比较复杂的需求,还是会存在逻辑过于复杂,Presenter层也出现难以维护的问题。下面我们就通过一个实际的例子,来看看面对复杂的业务逻辑,我们应该如何去设计和实现。

很多复杂的需求,在最初都是从一个简单的场景,一步步往上增加功能。在这个过程中,如果不持续的进行优化和重构,到最后就成了所谓的"只有上帝能看懂的代码"。说了这么多,进入正题,来看这个需求。

V1.0 单文件上传

实现一个简单的单文件上传,文件的索引存储在数据库中,文件存储在App的沙箱里面。这个应该对于有经验的客户端开发者来说是小菜一碟,比较简单也容易实现。我们可以把这个需求大致拆分成以下几个子需求

  1. 初始化上传View
  2. 更新上传View
  3. 点击上传按钮事件
  4. 数据库中获取上传模型
  5. 发起HTTP请求上传文件
  6. 检查网络状态

以上几项如果使用传统的MVC模式,实现起来如下图所示

MVC
我们可以看到上述需求基本都直接在UploadViewController中实现,目前需求还是比较简单的情形下面,还是勉强能够接受,也不需要更多的思考。如果使用MVP的模式进行优化,如下图所示 MVP.png

现在UploadPresenter负责处理上传逻辑了,而UploadViewController专注于UI更新和事件传递,整体的结构更加清晰,以后维护代码也会比较方便。

V2.0 多文件上传

需求来了!需要在原来的基础上支持多文件上传,意味着我们多了一个子需求

  1. 维护上传模型队列

很显然,我们需要在UploadPresenter中增加一个维护上传队列的功能,最初我也确实是这样实现的,但是由于文件上传需要监听的事件比较多,回调也比较频繁,直接在Presenter中继续写这样的逻辑代码,已经成倍增加了代码的复杂性。

所以经过一番思考,我考虑把文件上传这部分的逻辑单独提取出一层FileUploader,而UploadPresenter只负责维护FileUploader的队列以及检查网络状态。具体的实现如下所示。

MVP2.png
我们可以看到,分层之后的结构又更加清晰了,每一层的职责都比较单一,目前看起来一切OK!

V3.0 多来源上传

原来我们的上传文件的来源是存在于App沙箱里的,我们通过数据库查询可以找到这个文件的索引和路径,进而获取到这个文件进行上传。现在万恶的需求又来了,需要支持上传系统相册中获取的图片/视频。

  1. 支持系统相册和App沙箱中获取文件

到这里可能有些读者已经有点头大了,如果没有仔细思考,很可能从这里就走向了代码质量崩溃的道路。

这个时候,我们就要思考,他们是多来源,但是对于FileUploader来说,它其实不关心模型的来源,它只需要获取到模型的二进制流。于是,我们可以抽象出一个BaseModel,提供一个stream只读属性,两种来源分别继承BaseModel,各自重载stream只读属性,实现自己的构造文件stream的方法。对于FileUploader来说,它只持有BaseModel即可,这就是继承和多态的一个典型的使用场景

如果后续还有更多来源的文件,比如网络文件(先下载再上传?),也只需要继续继承BaseModel,重载stream即可,对于FileUploader和它的所有上层来说,一切都是透明的,无需进行修改。经过这样的设计,我们的代码的可维护性和可扩展性又好了。下面是架构图。

MVP3.png

V4.0 多方式上传

在HTTP文件上传中,我们可以直接上传文件的二进制流,这种就需要服务端做特定的支持。但更为常用和支持广泛的做法是使用HTTP表单文件传输,即组装HTTP请求的body时采用multipart/form-data的标准组装,传输数据。于是,我们又多了一个需求:

  1. 支持表单传输和流传输

思路和刚才的多来源上传差不多,我们把上面的两种来源的模型,即FSBaseMABaseM抽象为父类,父类含有各自的文件二进制数据的抽象,子类分别实现二进制直接组装流,和按multipart/form-data格式组装流,实现如下图。

MVP4.png

V5.0 支持FTP/Socket上传

刚才我们的文件上传,底层的协议是基于Http,此时我们需要支持FTP/Socket协议的传输,应该怎么办?

  1. 支持HTTP/FTP/Socket

经过上面的思考,相信你一定知道该怎么做了。这里留个思考,答案请戳这里MVP_V5架构

对比

最后,我们把目前的需求全都整理一下

  1. 初始化上传View
  2. 更新上传View
  3. 点击上传按钮事件
  4. 数据库中获取上传模型
  5. 发起HTTP请求上传文件
  6. 检查网络状态
  7. 维护上传模型队列
  8. 支持系统相册和App沙箱中获取文件
  9. 支持表单传输和流传输
  10. 支持HTTP/FTP/Socket

我们看看,如果分别采用MVC、MVP_V1、MVP_V2、MVP_V3、MVP_V4、MVP_V5,来实现目前的十个需求,我们的代码大致会分布在哪几层。


优化后的架构模式之间的比较

孰优孰劣一目了然。如果采用最原始的MVC模式的话,保守估计ViewController代码量至少3K行以上。

总结

代码优化和重构的技巧

在这次的项目重构中,我也总结了一些重构方面的技巧和贴士,希望能帮助到想开始进行代码重构的同学

事不过三

class Fruit {
    
    var name:String = ""
    
    func getColor() -> UIColor? {
        if name == "apple" {
            return UIColor.red
        } else if name == "banana" {
            return UIColor.yellow
        } else if name == "orange" {
            return UIColor.orange
        }
        return nil
    }

    func getPrice() -> Float? {
        if name == "apple" {
            return 10
        } else if name == "banana" {
            return 20
        } else if name == "orange" {
            return 30
        }
        return nil
    }
    
    func getType() -> String? {
        if name == "apple" {
            return "红富士"
        } else if name == "banana" {
            return "芭蕉"
        } else if name == "orange" {
            return "皇帝"
        }
        return nil
    }
}

这里的对名称name的相同的if/else判断出现了三次,如果此时我们多了一种水果梨,我们得修改上述所有的if/else判断,这样就会非常难维护。

这种场景我们可以考虑抽象出一个Fruit的抽象类/接口/协议,通过实现水果类/接口/协议的方式,此时如果多了一种水果,让这种水果继续实现Fruit协议就行,这样我们就通过新增的方式替代修改,提高了代码的可维护性。


protocol Fruit {
    func getPrice() -> Float?
    func getType() -> String?
    func getColor() -> UIColor?
    var name:String { get }
}

class Apple:Fruit {
    var name:String = "apple"
    func getColor() -> UIColor? {
        return UIColor.red
    }
    
    func getPrice() -> Float? {
        return 10
    }
    
    func getType() -> String? {
        return "红富士"
    }
}

class Banana:Fruit {
    var name:String = "banana"
    func getColor() -> UIColor? {
        return UIColor.yellow
    }
    
    func getPrice() -> Float? {
        return 20
    }
    
    func getType() -> String? {
        return "芭蕉"
    }
}

class Orange:Fruit {
    var name:String = "orange"
    func getColor() -> UIColor? {
        return UIColor.orange
    }
    
    func getPrice() -> Float? {
        return 30
    }
    
    func getType() -> String? {
        return "皇帝柑"
    }
}

合理分层

避免过度设计

重构的时机和对象

总结

最后我想谈谈设计模式。其实重构的过程其实也就是灵活运用设计模式对代码进行优化和改进。很多人设计模式也看了很多,学习了很多,但真正在工作中能合理使用的却很少。所以关键还在灵活运用四个字上,能做到这一点,你的水平就会上一个台阶。

所以在平时的工作中,我们要有对代码的Taste,知道什么样的是好代码,什么样的是脏代码,尽早发现可优化可改进的地方,持续产出高质量代码,而不是实现功能就万事大吉,否则迟早要为你以前偷的懒买单。 以上就是我在我司项目重构过程中的的一些总结和分享,水平有限,希望对大家有所帮助。

上一篇 下一篇

猜你喜欢

热点阅读