为什么要先介绍MVC?
如果你要想更佳深刻的理解MVP,并在实际开发中灵活的应用,那么就要先了解它的低配版MVC,他俩只是一步之遥,先了解MVC再学习MVP,MVP的优势才能凸显出来,这样连贯性的学习才会加深对MVP的理解。
目录
MVP那些事儿(5)……中介者模式与MVP的关系【知识点】
MVP那些事儿(6)……MVC变身为MVP
MVP那些事儿(7)……Kotlin实现MVP【知识点】
MVP那些事儿(8)……当MVP遇到Lifecycle【知识点】
MVP那些事儿(9)……探究MVP的最佳实践
MVP那些事儿(10)……MVVM双向绑定
MVP那些事儿(11)……基于MVVM的Architecture Components
回顾MVP那些事儿(2) 初探MVC架构的内容:
MVC的作用?它是设计模式吗?是框架吗?是架构吗?
MVC各个层的定义和职责
通过上一篇的内容,大家也对MVC已经有了一个大致的了解,在开启这一章内容前,希望大家能先阅读上一篇的内容,否则可能会“断片”。
在上一篇中,我们详细的介绍了MVC三个层的基本职责,我们再回顾一下这三个“片段”:
Model层
1、JavaBean不是Model,但Model也可以包含JavaBean的职责,但不是必须的。
2、Model是用来处理数据的,如获取数据(本地或者服务端),数据处理,如CURD。
View层
1、它的主要职责为呈现Model的数据、主动询问状态或被动的监听
2、通知控制器Controller去处理一些事情
3、接收Controller,编辑自己与Model无关的状态
Controller
1、接收View的操作,并转调给Model
2、改变View的状态
同时还遗留了两个问题:
1、如何把这三个片段组装起来?在Android里怎么写?
2、view在执行tasksRepository.getTaskCache()时,是怎么知道tasksRepository数据已经准备好了呢?怎么通知view的?
把片段“组合”起来
在模式设计时,我们会通过关系图来反映对象间的关系,如下图:
这是一张非常经典的MVC架构图(手残,有点丑……),相信大家对这张图已经不陌生了,OK,这张图我先放下不表,因为考虑到我们介绍的是架构,本身是抽象的,加上如此抽象的图,怕是不能好好的切入,所以我想尽可能的巨像化这个过程,当大家有一个清晰的理解后,再回过头来重新审视它。我本来是想放在文章的后半段祭出这张图的,但想了想有点不妥,原因是我们上期已经通过一整篇用来初探MVC,也该把MVC的架构图贴出来应一应景了……
那么MVC这三者的关系究竟是怎样的呢?
我们依旧通过现实中的场景来描述一下MVC三者的关系,让大家对这三者的关系有一个初期的直观的理解,起到一个抛砖引玉的效果,废话不多,场景开始。
场景
想必大家都租过房子,有当租客的体验,也有一部分人可能还有当过过房东经验,在当下的社会中,房屋中介是我们租房找房必不可少的环节,我相信大部分人都和中介打过交道,咳咳,那我们就还原一下租房的过程,
首先,确定角色
在这个场景里包含了三个角色,租客、中介、和房东,
其次,选择角度
我们从租户的角度描述一下租房的场景,只考虑正向流程。
再次,场景描述
租客要先找到中介并描述自己需要的房子,中介接到需求后,通过筛选发现有一个房东很适合,于是中介开始与这个房东接触,询问一些和房子租金相关的事宜,如果中介觉的合适,他会把反馈告知客户,如果客户也觉的合适,中介就会把租客和房东约在店里进行更进一步的协商,如果能促成合同,那么到此为止中介完成了现阶段的工作,剩下的事情,比如租客向房东每个季度缴纳房租,或者询问房东家里电器的使用方式,亦或者房东询问房客租约到期是否续租等等,这些沟通都可以抛开中介去完成,也就是说,不一定需要中介作为他们之间沟通的桥梁,但也并不意味着中介在以后的场景中是不可用的,比如,在租房过程中有一些事情租客和房东有分歧,也可以通过中介来进行协调和沟通。
还有一种中介的形式是,租客和房东是见不到彼此的,全程由中介负责,这种房叫托管房,租客不知道房东是谁,房东也不知道租客是谁,合同都和中介签,我们现在不讨论这种情况。
来个图描述一下上诉的情景
这张图的目的一来是复述一下上面的场景描述,二来是让大家直观的看到它们三者交互的一个关系。
中介、租客、房东,把他们和MVC的三个角色关联起来
通过上诉场景的描述,和前一篇帖子中介绍的MVC各个对象的职责,想必他们到底谁是谁一目了然,我们试着关联一下这几个对象
首先看中介,他负责了执行租客的招租要求,并告知房东这些要求,也就是说他作为中介向房东转告租客的需求,那中介就是我们的Controller。
再次看租客,他是需求的发出者,他比其他几个对象更积极的提出需求,就好比View,总是再向Controller提出需求,也会更主动的向Model询问状态,就像租客一样,遇到问题,要么找中介,要么去询问房东,所以,在这里把租客看作是View更加的贴切。
最后只剩下房东这个角色了,最后的Model就分配给房东吧。
接下来我们用MVC的方式来梳理一下中介、租客、房东的关系
1、View To Controller 求租房
2、Controller To Model 询问:出租吗?
3、Controller To View 告知:找到房源
4、View To Model 单聊:能便宜吗?
5、Model To View 单聊:没门儿!
上面这五张图,是把MVC三个对象间的关系,以及案例中的业务流程做了一个“融合”,是一个抽象到具象的“融合”,架构图中这些带箭头的线,是用来解释对象间关系的,无非就是那些谁依赖谁的解释,我同时又在线上加入了一些事件,是希望能把上面具象的问题引到抽象的架构中来,在实际中,这样的融合是不符合常理的,比如你无法用单个业务场景去定义对象间的依赖关系,比如View To Controller,难道只有求租房这个场景才可以这么用吗?显然不是的,包括各个角色中括号后面的称呼。所以,为了解决通用性的问题,人们把这样的具象问题抽象成了一种依赖关系。
架构是蓝图,抽象的存在,而框架是具象的,它是为了解决具体某一类问题而设计的。
MVC架构图是怎么来的?抛去具象,看本质
抽象到最后就是我们开篇的第一张图。
在某些场合里,需要我们去介绍我们的软件架构,这就需要我们具备描述架构的能力,我觉的可以先描述一下具象的问题,再描述向上抽离的步骤,最后陈述抽象的产物,也就是我们的架构,是一个不错的思路。
应用到实际中
通过上面大篇幅的抛砖引玉,相信大家对MVC的架构有了一个初步的认识,那么接下来我们开始真正的使用,还记得上一篇我们使用了一个需求:
需求:列表加载数据并且展示
我们依旧使用这个需求,因为前面定义好了三个层的类,接下来要做的是按照MVC的架构把他们拼装起来:
在Android中使用
开始加入到实际开发阶段,我们先创建一个Activity
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
紧接着,我们把上一篇定义好的三个MVC对象也加入进来
Model :TasksRepository
/**我是一个Model**/
public class TasksRepository {
//从服务器请求获取数据
void getTasks() {}
//从内存缓存获取数据
Data getTaskCache() {}
//从磁盘缓存获取数据
Data getTaskDiskCache(){}
//保存一条数据
boolean saveTask(Task task) {}
//对数据进行排序
Data orderData(Data data, int orderType){}
}
View :TasksView
/**我是一个View**/
public class TaskView {
//当列表初始化后,告诉控制器该加载数据了
void viewCreate() {}
//更新列表
void upDateList() {}
//just for ui
void beginLoadData(){}
}
Controller :TasksController
/**我是一个Contorller**/
public class TasksController {
void loadNomData() {}
}
现在我们有了Activity,TasksRepository、TasksView、TasksController
有了这些对象,我们就可以进行组合了,我们从什么地方下手呢?也许我们可以从MVC的架构图中得到些启示,虽然架构图很简单,但是有箭头,有方向,在UML中,这些线和箭头便是记录着对象间的关系:
按照架构图的提示,我们为TasksRepository、TasksView、TasksController这三个类梳理了一个大致的依赖关系,同时MainActivity被撂到了一边,并不是它和这三个对象没有关系,而是它现在还不能直接用,它需要变一下,为什么这么说呢,来看例子:
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化MVC
TasksController controller = new TasksController();
TasksView view = new TasksView();
TasksRepository model = new TasksRepository();
//依赖关系
controller.setView(view);
view.setController(controller);
view.setModel(model);
model.setController(controller);
model.setView(view)
//执行业务
controller.loadNomData();
}
}
我承认,我又开始抛砖引玉了,从这种写法来看,Activity到是像MVC的容器,它包含了MVC这三个对象,这么写有问题吗?在继续之前,引用一个概念,Is a…… and Has a
Is a 与 Has a
翻译过来,就是,我是什么,和我有什么
Is a 我是什么?
//我是什么动物?我是一只猫
class Animal implments Cat {
//喵喵叫
void miaomiao();
}
Has a 我有什么?
//我有什么动物?我有一只猫,一只狗,一只豹子
class Animal{
public Animal(Cat cat) {
//来只猫
this.cat = cat;
//来个豹子
this.baozi = new Baozi();
}
//来只狗
void setDog(Dog dog) {
this.dag = dog;
}
}
相信大家看出了其中的差别,回过头我们看一下MainActivity,它是什么,它又有什么,首先它是一个Activity,其次,它有TasksRepository、TasksView、TasksController这三个对象,重点在Has a上,像这样的依赖耦合度是非常高的,基本没有扩展的余地,为了降低耦合,可不可以让MainActivity少几个Has a?当然可以。
我是一只猫,我有一只狗和豹子
//我是什么动物?我是一只猫
class Animal implments Cat {
//喵喵叫
void miaomiao();
public Animal() {
//来个豹子
this.baozi = new Baozi();
}
//来只狗
void setDog(Dog dog) {
this.dag = dog;
}
}
所以我们让MainActivity变一下身,从MVC这三个对象TasksRepository、TasksView、TasksControlle中选一个变,这样强耦合的个数从3个变成了2个,那么选谁好呢?
目前主流的是选择View,和选择Controller,大家都是从业务的角度出发去选择,首先选择View,因为Activity里包含了布局对象,所以因为“离得近”让Activity变成View,选择Controller的是因为Activity本身的生命周期可以很好的用来控制业务流程,这样对Controller来说更加有利,说实话,我并没有深挖其中哪一个更好,因为我觉的这两种方案都有他们的侧重,还是要从实际的业务角度去考虑问题,那我们突发奇想让Activity既是View,又是Controller呢?这不就解决变成谁的问题了吗?个人觉的还是不要的好,你总不能介绍自己的时候说:大家好,我是猫,也是狗,我是男的也是女的,我相信到时候场面一定很混乱,我们还是保持类的职责单一性吧。但没准那天变成Model玩一下也可以的吧,所以说,目前我先从变成View来开始,后续的章节我们会讲解变成Controller该怎么办。
更新UML
同时更新一下代码,我们让MainActivity实现一个TasksView,赋予它View的职责
public class MainActivity extends AppCompatActivity implments TasksView{
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//略……
}
}
确定了Activity,我们才开始了第一步,接下来我们就要看一下Controller的初始化,在此之间需要引用一个概念,依赖注入。
依赖注入
什么是依赖注入?首先解释一下什么是依赖,看下面的代码:
class Animal{
public Animal(Cat cat) {
//来只猫
this.cat = cat;
//来个豹子
this.baozi = new Baozi();
}
//来只狗
void setDog(Dog dog) {
this.dag = dog;
}
//猫猫叫一下
void miaomiao() {
cat.miaomiao();
}
//豹子叫一下
void aoao() {
baozi.called();
}
}
aoao这个方法内部,调用了baozi这个对象叫的方法called(),也就是说,首先Animal这个对象自己是不会叫的,他需要依赖于Baozi对象,Animal依赖Baozi去叫,这个依赖就是Baozi,我们看到Baozi对象的初始化,是在Animal构造器中new出来的。再看一下miaomiao方法内部,依旧是依赖于cat的miaomiao方法去叫,而这个cat对象是从构造函数传进来的,baozi和cat同样是Animal的依赖,但获得的途径不同,cat的获得途径就是注入,而它又是依赖,所以我们称之为依赖注入,依赖注入的形式除了通过构造器参数外,还可以通过普通方法传入,如上面的setDog方法,我们注入了一只Dog。
依赖注入的好处
说到依赖注入的好处,不得不提Java的面向对象的特性中的多态,举个例子,上面的代码中的Cat可以是一个接口,我们在使用Animal类前,可以确定一下Cat的实现类,根据业务需求,接收到的可能是公猫,也可能是母猫,可能是美短,也可能是英短(猫的品种),如果我们把Cat的依赖放在Animal中去初始化,那么根据业务的变化,可能需要频繁的更改Animal类。第二点好处,当Cat的构造器发生变化,Animal也需要更改Cat,如果Cat需要另外的依赖,比如猫需要猫粮它才能活下去,那么Animal也要间接的依赖猫粮,对象间的耦合性就暴露了出来,所以从外部传入的依赖,Animal无需关心依赖的依赖。
看来依赖注入是非常好的设计手段,那么回到上面的问题,Contorller的初始化,我们知道Controller是依赖于View的,所以我们需要把View注入到Controller中去。
public class MainActivity extends AppCompatActivity implments TasksView{
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化Controller,this就是View,通过构造器注入
TasksController controller = new TasksController(tasksView:this);
}
}
通过依赖注入,我们把Controller和View的命运绑定在了一起。
说完了View -> Controller,我们还要解决Controller -> View,其实我们可以发现,Controller就是包含在View中的,因为Activity在上面已经变身成了View,而Controller也在Activity中通过new的方式进行了初始化,也就顺理成章的成了View的依赖了,只不过它不是通过注入的方式进入到View(MainActivity)中的,那么有什么办法可以做到这一点呢?当然有办法了,介绍一下依赖注入的第三种方式:通过构建一个Controller的工厂来负责生产Controller的实例,具体就不在这里细说了,以后讲到Dagger时可以和大家详细讨论。
Model的初始化
那么Model(TasksRepository)的初始化放在哪里去做呢?以及它与Controller和View的关系又是怎样的呢?从MVC的架构图和框架的UML图里可以得到答案,Controller和Model之间是一个单向的依赖,也就是Controller -> Model,同时Model与View是一个双向的依赖,
1、Controller -> Model
2、Model -> View
3、View -> Model
我们只需满足这三个条件,我们直接这样,看代码:
public class MainActivity extends AppCompatActivity implments TasksView{
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化Controller,this就是View,通过构造器注入
TasksController controller = new TasksController(tasksView:this);
//初始化Model,Model -> View and View -> Model
TasksRepository model = new TasksRepository(tasksView:this);
//Controller -> Model
model.setController(controller);
}
}
首先在Activty中,初始化Model,并将View注入到Model里,这样,同时满足了Model -> View 和 View -> Model,其次,通过model的setController方法将上面创建好的Controller注入到Model中,也就是满足了Controller -> View 这个条件。
阶段性总结
到此我们就完成了MVC片段的组合,我们从头再梳理一下这个过程
实现步骤:
1、Is View,Activity实现View
将Activity赋予View的职责。
2、Controller -> View ,在Activity中创建Controller的实例。
通知控制器Controller去处理一些事情
接收View的操作,并转调给Model
3、View -> Controller,将View注入到Controller。
接收Controller,编辑自己与Model无关的状态
4、Model -> View,在Activity中创建Model的实例。
主动询问Model的状态,呈现Model的数据
5、View -> Model,将View注入到Model。
监听Model的变化,如有变化通知View
6、Controller -> Model,将Contoller注入到View
监听Model的变化,如果变化通知Controller
心路历程
通过MVC的架构图,我们尝试的设计了UML,并且通过Is a ,Has a原则确定了View的归属,其次,通过依赖注入原则,确定了依赖创建方式,再次,通过MVC的架构图与UML确立了Controller,View, Model这三者之间的关系,再结合依赖注入原则完成了整个框架的搭建。
总结
在之前的介绍MVC各个层的职责中,“监听”这个词出现的频率很高,在面向对象语言的设计中,“监听”是一个动作,与它相反的便是“获取”这个动作了。目前这个框架还是个雏形,并没有“监听”的能力,同时包括各个层代码的不完善,以及控制反转等实际性内容的缺失…… 本来想用一章来写完,但篇幅有些长,看来不得不分了。以前总觉的看别人的帖子挺快的,不一会儿就看完了,但真正自己去码可是真的慢,主要是担心描述的不够清晰,担心不能把自己的想法很清晰的表达出来,同时又担心过于清晰的表达会不会又显得啰里八嗦,删了改,改了删的,总之,我会尽最大努力完成这个系列的,喜欢的就点个赞,鼓励鼓励我。
最后的最后,布置一个家庭作业,看下面三张图:
问题1,图一和图二的区别,以及图二算不算MVC
问题2、MVC可以像图三那样去设计吗?