从一个例子开始
现在假定一个开发人员,我们可以称他为小Y,他在负责公司C的一个移动端产品的开发。当前的工作进展是小Y正在开发产品的登录页面。这是一个很简单的页面,就想你所知道的最典型的登陆页面一样,包含一个账户名、一个密码两个文本输入框。在它们的下面是一个“登录”的按钮。当用户填写完成后点击“登录”按钮进行登录请求。
小Y面对以上的需求,进行了简单的分析:
- 登录过程是一个网络请求,需要在登录按钮的点击事件被出发之后进行。
- 需要对登录按钮的点击事件进行监听,通过回调方法的方式进行实现。
分析过后,小Y觉得这是一个很简单的实现嘛,便着手写代码了(以安卓平台示例)。
Button btnLogin = findViewById(R.id.btn_login);
btnLogin.setOnClickListener(
new View.OnClickListener(){
public void onClick(View view){
//调取登录接口
login();
}
}
);
编译运行,一切完美。
但是,事情并没有那么简单。在没多久之后,小Y的产品经历产生了一个新的想法:我们是不是应该像手机qq那样,加一个用户的头像呢?于是就这样,一个新的需求来了。
小Y思索,这没问题,开个空间就行,再用Glide(一个图片加载库)或者什么把用户的头像一加载就好了。
但是,产品经理发现了问题,当网络不太好的时候,头像还没加载成功的时候就已经登录成功了。他不喜欢这样的设计,所以找来小Y:我们需要做到这个,假如用户点击按钮的时候头像还没家在出来,那么你先等一等,直到用户的头像家在成功了再进行登录。
直到这个时候,小Y才感觉到有些不太好了。他分析了一下当前的需求:
- 需要把用户的头像加载到一个控件中
- 在头像加载成功前不能进行登录,哪怕是用户点击了“登录”按钮
- 也就是说,只有在头像加载成功和“登录”按钮被点击这两个事件都发生过之后才能进行登录。而且这两个都是异步的逻辑。
小Y在分析了新的需求之后,觉得可以使用一个全局标志位来解决这个问题,某一个事件完成后会修改这个标志位,当另一个完成的时候检查这个标志位是否已经被修改过,如果已经被修改过的话就证明两个条件都已经满足了。
但是,小Y总觉得心里不舒服,因为这样的解决方案会使得代码变得不够优雅,而小Y又是一个追求优雅的工程师。那么,换做是你你会怎么办呢?
背景
接下来进入正题。在开发中,有时候会有这样的需求:我们需要在多个条件满足时来触发一个操作,这些条件的满足往往是异步的,比如通常是在一个回调方法中得到满足。当条件A得到满足后我们会来指定一段代码(或者某个方法)来响应该条件。为了方便讨论,我们会给以上场景中的关键对象来命名。
- 目标程序(Target Code):为了达成一定的目的需要执行的一段代码。
- 目标条件(Target Condition):为了使目标代码得意执行所需要满足的条件。在没有异议的时候可以简称为“条件”。
结合上面叙述的小Y的案例,我们可以用一个简单的流程图来表示小Y遭遇到的问题。
场景模型描述
以上所描述的场景,可以进行简单的抽象,如下图所示:
根据以上抽象的描述,目标代码需要N条件同时成立才能执行,其中至少有一个条件是异步的。这种模型我称之为“异步多条件依赖模型”(Asynchronous Multiple Condtions Dependence,简称AMCD模型)。
那么,这个问题该怎么解决呢?首先,可以用一些全局变量来表示某些条件的满足,并在每个条件后都加以判断来确认是否全部条件都已经得到满足,但是前面也说了,这种方法很不优雅。其次,现在开源界有一些不错的方案可以解决以上问题,比如EventBus、RxJava等等。但是你会发现这些以事件发布订阅模型为基础的解决方案实现起来也显得很不优雅。而你又是一个偏爱优雅的开发者。除此之外,你们的项目很可能对第三方依赖有着很大的要求和限制。这个时候,就轮到我这篇文章中讲述的模式登场了–条件仓库模式。
条件仓库模式
想要完成该模式,需要来明确这个模式中都需要哪些角色。
首先,既然有多个条件,那个我们可以考虑设计一个条件的仓库(是一个条件的集合),同时条件仓库提供一些对条件的添加、移除、判断等方法。其次,根据AMCD模型的描述,结合实际的开发经验,在项目中条件添加的地方、条件被满足的地方和目标代码执行的地方很有可能是处于不同的类中的。和多个不同的类进行交互去对项目中的条件仓库进行管理,这又需要一个角色,我们可以称之为“仓库管理员”。那么,我们可以明确以下角色是必要的。
- 条件仓库(Condition Repo):存放条件,并且提供一些条件的管理方法,如添加、移除、判断等。
- 仓库管理员(Condition Repo Manager):它的职责是负责管理项目中的多个仓库,满足使用者对仓库的新建、调取、删除等操作。我建议该角色的实例化采用单例模式来实力,这样可以让使用者更加方便,不用再去关心“仓库管理员”对象的创建过程了。
以下是条件仓库模式的UML图:
条件仓库模式强调的是把代码之间执行顺序的逻辑关系转换成“条件” 和 “目标”的关系。我也鼓励开发者把项目中类似的需求按照条件和目标的逻辑来梳理。
一般来说,条件会有多个,而目标只有一个。条件中有一些是异步的。当我们把项目的代码逻辑按如此形式来梳理的话,就可以用条件仓库模式来很方便的解决类似的问题。
在一个项目中,如果详细梳理的话,这样的条件和目标的关系对可能会存在有很多对。那么具体怎么存放所有的条件就会影响到条件的存取、状态更新等操作是否方便可靠了。因为在该模式的关系对中,虽然条件有多个,但是目标只有一个,所以我们可以为每一个目标创建一个条件的仓库。这样的结构将会很清晰、简单。
回到例子中
我们再来回到小Y的案例中。现在我们可以结合“条件仓库模式”来解决小Y面对的问题。我们先把小Y所遇到的问题抽象成上述的“条件”-“目标”关系。
条件1: 用户点击了“登录”按钮
条件2: 用户的头像加载完成
目标: 执行登录请求
在确定了条件和目标之后,我们就可以着手解决了。
首先,我们需要给每个条件取一个名字,我建议是用一个成员变量来表示,而这个成员变量必须同时能被条件 和 目标所在的类对象访问的到。其次,我们也需要给目标取一个名字。
public static final String CONDITION_LOGIN_BTN_CLICKED = "1";
public static final String CONDITION_AVATAR_LOADED = "2";
public static final String TARGET_LOGIN = "3";
接下来就可以在创建条件仓库和仓库管理员了。
class ConditionRepo{
//目标
private Runnable target;
//条件和它们状态的kv对
private Map<String, Boolean> conditionState;
//other...
}
class ConditionRepoManager{
//仓库的索引,方便管理员管理
private Map<String, ConditionRepo> repos;
public void getRepo(String targetName){
//获取repo
};
//other...
}
有了以上的类,我们就可以帮助小Y优雅地解决问题啦:首先在初始化的时候创建一个条件仓库,向其中添加两个条件CONDITION_AVATAR_LOADED 和 CONDITION_AVATAR_LOADED,并初始化它们的状态都是未被满足(false)。其次,再初始化一个仓库管理员,把我们的目标TARGET_LOGIN在他那里进行注册。然后,就可以在图片加载完成和登录按钮被点击的地方分别执行“更新条件状态并检查是否所有的条件都已得到满足”这样的操作了。在以上判断是true的话就可以愉快地进行登录啦。
有了这样的架构,不管以后再添加多少个异步的条件都能够优雅地应对了,同时也会让代码的可读性提高很多,也不会出现一周之后望着自己曾经添加的各种成员标志位而不知其作用这样尴尬的情况了。
实现
接下来,我会把该种解决方案进行一个实现,采用java语言。详情请看AMCD-java