再谈组件化模块划分

前言

问题总来源于需求。

随着时间的推移,部门的组件化已经走过了第一个年头,这一年中我们遇到了很多问题,也解决了很多问题。当然随着组件化的推进,和多种业务场景的接入,新的需求不断产生,1.0版的设计规范和组件化结构的划分,已经难以满足现有需求了。

所以根据组内讨论结合实际在项目中出现的问题,以及产品经理对产品的希望等等因素,推出了本篇《再谈组件化模块划分》。

引入组件化主要是解决两个问题:

  1. 易维护:将大项目变小,让模块间彼此独立,降低了代码量、定位范围、逻辑耦合等,进而实现易维护的目的。

  2. 高复用:由于组件的独立,淡化了组件间的依赖关系,这让一个组件可以很方便的出现在不同组件化项目中。

当有以上两个需求的时候,可以考虑引入组件化。

一、设计参考原则

在讨论问题的时候经常会有一些难以定夺的问题点。

  • 例如对于Bean这个对象的处理,可以使用JSON模拟请求响应的形式,也可以通过类似于AIDL交付的形式。

讨论这样的问题时往往难以有一个指导参考。类似我们做面向对象的设计模式时,他的指导方针是设计模式六大原则。所以这里也先给出组件化的设计初衷,作为后续制定解决方案的参考条件。

  • 遵从隔离原则,按层次划分好组件后,组件间不可见(不允许出现组件依赖组件的情况)。
  • 最小引入原则,依赖可以通过组件化解决时,尽量通过组件化实现。
  • 高效率原则,解决方案带来的实现代价应尽可能小。
  • 不冗余原则,对于同样的逻辑或是数据类应尽量保证只有一个。
  • 易读原则,命名上应尽量简短准确的表达该文件或者代码所起的作用。
  • 易维护原则,实现方案应尽量容易维护,有利于快速定位问题,解决问题。
  • 兼容原则,当组件升级时,应尽量考虑老版本的兼容性。
  • 高可复用原则,组件开发时,思维应尽量脱离当前项目,提高组件的可复用性。

上面仅是个人的一点心得,实际问题可能有更多维度需要考虑。

在明确评判维度后,应该结合需求,在考虑权重后再作为评判参考。

二、MVP - 概念回顾

在说划分之前让我们回顾下MVP的概念,介绍这个概念主要是为了后续Module划分做理论支撑。

这里我们不讨论MVP的实现,仅阐述MVP各代表的是什么。

  • Model - 数据层,数据操作都由本层来完成,如数据的请求获取解析,和数据库增删改查等。
  • View - 视图层,和UI相关的操作都放置于此层。
  • Presenter - 逻辑控制层,逻辑相关的处理都在此层。

在MVP中P层是M和V的桥梁,可以理解为V层通过P层向M层请求数据,拿到数据后,P层做数据处理,完成后回调给V层。

三、 基于MVP进行组件内划分

正常来说组件化和MVP是没有必然关系的,只是我们在组件划分完成后,为了更好的表达功能意图和更高的易维护性,在组件内部采用了类似于MVP分层的方式来实现。

再重新划分之前我们的组件化结构是:

一个业务对应一个组件,组件涉及的所有逻辑和页面在一个Module中。

在我们的组件稳定后,发现一些情况下组件是需要作为SDK交付给其他项目使用的

在组件作为SDK交付时,开发者更需要的是M和P层的代码,而View层代码一般来说是不需要的。

另外作为SDK给别人使用时,需要明确两个点:

  • SDK内部的通讯是不能通过组件化框架实现的。
  • 不能影响使用SDK的开发者使用组件化的方式开发项目的。

基于以上,衍生一个新的需求:

  • 如何让一个组件既可以正常按组件化架构开发,又能方便的作为SDK对外开放。

这里我们给出的方案是对一个Module再次划分:

在这里插入图片描述
上面的结构简单来说就是对M和P层进行独立和下沉,而组件化框架由View层实现。
这样做同时满足了以上的需求:

  • M、P层可以作为SDK直接对外提供
  • 包装层实现组件化框架后底层依赖M、P层后依然可以按照组件化的模式开发
  • M、P层和组件化没有关系,不在对使用SDK方有组件化相关的要求

下面是我们这样处理的原因。

3.1 M、P层可以作为SDK直接对外提供

P层是逻辑处理层,一般来说和其他组件的业务交互需要在本层来完成。

例如:首页组件,根据当前的用户信息查找对应的本地缓存。那么一般来说首页组件会向登录组件请求用户数据,然后和组件内的数据做比较后返回结果。

以上的交互中存在一个问题,我们将组件化框架带入到了P层,这影响了P层的复用性

原因前面已经说过了,当M、P层作为SDK交付给其他项目时,我们是不能要求开发者的项目是否使用组件化模式开发的,即使使用了组件化开发模式,也不能影响他们使用哪个组件化框架。

所以为了更高的复用性和兼容性,M、P层不再引入组件化框架,而是将组件化框架的实现挪至包装层。

当然在这会引入一些问题,这个我们在 “四、具体实现” 中给出。

3.2 View层不在冗余

而View层在不同的项目中往往是不能直接使用的(如同一项目的手机版和PAD版)。

当我们的组件逐渐稳定后,发现虽然使用了MVP的开发模式,但是往往V层的实现在别的项目里是显得多余的,因为不同的交互逻辑可能导致V层基本无法复用,此时如果将MVP打包成一个组件交付过去,那么View层的代码就有点冗余了,即使给了源码可能也要做一些处理。

所以将V层与MP层的分开,当发现View层不适合时,我们可以只交付MP层过去。

四、具体实现

4.1具体实现架构图

在这里插入图片描述
下面由上之下的介绍一下项目结构。

  • APP层还是为了打包而存在的壳
  • Module层里面存放了一些无法下沉的逻辑,可以理解为组件所在项目独有的逻辑。同时组件化框架的实现也在Module层。所以组件间的通信也由Module层实现。
  • 将原Module中可以下沉的逻辑和数据相关的操作,下沉至MP层,之所以叫MP层,是因为叫Lib或Module容易在理解上引起歧义。MP层仅被自己的Module层依赖,同时MP层仅依赖router_bean!
  • 最后是下面的Lib层,Lib的意思是可以被多个组件进行依赖。

4.2 关于 lib_router_bean

正常来说,Bean应该是存在于MP的Model部分,但是由于我们需要MP层有作为SDK的能力,可以独立的对外提供,换句话说就是他应该没有依赖,从上面的依赖关系中也可看出,他仅依赖的router_bean这个模块。但是这样就导致了一个问题,由于他只对上层的Module开放,也就只有他上层的Module可以看到他的的Bean,别的Module是看不到的,那么在交互的时候就产生了问题。

组件和组件时平等的,所以当Bean仅对自己的上层Module可见时,其他Module的Bean也是这样。这在组件交互时就产生了问题。

例如:首页组件,根据当前的用户信息查找对应的本地缓存(CacheBean)。那么一般来说首页组件会向登录组件请求用户数据(AccountBean),然后和组件内的数据做比较后返回结果。

上面例子中的AccountBean就变成了只有登录组件内可见,首页的回调是无法使用该数据对象的。

同理router中的接口也会有这样的问题。

所以为了让Bean在各个组件间可见,我们选择了对Bean层进行下沉抽离,所有涉及到对外开发的Bean统一到 lib_router_bean 中进行保管,而此组件不依赖任何其他库,仅作为数据Bean的容器,解决Bean可见性的问题。

4.3 P层如何拿到数据

由上面的结构划分可知,此时的P层已经不具备组件通信的能力了。

但是P层类似于查询类型的能力,又需要注入一个参数。此时我们的解决方案是,将这种类的参数变成P层的方法参数,然后由上层Module通过组件化获取后提供。

//由P层自己实现的查询当前用户缓存的方法
public CacheBean getCurrentCache(){
	AccountBean accountBean=Router.get(LoginModel).getCurrentAccount;
	return model.getCache(accounBean);
}

//参数化后的实现
//P层不在通过组件化通信的方式查询数据,而是通过参数化的方式拿数据,由调用方提供。
public CacheBean getCurrentCache(AccountBean account){
	return model.getCache(accounBean);
}

4.4 MP层作为SDK对外发布

上面 lib_router_bean 的出现帮我们解决了MP层作为SDK开发时数据可见的问题,但是我们的MP层还是产生了对 lib_router_bean 的依赖。这里我们的解决方案是,在 lib_router_bean 明确后,可以将 lib_router_bean 打成jar包放到MP层中,然后将MP打成一个独立的SDK再对外发布。

五、细节问题

5.1 支线与主线

在上面的结构划分完成后,我们可以将MP层理解为主线,尽量的将通有逻辑下沉至MP层。
这一层可以单独的抽离出当做标准化SDK进行维护,对外提供通有的标准化能力和接口。
而Module层我们可以看成支线代码,Module所在项目的特性可以在这一层进行体现。
这样就解决了问题同步的问题。

5.2 逻辑下沉

由于MP层变成了标准的SDK,所以需要尽可能的展现兼容性和能力的健壮性。
这里我们给出的建议是尽可能的将Module中的业务逻辑做标准化下沉,标准化下沉的意思是,脱离出当前项目的逻辑,将其考虑成他项目也会使用到类似的功能。

这里为大家举个例子:

public void onCLick(View v){
	if(v.getId()==R.id.login){
		if(TextUtil.isEmpty(username)){
			showToast("账号不能为空"!)
		}
		//...其他判断逻辑
		presenter.login(username,password);	
	}
}

一般来说我们会有一些判断逻辑或者数据的处理逻辑留在上层。当我们判断数据满足传入条件时,再将数据向下传入。这样做方便了我们直接通过UI给用户反馈信息,但是缺点就是这些判断逻辑没有沉淀。

所以为了不断增强SDK的能力我们需要换一种思维:

public void onClick(View V){
	if(v.getId()==R.id.login){
		int result=presenter.login(username,password);
		if(result==-1){
			showToast("账号不能为空"!)
		} 
	}
}

5.3 下沉后的返回码与多语言

由上面可知,我们的一个方法可能返回多个返回码,那么就需要友好的处理返回码和返回信息,因为只有码找不到对应信息的话,会增加很多沟通成本。

我们这里的建议是,提示性信息还是需要放到SDK本身的string.xml中的,至少提供一个语言版本。然后在程序中使用MessageMap的形式,将code作为key,message作为Value进行存放,形成映射关系后,开发者就可以很方便的通过MessageMap找到对应的Message了。

另外对于一个功能他的返回码可能非常多,还是已登录为例,他可以是账号为空,账号违法,密码为空,密码违法等判断情况发生,但是这么多种情况一般来说是不用都展示给用户的,那么此时就需要使用SDK的开发者自己进行二次封装了。比如说将账号为空和账号违法合并成一条账号不合法。

在这次封装后,SDK的语言体系只作为参考,翻译的重点还是Module本身的语言体系。

当然我们也可以定义Message的标注化,然后丰富到SDK的string.xml,这样也可以简化上层的翻译业务。

六、结语

组件化的好处是易维护和高复用,两者可以同时并存或单一存在,所以有其中一个需求时,就可以考虑使用组件化了。不过仍然在一些项目中看到,为了图方便而使用违背组件化设计初衷的方式去实现业务,这里我想说的是,既然选择了,就请标准,不要折中,因为如果折中我们完全可以去掉组件化,用更方便的方式去开发项目。

对于组件化应该理性看待,组件化的引入是增加了开发量的,因为我们使用一些代码和项目结构去建立规则,进而控制我们的业务结构,当然这是为了业务的易维护和高可复用。所以一些小型项目是可以不引入组件化,或者是上面稍显复杂的组件化架构的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值