架构

作者: 飞哥漂流记 | 来源:发表于2020-09-16 17:25 被阅读0次

    设计一个App的思路:

    原则:易读 易维护 易扩展  ;技术储备;语言选择

    组成:

    1.应用入口(Appdelegate):存放推送 IM 支付回调等

    2.功能模块:根据业务进行划分:可以灵活采用MVC MVVM MVP

    3.管理模块:登陆状态信息 单例 网络监听 广告页

    4.工具类:自己写的工具类

    5.基类:一些定制化的内容页面 样式 空数据页面 无网络提示页面

    6.分类:对系统类 自定义类增加的类别

    7.宏定义文件:全局通用的宏定义 方法

    8.资源文件:图片 json xml test plist

    9.第三方库的封装:

    10.Cocoapods:


    重构需要考虑的因素:

    1.明确重构的目的和重用性

    2.定义重构完成的界限

    3.持续渐进式重构

    4.确定当前的架构状态

    5.不能忽略数据的重用性

    6.管理好技术债务

    7.远离虚华的东西 追求实际

    8.做好准备面对压力,做好面对非技术的准备

    9.了解当前业务

    10.时刻注意代码的质量

    11.团队一致 做好准备


    MVC:https://www.jianshu.com/p/eedbc820d40a

    MVC是软件工程中的一种软件架构模式,它把软件系统分为三个基本的部分:模型Model、视图View以及控制器Controller;

    数据Model: 负责封装数据、存储和处理数据运算等工作

    视图View: 负责数据展示、监听用户触摸等工作

    控制器Controller: 负责业务逻辑、事件响应、数据加工等工作

    在iOS中,M和V之间禁止通信,必须由C控制器层来协调M和V之间的变化,C对M和V的访问是不受限的

    Controller 可以直接与 Model 对话(读写调用 Model),Model 通过 Notification 和 KVO 机制与 Controller 间接通信

    Controller 可以直接与 View 对话,通过 outlet,直接操作 View,outlet 直接对应到 View 中的控件,View 通过 action 向 Controller 报告事件的发生(如用户 Touch 我了)。Controller 是 View 的直接数据源(数据很可能是 Controller 从 Model 中取得并经过加工了)。Controller 是 View 的代理(delegate),以同步 View 与 Controller

    MVC的缺点在于并没有区分业务逻辑和业务展示, 这对单元测试很不友好


    MVP:

    MVP模式是MVC模式的一个演化版本(好像所有的模式都是出自于MVC~~),MVP全称Model-View-Presenter;

    MVP的 V 层是由UIViewController 和UIView 共同组成;

    Model:与MVC中的model没有太大的区别。主要提供数据的存储功能,一般都是用来封装网络获取的json数据的集合

    Presenter:作为model和view的中间人,从model层获取数据之后传给view,使得View和model没有耦合

    view 将委托presenter 对它自己的操作,(简单来说就是presenter发命令来控制view的交互,要你隐藏就隐藏,叫你show 你就乖乖的show)

    presenter拥有对 view交互的逻辑(就是上面说的意思)

    presenter跟model层通信,"Present"一方面通过Service层调用接口获取数据给Model层,并将数据转化成对适应UI的数据并更新view

    presenter不需要依赖UIKit

    view层是单一,因为它是被动接受命令,没有主动能力

    presenter 作为业务逻辑的处理者,首先要向Service层拿数据赋值给model,所以它将可以向model层通信。其次,UI的处理权移交给了它,所以它需要与view成通讯,发送命令更新UI。同时,UI的响应将触发业务逻辑的处理,所以view 层向presenter层通讯,告诉他用户做了什么操作,需要你反馈对应的数据来更新UI。这样就完成了从用户交互获得交互反馈到整个业务逻辑。

    关于C端和P端的循环引用的问题, 直接用weak关键字就可以解决了

    总得来说MVP的好处就是解除view与model的耦合,使得view或model有更强的复用性

    只需要初始化P层, 然后调P层的接口就可以了. 至于P层内部的逻辑, 我不需要知道

    V层也只专注于视图的创建

    M层只专注于模型的构建(字典->模型)

    优点:

    对Controller进行瘦身,View和Model之间不存在耦合,同时也将业务逻辑从View中抽离,复用性更好

    缺点:

    由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了


    MVVM:

    MVVM

    在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件

    view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)

    viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方

    使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性

    MVVM模式将Presenter改名为ViewModel,基本上与MVP模式完全一致。

    唯一的区别是,它采用双向绑定(data-binding) : View<->ViewModel, ViewModel作为Model中值的映射,是数据发生改变时,通知View中发生改变,以后不需要考虑View和Model之间的交互更新,只需着手界面布局逻辑即可。

    ①View和Model 不直接关联,而是通过ViewModel作为枢纽,沟通View和Model之间的关系。

    ②View中控件的值与属性进行绑定,通过KVO键值观察(这样当model的值发生变化时,View会自动发生改变) 

    View和Model通过ViewModel实现动态关联

    MVVM 的注意事项

    view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)

    viewModel 引用model,但反过来不行

    MVVM 的优势:

    低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上

    可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑

    独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计

    可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试

    MVVM 的弊端:

    数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了

    对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)

    绑定是一种响应式的通信方式。当被绑定对象某个值的变化时,绑定对象会自动感知,无需被绑定对象主动通知绑定对象。可以使用KVO和RAC实现。例如在Label中显示倒计时,是V绑定了包含定时器的VM。


    组件化:(使用cocoapods进行组件化的实现)

    组件化方案的几种实现:

    方案一:url-block

    通过在启动时注册组件提供的服务,把调用组件使用的url和组件提供的服务block对应起来,保存到内存中。在使用组件的服务时,通过url找到对应的block,然后获取服务

    出现的问题:

    1、需要在内存中维护url-block的表,组件多了可能会有内存问题

    2、url的参数传递受到限制,只能传递常规的字符串参数,无法传递非常规参数,如UIImage、NSData等类型

    3、没有区分本地调用和远程调用的情况,尤其是远程调用,会因为url参数受限,导致一些功能受限

    4、组件本身依赖了中间件,且分散注册使的耦合较多

    方案二:protocol-class

    通过protocol定义服务接口,组件通过实现该接口来提供接口定义的服务,具体实现就是把protocol和class做一个映射,同时在内存中保存一张映射表,使用的时候,就通过protocol找到对应的class来获取需要的服务

    出现的问题:

    依然没有解决组件依赖中间件的问题、内存中维护映射表的问题、组件的分散调用的问题

    方案三:target-action

    通过给组件包装一层wrapper来给外界提供服务,然后调用者通过依赖中间件来使用服务;其中,中间件是通过runtime来调用组件的服务,是真正意义上的解耦,也是该方案最核心的地方。具体实施过程是给组件封装一层target对象来对外提供服务,不会对原来组件造成入侵;然后,通过实现中间件的category来提供服务给调用者,这样使用者只需要依赖中间件,而组件则不需要依赖中间件

    方案四:使用cocoapods进行组件化的实现)

    具体就是建立一个项目工程的私有化仓库,然后把各个组件的podspec上传到私有仓库,在需要用到组件时,直接从仓库里面取


    1.添加Podfile文件 pod init 然后会发现你的工程目录下多了Podfile文件

    2.生成xcworkspace工程 pod install

    3.新建一个Lib(自己起名)文件夹,用来存放组件库(其他独立工程)然后cd到Lib下 执行 pod lib create 

    XXX(工程名)

    4.打开新建的XXX(工程名)工程里的Example,可以看到pods里面,有个ReplaceMe的文件,意思就是要替换它,换成我们自己需要对外提供的类

    5.新建一个类,比如TRUXXX,复制粘贴到ReplaceMe同级目录下,并删掉ReplaceMe.m文件

    6. 之后cd到Lib/TRUXXX/Example/文件目录下,执行pod install 这个时候在Development Pods文件下会多出这两个文件,这就是本地开发的pods文件

    7.而Podfile的内容其实是

    pod 'TRUXXX', :path => '../'

    说明他获取的是本地路径

    然后删除Example for TRUXXX里面的TRUXXX类,不然运行会因为类重复报错。

    至此,一个组件的本地库就创建完成了。

    8. 壳工程使用本地组件库

    首先cd到壳工程LZDemo目录下,修改LZDemo的Podfile文件,增加

    pod 'TRUXXX', :path => 'Lib/TRUXXX'

    执行 pod install


    组件需要对外提供依赖关系。所以我们还得多做一步操作,那就是增加podspec文件

    以TRUXXX为例,cd到TRUXXX目录下,执行

    git tag 0.1.0

    git push --tags

    这个tag分支就是将来提供给别人依赖的版本号分支,有了它,别人使用你的组件的时候就可以根据版本号来控制了。

    改好后,在上传之前,最好先本地检查一下podspec是否合法

    执行下面语句

    pod lib lint --verbose

    如果出现passed validation,说明通过,可以提交到cocoapods上了

    成功后,就可以pod search到我们提交的库了

    ps:如果搜不到,不是没传成功,是我们的本地搜索库没更新,可以先删除~/Library/Caches/CocoaPods目录下的search_index.json文件或者pod repo update一下

    rm~/Library/Caches/CocoaPods/search_index.json


    组件间通讯

    1. Protocol注册方案

    通过JJProtocolManager 作为中间转化

    + (void)registerModuleProvider:(id)provider forProtocol:(Protocol*)protocol;

    + (id)moduleProviderForProtocol:(Protocol *)protocol;

    有组件对外提供的procotol和组件提供的服务由中间件统一管理,每个组件提供的procotol和服务是一一对应的。

    例如:

    在JJLoginProvider中:load方法会应用启动的时候调用,就会在JJProtocolManager进行注册。JJLoginProvider遵守了JJLoginProvider协议,这样就可以对外根据业务需求提供一些方法。

    + (void)load

    {

        [JJProtocolManager registerModuleProvider:[self new] forProtocol:@protocol(JJLoginProtocol)];

    }

    - (UIViewController *)viewControllerWithInfo:(id)userInfo needNew:(BOOL)needNew callback:(JJModuleCallbackBlock)callback{

        CLoginViewController *vc = [[CLoginViewController alloc] init];

        vc.jj_moduleCallbackBlock = callback;

        vc.jj_moduleUserInfo = userInfo;

        return vc;

    }

    这样就可以在需要登录业务模块的地方,通过JJProtocolManager取出JJLoginProtocol对应的服务提供者JJLoginProvider,直接获取。如下:

    id<jjwebviewvcmoduleprotocol> provider = [JJProtocolManager moduleProviderForProtocol:@protocol(JJWebviewVCModuleProtocol)]; 

       UIViewController *vc =[provider viewControllerWithInfo:obj needNew:YES callback:^(id info) { 

           if (callback) { 

               callback(info); 

           } 

       }]; 

       vc.hidesBottomBarWhenPushed = YES; 

       [self.currentNav pushViewController:vc animated:YES];</jjwebviewvcmoduleprotocol> 

    2. URL注册方案 OPENURL

    原理:

    通过url注册服务, 其他地方通过url, 获取服务;框架在维护一个url-block的表格

    特点:

    每个业务组件, 都需要依赖这个框架

    url维护成本高 硬解码

    可以在组件内部任何地方调用/注册服务, 没有必要统一组件接口服务

    [MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) { 

    NSNumber *id = routerParameters[@"id"]; 

    //create view controller with id 

    // pushview controller 

    }]; 

    首页只需调用 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以打开相应的详情页。

    3. Target-Action runtime调用方案

    原理:

    每个组件, 提供一个统一披露的接口文件

    额外的维护一个中间件的分类扩展(在此处进行硬解码 通过运行时进行物理解耦)

    其他地方通过target-action;的方案进行交互

    特点:

    统一了组件api服务

    组件与框架之间无依赖关系

    需要额外维护中间件类扩展

    实现:

    我们主要是依赖CTMediator 这个中间件工具类中主要使用如下方法:

    - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget

    方法内部使用Runtime调用 需要传三个参数

    当前需要调用的类名 (字符串)

    当前需要调用类的方法名 (字符串)

    需要传的参数 (字典形式)

    # 通过Runtime 把字符串 转换类

    Class targetClass = NSClassFromString(ClassString);

    id  target = [[targetClass alloc] init];

    # 把字符串转换成事件

    SEL action = NSSelectorFromString(actionString);

    # 如果当前类中有这个事件 那就执行这个事件 把需要的参数传值

    if ([target respondsToSelector:action]) {

        return [target performSelector:action withObject:params];

    }

    4.使用cocoapods进行组件化的实现)

    1.每个业务组件库里面会有一个控制器的配置文件(路由配置文件),标记着每个控制器的key;

    2.在App每次启动时,组件通讯的工具类里面需要解析控制器配置文件(路由配置文件),将其加载进内存;

    3. 在内存中查询路由配置,找到具体的控制器并动态生成类,然后使用==消息发送机制==进行调用函数、传参数、回调,都能做到。

    5. 依赖注入

    组件化的好处:

    业务分层、解耦,使代码变得可维护;

    有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;

    便于各业务功能拆分、抽离,实现真正的功能复用;

    业务隔离,跨团队开发代码控制和版本风险控制的实现;

    模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;

    在维护好各级组件的情况下,随意组合满足不同客户需求;

    https://www.jianshu.com/p/59c2d2c4b737创建组件化的步骤

    各个组件该如何进行拆分:

    1.  项目主工程:主工程就是一个空壳子工程

    2.  业务组件:业务组件就是各个独立的产品业务功能模块

    3.  基础工具类组件:基础工具类是各个互相独立,没有任何依赖的工具组件。它们和其它的工具组件、业务组件等没有任何依赖关系。这类组件例如有:对数组,字典进行异常保护的Safe组件,对数组功能进行扩展Array组件,对字符串进行加密处理的加密组件等等。

    4.  中间件组件:中间调度者就是一个功能独立的中间件组件

    5.  基础UI组件:视图组件就比较常见了,例如我们封装的导航栏组件,Modal弹框组件,PickerView组件等。

    6.  业务工具组件:这类组件是为各个业务组件提供基础功能的组件。这类组件可能会依赖到其他的组件。例如:网络请求组件,图片缓存组件,jspatch组件等等

    详细操作步骤:

    第一步:

    我们先创建一个空的iOS工程项目:MainProject,这个空项目作为我们的主工程项目,就是上面所说的壳子工程项目,然后初始化pod

    第二步:

    我们创建一个空工程项目:ModuleA,这个ModuleA 项目作为我们的业务A组件。然后我们初始化pod,初始化podspec文件

    第三步:

    我们创建一个空工程项目:ModuleB,这个ModuleB 项目作为我们的业务B组件。然后我们初始化pod,初始化podspec文件

    第四步:

    我们创建一个空工程项目:ComponentMiddleware,这个项目就是我们上面所说的中间调度者。然后我们初始化pod,初始化podspec文件。

    第五步:

    我们创建一个空工程项目: ModuleACategory,这个工程是对应业务组件A的一个分类工程。然后我们初始化pod,初始化podspec文件。

    第六步:

    我们创建一个空工程项目: ModuleBCategory,这个工程是对应业务组件B的一个分类工程。然后我们初始化pod,初始化podspec文件。

    第七步:

    我们在主工程MainProject的Podfile中引入我们的业务组件B工程ModuleB,以及引入我们的ModuleB的分类工程:ModuleBCategory。然后我们pod install。这时已将这两个组件库引入到我们的主工程中了。

    #import <ModuleBCategory/ComponentScheduler+ModuleB.h>

    - (void)moduleB {

        UIViewController *VC = [[ComponentScheduler sharedInstance] ModuleB_viewControllerWithCallback:^(NSString *result) {

            NSLog(@"resultB: --- %@", result);

        }];

        [self.navigationController pushViewController:VC animated:YES];

    }

    第八步:

    上面第七步中,我们用到了ModuleBCategory 这个分类工程。这个工程我们只对外暴露了两个文件。这两文件是上面的中间调度者的分类,也就是说是中间件的分类。我们先来看下这个分类文件的.h 和.m 实现

    #import "ComponentScheduler+ModuleB.h"

    @implementation ComponentScheduler (ModuleB)

    - (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback {

        NSMutableDictionary *params = [[NSMutableDictionary alloc] init];

        params[@"callback"] = callback;

        return [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];

    }

    @end

    第九步:

    这个分类的作用你可以理解为我们提前约定好Target的名字和Action的名字,因为这两个名字中间件组件中会用到。

    因为上面第八步中引用到中间件工程,这里我们就来看下中间件工程到底做了什么工作。还记得上面第八步中,我们调用了一个中间件提供的函数:performTarget:action:params:shouldCacheTarget吧,这个是中间件核心函数。

    这个函数最终调用到苹果官方提供的函数:[target performSelector:action withObject:params];

    看到 performSelector: withObject: 大家应该就比较熟悉了,iOS的消息传递机制。

    [Target_ModuleB performSelector:Action_viewController withObject:params];

    上面这行伪代码意思是: Target_ModuleB这个类 调用它的 Action_viewController: 方法,然后传递的参数为 params。

    细心的小伙伴们就会发现,我们没有看到过哪里有这个Target_ModuleB 类啊,更没有看到Target_ModuleB 调用它的 Action_viewController: 方法啊。

    是的,这个Target_ModuleB类和类的Action_viewController方法就在第十步中讲解到。

    第十步:

    业务组件B除了提供组件B的业务功能外,业务组件B还需要为我们提供一个Target文件

    #import "Target_ModuleB.h"

    #import "ModuleBViewController.h"

    @implementation Target_ModuleB

    - (UIViewController *)Action_viewController:(NSDictionary *)params {

        ModuleBViewController *VC = [[ModuleBViewController alloc] init];

        return VC;

    }

    @end

    从上面的实现文件中,我们可以看到,Target文件的作用也很简单,就是为我们提供导航跳转的目标控制器实例对象。这里的目标控制器实例就是业务组件B的ModuleBViewController 实例。

    相关文章

      网友评论

        本文标题:架构

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