前言:
本文主要是对常见设计模式的一些分析,以及讲述在Android项目中实现Mvvm模式的两种方式。通过Databinding或者ViewModel+LiveData+Repository如何实现Mvvm的相关设计以及他们各自优缺点的一些比较。
作为一名移动开发者,在项目开发的过程中,总会遇到一些问题。比如,在现在的项目开发过程中,就遇到一个类中或者说一个模块的代码逻辑过多的问题,尤其是在Activity/Fragment/View中动辄上千行代码,各种数据请求逻辑(网络,数据库),数据处理逻辑(排序,分类),UI渲染(View层本职工作)全部混杂在其中。写的时候非常潇洒,思路很清晰,想到一步写一步。然而等到再过一段时间需要修改这些功能的时候,发现自己已经无法找回原来自己的思路了。UI与数据逻辑耦合严重,导致后期代码维护和功能扩展时会异常艰难。基于上述的考虑和往常经验,没错,设计模式可以帮助我们解决这些问题。那么下面,我们先回顾下这些设计模式以及他们各自的优缺点。
设计模式组图摘自: MVC,MVP 和 MVVM 的图示
MVC模式:
视图(View):用户界面。
控制器(Controller):业务逻辑
模型(Model):数据获取模块
1、View 传送指令到 Controller
2、Controller 完成业务逻辑后,要求 Model 改变状态
3、Model 将新的数据发送到 View,用户得到反馈
MVP模式:
MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
1、 各部分之间的通信,都是双向的。
2、View 与 Model 不发生联系,都通过 Presenter 传递。
3、View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
基于上述MVC模式的优点,在实际开发中,MVP模式被更多的应用在项目开发过程中了。
MVVM模式:
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。
各模式在Android项目中的应用:
MVC模式:
View:对应于xml布局文件
Model:数据获取模块
Controllor:对应于Activity业务逻辑,数据处理和UI处理
在Android开发过程中,乍一看Activity还是比较符合MVC的设计模式的。Xml文件对应View层,负责UI的呈现,数据获取模块对应model层,而Activity作为Controller,负责处理数据逻辑之后,将数据交给UI层去展示。问题在于在此模式下,Xml作为View层的职责实在太弱,比如涉及到背景切换,文字大小颜色变化等等,都不能直接在xml中进行设置和修改,还是需要Activity中进行相应的切换。这样就导致,Activity作为View层+Controller层,其功能涵盖UI渲染和数据逻辑获取及处理,导致逻辑和UI 耦合严重,甚至经常出现上千行甚至几千行代码的情况,不利于代码的维护和后续扩展。
MVP模式:
View:对应于Activity/Fragment/自定义View,主要负责UI渲染。
Model:数据获取模块
Presenter: 负责数据处理以及View和Model的交互等,持有Model和View的引用。
在MVP模式中,View层只负责UI渲染,不再需要处理对应的业务逻辑,View层的量级大大轻化。而Presenter在获取到Model的数据,并进行处理后,通过View层暴露的接口调用去更新UI,这样View层和
Model层不互相持有引用,保证是隔离和解耦的。这样整个业务逻辑处理和视图的渲染是隔离的,就不会再出现上文提到的Activity/Fragment/View臃肿和混乱的问题。
但是MVP模式也会存在一系列的缺点:
1、Presenter层要处理的业务逻辑过多,复杂的业务逻辑会使P层非常庞大和臃肿。
2、Presenter通过接口方式持有View层引用,接口及接口中声明的方法粒度无法把握,可能需要声明大量接口以及接口中需要声明太多方法,而其中有些方法是否会用到以及是否会增加或删减还需要后续进一步确认。
3、Activity中需要声明大量跟UI相关的方法,而相应的事件通过Presenter调用相关方法来实现。两者互相引用和调用,存在耦合。一旦View层的UI视图发生改变,接口中的方法就需要改变,View层和P层同时都需要修改。
MVVM模式:
View:对应于Activity/Fragment/自定义View,主要负责UI渲染。
Model:数据获取模块
ViewModel: 负责业务逻辑处理,负责View和Model的交互。和View层双向绑定。
Mvvm模式是通过将View层和ViewModel层进行双向绑定, View层的变化会自动通知给ViewModel层,而ViewModel层的数据变化也会通知给View层进行相应的UI的更新。这样,Model层只负责暴露获取数据的方法,View层只负责监听数据的变化更新,而ViewModel负责接收View层的事件指令以及获取并处理数据。从而实现业务逻辑和Ui的隔离。
使用MVVM模式的优点:
1、低耦合度:
在MVVM模式中,数据处理逻辑是独立于UI层的。ViewModel只负责提供数据和处理数据,不会持有View层的引用。而View层只负责对数据变化的监听,不会处理任何跟数据相关的逻辑。在View层的UI发生变化时,也不需要像MVP模式那样,修改对应接口和方法实现,一般情况下ViewModel不需要做太多的改动。
2、数据驱动:
MVVM模式的另外一个特点就是数据驱动。UI的展现是依赖于数据的,数据的变化会自然的引发UI的变化,而UI的改变也会使数据Model进行对应的更新。ViewModel只需要处理数据,而View层只需要监听并使用数据进行UI更新。
3、异步线程更新Model:
Model数据可以在异步线程中发生变化,此时调用者不需要做额外的处理,数据绑定框架会将异步线程中数据的变化通知到UI线程中交给View去更新。
4、方便协作:
View层和逻辑层几乎没有耦合,在团队协作的过程中,可以一个人负责Ui 一个人负责数据处理。并行开发,保证开发进度。
5、易于单元测试:
MVVM模式比较易于进行单元测试。ViewModel层只负责处理数据,在进行单元测试时,测试不需要构造一个fragment/Activity/TextView等等来进行数据层的测试。同理View层也一样,只需要输入指定格式的数据即可进行测试,而且两者相互独立,不会互相影响。
6、数据复用:
ViewModel层对数据的获取和处理逻辑,尤其是使用Repository模式时,获取数据的逻辑完全是可以复用的。开发者可以在不同的模块,多次方便的获取同一份来源的数据。同样的一份数据,在版本功能迭代时,逻辑层不需要改变,只需要改变View层即可。
Databinding的使用:
(对databinding已经比较熟悉的同学可以直接忽略本章)
Google之前推出Android Mvvm模式的框架,Databinding。先来看下Databinding是如何实现Mvvm模式的数据绑定框架的。
build.gradle中添加:
android {
....
dataBinding {
enabled = true
}
}
先来看下布局文件:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
其中需要注意的是,Databinding的布局文件中是以layout为根节点的,然后通过
<data>
<variable name="user" type="com.example.User"/>
</data>
这种方式来声明类中需要使用的数据Model。
在layout中绑定数据时,
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
绑定的数据实体
使用@{}语句来将User的firstName属性和TextView进行绑定。
其中进行数据绑定的数据Model的属性是不可变的。
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
public class User {
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
在databinding时,上述两种写法是等价的。android:text="@{user.firstName}" 通过这种方式,android:text就会和User的firstName属性以及getFirstName()方法绑定。当然如果存在firstName()方法也是可以的。
数据绑定
默认情况下,databinding会根据layout文件的名字生成一个binding class。比如如果布局文件是main_activity.xml,就会生成一个MainActivityBinding的class。MainActivityBinding知道布局文件中所有的View属性以及他们的绑定关系(比如上文中的User),还有如何通过binding表达式对他们进行赋值。下面有个简单的示例:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
通过上面的方式就完成了User和View的绑定。很明显,你也可以通过下述方式获取整个layout的View。
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果你是在ListView 或者RecycleView的Adapter中bind Item,你可以通过如下方式获取:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
事件绑定
Databinding支持你写表达式的方式来处理从View分发出的事件(比如android:onClick)。事件的属性名字是有Listener的方法名决定的,需要保持一致。比如View.OnLongClickListener就有一个onLongClick()的方法,那么这个事件的属性就是android:onLongClick。
public class MyHandlers {
public void onClickFriend(View view) { ... }
public void onLongClickFriend(View view){...}
}
Binding表达式可以为View添加一个ClickListener:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onLongClick="@{handlers::onLongClickFriend}"
android:onClick="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>
需要注意的是表达式中的方法签名需要和Listener中的方法签名保持一直,否则是会报错的。
事件绑定是有两种方式的,Method References和Listener Bindings 。他们之间最大的差别就是,Method References的Listener实现创建是在数据被绑定时就完成的,而Listener Bindings 则是在相应事件触发的时候才会执行绑定表达式的操作。这里不再做过多介绍,感兴趣的可以直接去官方文档查看。
相应类的import
databinding支持像Java一样,直接引用其他的类。
<data>
<import type="com.example.MyStringUtils"/>
<import type="android.view.View"/>
<variable name="user" type="com.example.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
/>
Databinding支持表达式:
可以在layout中 像Java一样使用如下的一些表达式:
数学表达式 + – / * %
字符串链接 +
逻辑操作符 && ||
二元操作符 & | ^
一元操作符 + – ! ~
Shift >> >>> <<
比较 == > < >= <=
instanceof
Grouping ()
Literals – character, String, numeric, null
Cast
函数调用
值域引用(Field access)
通过[]访问数组里面的对象
三元操作符 ?:
示例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
Data Objects
POJO(plain old Java object)可以被用于databinding。但只是改变一个POJO并不会使UI发生变化。databinding是通过在数据变化时给出相应的通知来告知Ui层进行更新的。共有三种数据变化的机制,Observable objects, observable fields, and observable collections.
当他们中的任何一个与UI进行绑定,并且其数据属性发生变化时,都会通知到UI进行更新。
Observable Objects:
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
注意,此时User需要继承 BaseObservable ,相应属性获取方法需要添加注解@Bindable,而且在setValue时需要调用notifyPropertyChanged(BR.propertyName)。
ObservableFields:
如果你的Entity不想继承BaseObservable 以及做上述的那些操作,你也可以使用ObservableFields。
android.databinding.ObservableField
android.databinding.ObservableBoolean,
android.databinding.ObservableByte,
android.databinding.ObservableChar,
android.databinding.ObservableShort,
android.databinding.ObservableInt,
android.databinding.ObservableLong,
android.databinding.ObservableFloat,
android.databinding.ObservableDouble,
android.databinding.ObservableParcelable. ObservableFields
等等。使用时,在Data Class中创建一个Public final 的属性即可。
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
通过下述方式就可以设置并得到Data 属性的值。
user.firstName.set("Google");
int age = user.age.get();
使用set方法时,会实时通知到UI进行相应更新。
Observable Collections:
当key是引用类型,比如String时, android.databinding.ObservableArrayMap 是比较有用的。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在layout中,map可以通过String类型的Key获取到对应的Value
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text='@{user["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user["age"])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
key是整数时,可以使用android.databinding.ObservableArrayMap
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
自定义绑定
相信这时候大家就会有这个疑问了,那如果我给一些View添加了自定义的方法或者属性(官方不支持的方法),该如何进行绑定呢?是的,databinding当然也是支持自定义进行绑定。
我们先找一个官方支持的属性看时如何实现的,比如android:paddingLeft,是可以单独进行设置的,但是View的方法中,是没有setPaddingLeft的方法的,只有setPadding(left, top, right, bottom)方法。那么此时单独绑定paddingLeft就如下:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding adapters在其他类型的定制上是很有用的。当存在冲突时,开发者自己创建的binding adapter会覆盖默认的adpter。你也可以创建接收多个参数的adapter。
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
这个adapter只会在ImageView存在String类型的url值和Drawable类型的error值传入时才会触发执行。
当一个Listener有两个方法时,我们需要将他们拆成两个接口。比如 View.OnAttachStateChangeListener有两个方法,onViewAttachedToWindow() 和 onViewDetachedFromWindow() 我们需要需要为其创建两个不同的接口和Handler 方法。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因为改变一个Listener就会影响到另一个,所以我们需要建三个Binding Adapters,以便他们的方法可以被单独设置,也可以一起同时被设置。
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
Databinding的缺点:
综上我们介绍了Mvvm模式的优点,databinding是实现Mvvm模式的一种数据绑定框架,自然拥有上述优点,那么databinding有哪些缺点呢?
1、databinding的View层UI显示是在xml文件中实现的,当出现问题时,无法确定是Model的问题还是xml的问题,而且xml中也不方便调试。
2、基于数据驱动的databinding ,在比较大的模块时,Model层数据可能会比较大。而且在释放的时候不能像其他模式一样,不同View的数据无法根据各自View的生命周期来进行释放,而是在整个页面销毁时统一释放,这样就会占用更大内存。
3、View的复用。我们在项目中经常用到View的复用。而在databinding中 View在布局文件中可以绑定不同的model,在复用时除了要考虑View的复用之外,还需要考虑model的问题。
4、当然还有一个缺点。对于做前端开发的开发者来说,在布局文件中写对应的绑定数据和事件的逻辑是很正常的操作,但是对于移动开发者而言,这会是一个全新的体验,也许有人就无法接受这种情况。
ViewModel+LiveData+Repository实现MVVM的设计模式:
Google于I/O大会上提出了Android Architecture Components,里面提到的架构化组件可以更好的帮助我们去做一些框架的工作。而ViewModel的概念和 LiveData的可被观察性则让我看到了在Activity/Fragment/View(而不是xml文件)中实现双向绑定的可能性。
需要了解Android架构化组件的同学,可以查看:
Android Architecture Components
Android Architecture Components官方文档
那么使用ViewModel+LiveData+Repository 如何构建一个MVVM模式的项目呢?
下图为各模块关系图:
View层:
View层由Activity/Fragment/自定义View组成。在View层中,View不做具体的数据和逻辑处理操作,View层只会将数据和事件操作和ViewModel进行绑定。View 监听数据的变化,当ViewModel中的LiveData数据发生变化时,进行Ui的更新操作。同时,View层将事件绑定以命令的方式交给ViewModel去处理。总之,View层只做UI相关操作,逻辑处理数据获取等交给ViewModel。
ViewModel:
ViewModel是逻辑处理类,负责数据处理和View层与Model层的交互。ViewModel通过数据仓库Repository获取数据来源,处理来自View的事件命令,同时更新数据。
Repository:
数据仓库: 一个数据仓库负责获取同类型的数据来源。比如图书数据仓库能够获取各种条件筛选的图书数据,这份数据可以来自网络(Retrofit + OKHttp),本地Database(Room),缓存 (HashMap)等等,ViewModel在从Repository获取数据时,不需关注数据具体是怎么来的。
Model :
因为有了Repository的概念,所以这里的Model的定义相对简单,就是JavaBean,即网络请求对应的实体数据。
ViewModel的组成:
1、Context:
ViewModel因为需要通过Repository来异步获取相关数据,持有Context可以在异步请求结束时,判断Context是否已经销毁来决定是否取消回调。而ViewModel需要处理逻辑,一些工具类本身也需要Context。
2、Data Field 数据绑定(Observable Model):
在Databinding中,ViewModel会持有ObservableField等的数据,View监听ObservableField的数据变化,而当ViewModel中ObservableField数据变化时即会通知UI进行相应更新。在不使用databinding时,就可以通过LiveData起到相同的作用。
3、事件绑定:
View层的一些事件,比如onClick事件,OnRefresh事件等等,在事件触发时,通过ReplyCommand的方式,将事件传递给ViewModel层进行对应的逻辑操作。
事件绑定操作通过RxJava来实现。相对于使用Listener来说,使用RxJava更便捷,而且不会出现Listener回调方法中可能会返回View的引用。这样的话,就无法避免在ViewsModel中使用View的引用直接操作UI的问题。
4、数据仓库:
数据仓库负责获取数据来源。比如网络请求,数据库请求,本地文件等的数据的获取通过Repository来得到。
5、Model :
这里的model即为所需数据对应实体,不包含获取数据的接口方法和实现等。获取数据交给Repository,处理数据交给ViewModel。
6、childViewModel:
在一些情况下,ViewModel会持有childViewModel。比如Activity中嵌套Fragment,Activty对应ViewModel,而Fragment也有一个ViewModel。那么Activity的ViewModel是可以持有FragmentViewModelde 引用的。
如下为一个ViewModel的部分代码实现:
ViewModel中主要通过Repository来获取数据,并根据View层传递过来的事件指令操作数据,数据发生改变后,View层监听到变化并更新UI。通过这种方式,完成双向绑定和MVVM的设计流程。
关于ViewModel和Repository管理的相关设计类图如下:
ViewModelManger :
主要是对ViewModel的生命周期进行管理,一个CustomContext对应一个BaseViewModel,而一个CustomContext同时对应一个Activity/Fragment/View的生命周期。当View层生命周期结束时,就将Map中指定Key的BaseViewModel移除,并调用其onDestroy方法来完成相应数据销毁工作。
BaseRepository:数据仓库实现类。主要用于获取数据,包括但不限于网络数据,数据库,本地文件等等的数据来源。
BaseViewModel : 每个Activity/Fragment/View 对应一个BaseViewModel。从BaseRepository获取数据并根据View层Command事件处理数据并改变数据的值。用于View层和 Repository Model的交互以及数据逻辑的处理。
和Databinding相比,其优点在于:
1、Mvvm模式的优点其都具备。
2、Databinding不便于调试,而ViewModel+LiveData因为其双向绑定的操作都在Activity/Fragment/View中进行,不会出现像在xml中不便调试的问题。
3、会对ViewModel进行统一管理,在页面销毁时,销毁对应的ViewModel以避免造成内存占用。
4、还有一点,对于移动开发者而言,可能更易接受这种Mvvm模式的绑定方式,而不是在布局文件中直接进
行数据和事件的绑定操作。
本文通过对比几种不同的设计模式,并详细介绍了MVVM模式的两种实现方式,供大家了解和参考。不同的设计模式会有各自的优缺点,不同的项目应该按照各自的实际情况和使用体验来进行对应的设计。在选择的过程中需要考虑诸多问题,比如性能问题,使用便捷程度,单元测试,是否相互独立等。当然还有一点很重要的就是,共同开发的同事是否认同并喜欢这种开发方式,毕竟这会影响到他们的开发流程,习惯和体验。如何说服他们认可这种设计模式,就需要你提供便捷和健壮的封装框架,使他们很爽很开心很便捷地使用这种设计模式,并且能够满足项目代码的可扩展性,解耦和相关独立。做到这些,就能满足调用者,项目代码健壮性,模块独立性和可扩展性的需要了。