V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
LINAICAI
V2EX  ›  iOS

iOS 架构问题

  •  
  •   LINAICAI · 2016-07-11 22:17:26 +08:00 · 2076 次点击
    这是一个创建于 3092 天前的主题,其中的信息可能已经有所发展或是发生改变。

    大家好,目前公司 iOS 项目架构上遇到一些问题,虽然也看了不少架构文章,也准备着手优化架构上存在的一些问题,但总有那么一丢丢犹豫不定或者解决不了的,想在这里从 iOS 大神们得到一些建议和帮助。

    首先说下目前架构,典型的 MVCS ,最后的 S 是数据存储层, M 层是瘦 Model ,只用来表达数据的外观, C 和 V 比较经典,之所以使用 MVCS ,第一是觉得上手比较容易,分层明确。第二是觉得瘦 Model 和减少 C 的复杂度是一个项目走向复杂和可扩展的出路。
    

    但目前实践过程中遇到一些问题,下面列出来:

    1 、 S 层不单只做了存储,还负责作为数据源提供给 C 层,除此之外还负责从网络层获取数据,也就是变成了半个业务层。
    
    2 、由于上面 1 的问题(数据源),导致类似分页的 pageIndex , pageSize 也放进了 S 层,虽然 C 层是舒服了,但 S 层免不了开始容易出错并不好找错,虽然根据业务和模块有很多的 S 类,但还是觉得不对劲。
    3 、跨层调用使用通知,发现不好统一管理,代码乱飞。
    

    想问下,我应该怎么处理以上问题,我提出几个比较困惑的问题:

    1 ) S 层能否作为数据源提供数据给 C?,业务层(部分模块很复杂)放在哪里会比较合适?
    
    2 )类似分页的 pageIndex , pageSize 该放进业务层还是 S 层、 C 层?
    
    3 )跨层调用想过 KVO+观察者模式,但目前数据源在 S 层,无法方便的解除观察,还是说把业务逻辑回归 C?
    
    8 条回复    2016-07-27 11:30:40 +08:00
    expkzb
        1
    expkzb  
       2016-07-11 22:31:52 +08:00
    kvo 请使用 https://github.com/Raizlabs/RZDataBinding
    坚决不要用通知来传递数据,其他随意
    LINAICAI
        2
    LINAICAI  
    OP
       2016-07-11 22:34:51 +08:00
    @expkzb KVO 的库太多了也是不知道怎么选择,通知是我不到万不得已都不会去用的。
    kitalphaj
        3
    kitalphaj  
       2016-07-12 07:53:38 +08:00
    S 是 Service 是吧。那 1 就不是什么大问题,只要负责数据提供的河网络获取的 Service 分开就好。至于 2 , pageIndex 这种当然不能放到 S 层,除非是全局的。一般情况放到 C 就好了,毕竟页码也是显示逻辑的一部分,如果你用 ViewModel 倒是可以放到 ViewModel 里面。 3 分情况,如果是从 S 到 C 的回调(比如网络)就用 RxSwift (或者 KVO 也行)或者简单一点 Callback 也行。如果是 V 到 C 的,就用 Delegate 就好了。 C 到 S 或者 C 到 V 直接调用传参数就好不需要什么异步之类的。至于你说的数据源在 S 就无法方便的解除观察是什么意思?
    LINAICAI
        4
    LINAICAI  
    OP
       2016-07-12 09:32:29 +08:00
    @kitalphaj 这里的 S 不是 Service 而是 storage ,而我在 S 中确实是有另外的 service 获取网络数据的,这里的 S 层负责把网络数据(网络请求的 service 中再次封装 AF)返回的 json 转成 model 对象并封装成数据源并通过 block 回调给 C
    由于这个 S 或者 model 的基类是 NSObject ,不知道什么时候解除观察才是最好的时机,毕竟我是要 KVO 数据源或者 model 的。
    kitalphaj
        5
    kitalphaj  
       2016-07-12 10:38:38 +08:00
    @LINAICAI 好吧,估计要看具体代码才能明白为什么解除观察会有问题。如果你是怕 object 被释放后 observer 还在什么的,那就像一楼说的,用第三方库来保证不出异常。
    coa
        6
    coa  
       2016-07-26 20:23:08 +08:00
    即便决定为 C 瘦身,分离出来的业务逻辑也总得有个去处。我的做法是每个 C 对应一个 DataManager ,同样继承于 NSObject ,在这层负责业务逻辑和存储,也就是合并 Service 层和 Store 层,拆分得过细的话 C 得来回对接也是累。 C 只负责动画、页面跳转等,不处理具体数据。
    每个 V 对应一个 VM ( ViewModel )层,但这个 VM 没像传统 MVVM 层一样处理业务逻辑,只存放视图所要表现的数据,以及负责把 Model 转换成表现数据( NSDate 转成 NSString 等)。这样做的好处是复用性较好,换了 Model ,只需增加一个工厂+viewModelWithOtherModel 即可。此层的数据呈现既可以用传统方法 bindDataWithViewModel 和 V 进行数据绑定,也可以直接 KVO ,同推荐用一些轻量的第三方库,或者干脆直接上 ReactiveCocoa ,原生 KVO 实在繁琐,还得惦记着移除。 VM 层下还可以有多个子 VM 。
    Model 层也和楼主一样是瘦 Model ,即 entity ,不负责相关业务逻辑。原始数据转 entity 这一类都在 DataManager 里做。 DataManager 和 C 的交互可以直接用 callback ,也可以斟酌用 RAC 包装成信号,获取更大灵活性。
    就这样, C 持有 DataManager , DataManager 获取数据并转换出(多个) ViewModel , ViewModel 持有 entity , ViewModel 和 V 绑定, DataManager 和 ViewModel 不持有任何 UI 元素,互不干扰,需要做单元测试等也方便。
    LINAICAI
        7
    LINAICAI  
    OP
       2016-07-26 23:37:54 +08:00
    @coa 按照你的办法,一个 C 可是至少对应一个 ViewModel 和一个 DataManager 啊,类数量翻番了,更别说如果一个 C 有多个 V)的情况下, ViewModel 的数量又数不清多少了,对于大项目来说根本不可行的,就算你强行推这套,也必定受到其他人的反对,你的团队成员要么偷懒要么会执意写自己的了。

    在我的架构中,我也有一个 DataManager 层(也继承于 NSObject ),但这个 DataManager 封装了某个模块的数据请求和存储的逻辑,然后对应着不止一个 C ,也就是把原来的每个 C 建一个 DataManager 改为多个 C 对应一个 DataManager ,然后我没有使用 ViewModel ,因为我觉得 ViewModel 是把双刃剑,用的好是不错,但用不好就是个坑。

    最近也是发现 C 下的部分与业务无关的逻辑难以复用(如:重复的跳转一个控制器的业务),所以想试试 ViewModel 能否帮我解决这个问题,但目前项目如果全面支持 ViewModel 这种的话,改动又过于大。

    非常感谢您的对答,我发现我们的想法很多地方一致,希望能跟你更多的交流项目架构上的设计理念。
    coa
        8
    coa  
       2016-07-27 11:30:40 +08:00
    @LINAICAI
    C 并不和 VM 对应啊,其实更像是把业务逻辑部分从传统的 VM 中剥离出来,放入 DataManager ,而 VM 只对应 V ,有多少 V 就有多少 VM 。 DataManager 对应楼主的 Store ,但也确实多出来一堆 VM ,但这部分是可以复用的,个人认为值。不知道楼主的瘦 Model 中具体是怎样的,比如日期,如果存储的是 NSString 的话,那这个其实就是楼主的瘦 Model 了,只不过可能多了数据转换这一步。
    举个栗子,一个 tableview 上每个 Cell 都显示着人名和生日,刚开始学 iOS 时是直接在 C 中获取,然后在 tableview 的代理方法处转换并绑定。但这转换和绑定部分是可以复用的。所以现在的做法是在 DataManager 中获取并,然后 VM 中自己转换, V 中进行绑定。比如 DM 中从 JSON 取来一系列{"name":"John", "birth":"1984-1-1"},接着转成一系列 entity :{NSString *name, NSDate *birth}。 VM 有自己的+viewModelWithEntity:,转换成一系列 VM :{NSString *name, NSString *birthString}。这一系列 VM 就存在 DM 中的 VMs 数组。 tableview 代理处只需[cell bindDataWithViewModel:viewmodel]即可(此时的 V 是一个 Cell )。这样 VM 和 V 拥有较高的复用性。甚至如果多处 tableview 都只是呈现列表, tableview 这一部分还可以进一步抽象封装成一个 baseTableViewController ,只留出 callback 。当然这是很传统的方法,只不过代码存放位置做了调整,应该不会像双向绑定那样有其他不确定性,比较好上手和过渡。

    也试过用单例封装一些通用的数据请求逻辑和一些全局变量,可能和楼主的这个有些像。不过到了后边,密密麻麻一整片,看着也是累。估计拆分成 category 会明朗些。现在的做法是各个 DataManager 都继承于一个 BaseDataManager ,发现逻辑或数据其他 C 也用得上的就直接剪到基类去,应该比 category 好点。

    这个没啥好办法啊, C 是最不能复用的那个了吧,所以其他能复用的一般都尽可能单独抽取出来了。能注意的是尽可能保持各处统一,比如楼主重复跳转到一个控制器这个,举个栗子,在多个地方点击头像跳转到用户详情页,额,不知道是不是这种跳转,假设是一回事儿,我的做法是下边这种,头像的 tappedBlock 类似这样:
    ```
    UITabBarController *tabBarController = (UITabBarController)UIApplication.sharedApplication.delegate.window.rootViewController;
    UINavigationController *nav = tabBarControler.selectedViewController;
    XXUserViewController *userViewController = [XXUserViewController alloc] initWithUid:uid];
    [nav pushViewControlleruserViewController:animated:YES];
    ```
    这一段可以直接封装进头像控件。直接从切面取出 nav ,不用担心控件可能不是刚好处于 vc.view 需要顺着响应链去查找 nav 。当然这需要各个 Tab 都有个 RootNav 才能做到完全通用。跳转使用的是模态则直接让 tabBarController 来。

    可能因为团队成员专写 iOS 的就我一只,老大负责疑难杂症和 review ,对具体代码过程没啥限制,项目也不大,所以想使用啥都可以试试。可以边写边改进哈哈。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   927 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 21:27 · PVG 05:27 · LAX 13:27 · JFK 16:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.