美文网首页
项目重构计划

项目重构计划

作者: 月剪西风 | 来源:发表于2019-10-22 16:27 被阅读0次

    项目重构

    横向代码耦合——业务代码之间没有明显的模块边界,模块之间调用呈网状结构。
    纵向代码耦合——没有合理的纵向分层,很多基础功能代码里包含了大量的业务功能,业务代码与基础功能代码没有严格的逻辑界线。

    当前项目大致架构

    当前项目大致架构.png

    问题详述

    • 1.Activity和fragment与网络请求和数据处理业务耦合严重;
    • 2.项目中使用的列表控件大部分使用listView和gridView实现;
    • 3.数据与view之间关联含有大量的查找引用代码和赋值操作等;
    • 4.列表适配器未进行封装,每个adapter中代码量较大,不能集中于业务展示逻辑;
      例如:
      public class ProductListGeiAdapter extends ArrayAdapter {}
      项目中大部分适配器都直接继承自ArrayAdapter,类文件中充斥大部分与当前业务相关的逻辑控制方法;
    • 5.图片框架中的库方法、类调用遍布项目代码各处,第三方库对代码侵入性较强;
    • 6.缓存数据中采用afinal框架中finalDb进行操作,afinal体量较大,涵盖了http、bitmap请求等多种功能业务,在单纯实现数据库操作方面而引入大而全的库不值得,可以使用更轻量级的数据库操作框架,例如ORMLite的对象关系映射框架;
    • 7.Volley库网络请求框架,对于大文件的上传下载表现糟糕;对于httpclient库具有依赖性;
    • 8.各种功能类、工具类函数方法遍布各处,对于不依赖与当前上下文对象的方法定义没有统一的类存放;
    • 9.项目中存在大量未使用的类和方法;
    • 10.在重构过程中将对代码进行必要的清理,使用lint工具对资源文件进行清理等;
    • 11.函数方法重复定义严重,类的单一职责原则体现不佳,各种功能函数涵盖其中;
    MVC结构.png

    Activity和Fragment同时承担了Controller和View的职责,导致他们变得及其臃肿且难以维护;
    由于Controller和View的揉合,导致单元测试起来很困难;
    回调嵌套太多,面对复杂业务时的代码逻辑不清晰,难以理解且不利于后期维护;
    各层次模块之间职责不清晰等。

    项目重构

    目标

    稳定,层次分明,功能模块耦合性降低,可读性强,拓展性强,代码清晰,提高编译效率;提高代码复用,促使UI独立,业务独立,数据独立,可测试

    原则:

    渐进式重构 ,iOS / Android 互相参考 ,理清业务再重构;

    MVP的设计模式与DataBinding

    整体结构.png

    整体架构

    基于Android官方databinding实现方案,结合mvp的开发模式,应用mvvm架构实现数据与界面之间的关联;处理好复杂业务与数据、网络等各个模块的解耦,简化代码;,业务逻辑可以在不依赖UI,数据库,网络服务等其它外部因素的情况下进行测试,在不变动系统其它部分的情况下,可以很方便的改变UI;

    预期view与数据.png

    View和Model之间通过Android Data Binding技术,实现视图和数据的双向绑定;ViewModel持有Model的引用,通过Model的方法请求数据;获取数据后,通过Callback(回调)的方式回到ViewModel中,由于ViewModel与View的双向绑定,使得界面得以实时更新。同时,界面输入的数据变化时,由于双向绑定技术,ViewModel中的数据得以实时更新,提高了数据采集的效率。

    采用ViewModel解决MVP中View(Activity)和Presenter相互持有对方引用的问题,界面由数据进行驱动,响应界面操作无需由View(Activity)传递,数据的变化也无需Presenter调用View(Activity)实现,使得数据传递的过程更加简洁,高效,项目中的viewmodel大致可以理解为项目中presenter。

    View(视图层)采用XML文件进行界面的描述,使用activity和fragment进行展示;
    Model(模型层)通过网络和本地数据库获取视图层所需数据,采用内存缓存,文件缓存和数据缓存等多级别缓存组成的数据获取逻辑;
    ViewModel(视图-模型层)databinding负责View和Model之间的通信,以此分离视图和数据。

    安居客架构.png

    整体架构参考安居客架构演进之路,界面层使用大量fragment构成依赖于activity管理,使用presetner关联于数据层,使用接口定义的协议减轻对实际业务的依赖和数据获取方式的固定模式,方便后期的需求变更与界面的重用;
    使用eventbus作为事件总线,连接各个模块和功能界面之间的联系,实现解耦,基于rxjava的响应式编程,处理接口嵌套回调的问题;
    基础数据服务通过统一的数据管理者提供view层;

    大致三层结构.png

    三层架构中:
    基础组件层,相当于我们依赖的一些第三方框架,例如项目中
    retrofit网络请求,okhttp,gson解析组件,数据操作组件等;
    业务组件层相当于项目中友盟分享,连连支付等,依赖第三方业务模块,相对比较独立的部分,后期将在项目建立这种类型的业务组件以module的形式作为依赖模块即可;
    业务层相当于项目中实际的业务逻辑功能和界面等的整合;

    View Layer: 只负责UI的绘制呈现,包含Fragment和一些自定义的UI组件,View层需要实现ViewInterface接口。Activity在项目中不再负责View的职责,仅仅是一个全局的控制者,负责创建View和Presenter的实例;
    Model Layer: 负责检索、存储、操作数据,包括来自网络、数据库、磁盘文件和SharedPreferences的数据;
    Presenter Layer: 作为View Layer和Module Layer的之间的纽带,它从model层中获取数据,然后调用View的接口去控制View;
    Contract: 参照Google的demo加入契约类Contract来统一管理View和Presenter的接口,使得某一功能模块的接口能更加直观的呈现出来,这样做是有利于后期维护的。

    项目分层依赖关系.png

    业务隔离

    业务之间存在两种耦合关系:
    页面解耦 - 页面之间跳转使用跳转协议
    Android实现方式:

        1.在 AndroidManifest.xml 文件中定义 URI
        <activity
        android:name=".ui.BbbActivity"
        <intent-filter>
            <category android:name="android.intent.category.DEFAULT" />
            <action android:name="android.intent.action.VIEW" />
            <data
                android:host="bbb"
                android:path="/home"
                android:scheme="wsc" />
        </intent-filter>
        </activity>
        2.封装跳转 Intent
        
        final Uri uri = new Uri.Builder().authority("wsc").path("home/bbb")
            .appendQueryParameter("message", new Gson().toJson(messageModel)).build();
        final Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(uri);
        startActivity(intent);
        3.代码进一步封装
        
        ZanURLRouter.from(getContext())
            .withAction(Intent.ACTION_VIEW)
            .withUri("wsc://home/bbb")
            .withParcelableExtra("message", messageModel)
            .navigate();
    

    注意:两端协议要保持一致
    需要通过工程手段保证页面 URI 唯一

    缓存数据设计

    项目中上层模块仅仅通过特定的数据访问接口,获取展示数据,数据仓库负责提供功能界面的所需的有效数据,并进行数据校验等;


    数据缓存.png

    项目中的提供给ui界面的数据,大量数据通过数据库或者文件方式存储,功能界面申请数据时,通过统一的数据提供接口,优先从内存中获取数据,内存中数据不存在则从数据库或者文件等获取,最后才从网络请求数据,这其中需要进行数据校验,判断是否失效等;从内存或者数据库中获取的数据,若经过校验通过则避免网络请求,若校验失败,则从网络请求数据,并更新缓存内容;

    项目避免使用数据库操作的注解方式,用实体方式直接保存对象到数据,使用内容提供者作为数据存储的方式,提高app性能,避免采用大量反射和注解;

    缓存使用方式.png

    业务层向数据层请求数据;
    数据层检查缓存中有没有请求需要的数据;
    如果有缓存数据,则直接返回缓存数据;
    如果没有缓存数据,则从网络API获取数据,并将数据加入缓存,然后返回数据。

    调用网络API时,判断网络状态,根据不同状态做不同处理。如果网络不可用,就无需发起请求了。网络可用时,区分是连接WIFI还是连接移动网络。连接移动网络时,限制调用比较耗流量的请求。连接WIFI时,则无需设置这种限制,还可以预先请求一些接口,比如请求当前分页数据时,可以将下一页的数据也预先请求。

    缓存策略

    不同的接口做不同的缓存处理。首先,缓存只适用于获取数据的接口,对于修改数据的接口则不适用。其次,不同接口缓存时间一般也不同,对于很少变动的数据缓存时间可以设置长一些,而频繁变动的数据缓存时间则比较短,甚至不进行缓存。最后,缓存数据因为比较多,我们一般保存在数据库,而对于调用频率高、最新的数据,还会在内存中也拥有一份缓存,不过缓存时间比较短。请求缓存数据时,会先检查内存缓存中有没有,有则直接将缓存的数据返回,没有才从数据库或者文件中获取。

    当与外部交互的时候,要符合面向接口编程的原则,只要提供开放的数据接口就可以。对于接口的参数需要说明一下,上面提到的参数有appKey、version、currentPage,还有签名sign、时间戳time,其实可以分为两类:系统参数和业务参数。像appKey、version、sign、time这些属于系统参数,而currentPage,或username之类的则属于业务参数。数据层开放的数据接口的参数只需要包含业务参数就可以了,业务层并不需要关心系统参数是什么,系统参数在数据层内部封装API时指定就可以了。

    服务端控制缓存

    通过服务端的“Cache-Control”和“max-age”来告诉客户端有没有缓存以及缓存的时间,推荐的使用方式,需要服务端配合,比较灵活。

    客户端直接控制缓存

    请求之后把数据缓存在本地,直接以文件缓存在本地,当然也可以缓存在本地的sqlite,以sqlite文件的形式缓存数据处理更灵活点,然后客户端自己处理缓存的时间,过期则直接清除数据;对于一些不太经常变化的页面,采用这种缓存可以减少客户端流量,同时减少服务器并发量;

    缓存使用场景
    在网络状态不佳的情况下,可以在实时性要求不高的情况下,优先使用缓存数据,展示界面,在实时性要求比较高的界面,在加载缓存之后,立即请求服务端数据,更新数据,保证在用户看到真实数据之前,不是显示一个简单的加载进度条或者空白页面。
    另外考虑随着时间的流逝,缓存数据会越来越多,需要增加删除过期缓存的功能,设置一个阀值,在保存缓存的时候,判断当前缓存的总量是否大于阀值,如果是则删除时间较早的缓存

    项目中可以使用simpleCache作为第三方框架进行缓存操作,可以实现普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java对象,和 byte数据的缓存操作;

    一、缓存管理的方法
    缓存管理的原理:通过时间的设置来判断是否读取缓存还是重新下载,数据库法(可以考虑使用contentprovider)和文件法。

    二、数据库法缓存管理
    在下载完数据文件后,把文件的相关信息如url,路经,下载时间,过期时间等存放到数据库,下次下载的时候根据url先从数据库中查询,如果查询到当前时间并未过期,就根据路径读取本地文件,从而实现缓存的效果。
    三、文件法缓存管理
    使用File.lastModified()方法得到文件的最后修改时间,与当前时间判断是否过期,从而实现缓存效果。操作上倒是简单,比较时间即可。本身处理也不容易带来其它问题,代价低廉。

    四、说明

    1. 不同类型的文件的缓存时间不一样。
      不变文件的缓存时间是永久,变化文件的缓存时间是最大忍受不变时间。
      图片文件内容是不变的,直到清理,我们是可以永远读取缓存的。
      配置文件内容是可能更新的,需要设置一个可接受的缓存时间。
    2. 不同环境下的缓存时间标准不一样。
      无网络环境下,我们只能读取缓存文件,哪怕缓存早就过期。
      WiFi网络环境下,缓存时间可以设置短一点,一是网速较快,而是流量不要钱。
      移动数据流量环境下,缓存时间可以设置长一点,节省流量,就是节省金钱,而且用户体验也更好。

    组件化

    基于可重用的目的,将一个大的软件系统拆分成一个个独立组件。
    组件化的带来的好处不言而喻:
    避免重复造轮子,节省开发维护成本;
    降低项目复杂性,提升开发效率;
    多个团队公用同一个组件,在一定层度上确保了技术方案的统一性。
    项目中涉及到很多第三方业务逻辑框架,例如友盟分享,将对其进行与app主业务进行拆分,使用module形式组织,达到重用目标;

    模块化

    问题:全量编译时间太长,模块间耦合严重,不利于并行开发测试

    业务模块间解耦
    单个业务模块单独编译打包,加快编译速度
    并行开发、测试
    通过业务模块注册机制的方式来达到解耦合的目的。每个业务模块对外提供相应的业务接口,同时在系统启动的时候向系统注册自己模块的Scheme, 当其他业务模块对该业务模块有依赖时,从系统中获取相关实例,并调用相应接口来实现调用,从而实现了业务模块之间的解耦目的。
    项目中将对实际业务情况,进行拆分处理,尝试是否需要进行模块化分离方案;

    三层架构

    整个项目分为三层,从下往上分别是:
    Basic Component Layer: 基础组件层,顾名思义就是一些基础组件,包含了各种开源库以及和业务无关的各种自研工具库;
    Business Component Layer: 业务组件层,这一层的所有组件都是业务相关的,例如上图中的支付组件AnjukePay、数据模拟组件DataSimulator等等;
    Business Module Layer: 业务module层,在Android Studio中每块业务对应一个单独的module。每个单独的Business Module都必须准遵守前面提到的MVP架构。
    对于Business Module Layer,各业务模块之间的通讯跳转可以采用路由框架Router来实现;
    对于Business Component Layer,单一业务组件只能对应某一项具体的业务,对于有个性化需求的对外部提供接口让调用方定制;
    合理控制各组件和各业务模块的拆分粒度,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于CommonBusuness的组件中,在后期不断的重构迭代中视情况进行进一步的拆分;
    上层的公有的业务或者功能模块可以逐步下放到下层,合理把握好度就好;
    各Layer间严禁反向依赖,横向依赖关系由各业务Leader和技术小组商讨决定。

    热补丁修复

    使用阿里百川的热修复方案,在线上出现问题的时候,能够在不发版本的情况下,及时解决问题;

    移动App性能监控系统

    移动应用崩溃、移动应用错误、移动应用请求响应时间、移动应用交互性能、运营商网络响应时间来帮助我们分析问题。

    异常处理跟踪

    腾讯bugly,或者友盟等第三方的框架跟踪

    相关文章

      网友评论

          本文标题:项目重构计划

          本文链接:https://www.haomeiwen.com/subject/wedzzttx.html