理想和现实的差距

在一个足够复杂的场景下,如果能践行Model与UI的依赖关系,程序的可测性(React还是谁来着,也管它叫Predictable,可预测)就有了一定的保障。

但是,很多情况下,没有那么理想,比如

  • 很多Model被展现一次就没事儿了,压根儿就没有动态修改
  • 很多Model只被在一处展现,因此它动态修改的时候,在UI改和在Model里改,工作量是一样的
  • UI的调整并没有那么理想化,无法解释为纯UI的问题,几乎每次调整都涉及到业务逻辑的调整
  • 无所谓视图逻辑和业务逻辑,我们认为展现形式是业务逻辑的一部分,并不是什么卵的视图逻辑

Declarative vs. Imperative:命令式编程与声明式编程

three-ds-of-web-development

前端攻略-从路人甲到英雄无敌二:JavaScript
与不断演化的框架

形象地来描述命令式编程与声明式编程的区别,就好像C#/JavaScript与类似于XML或者HTML这样的标记语言之间的区别。命令式编程关注于
how to do what you want done
,即事必躬亲,需要安排好每个要做的细节。而声明式编程关注于 what you want
done without worrying about how
,即只需要声明要做的事情而不用将具体的过程再耦合进来。对于开发者而言,声明式编程将很多底层的实现细节向开发者隐藏,而使得开发者可以专注于具体的业务逻辑,同时也保证了代码的解耦与单一职责。譬如在Web开发中,如果你要基于jQuery将数据填充到页面上,那么大概按照命令式编程的模式你需要这么做:

var options = $("#options");
$.each(result, function() {
    options.append($("<option />").val(this.id).text(this.name));
});

而以Angular 1声明式的方式进行编写,那么是如下的标记模样:

<div ng-repeat="item in items" ng-click="select(item)">{{item.name}}
</div>

而在iOS和Android开发中,近年来函数响应式编程(Functional Reactive
Programming)也非常流行,参阅笔者关于响应式编程的介绍可以了解,响应式编程本身是基于流的方式对于异步操作的一种编程优化,其在整个应用架构的角度看更多的是细节点的优化。以 RxSwift 为例,通过响应式编程可以编写出非常优雅的用户交互代码:

let searchResults = searchBar.rx_text
    .throttle(0.3, scheduler: MainScheduler.instance)
    .distinctUntilChanged()
    .flatMapLatest { query -> Observable<[Repository]> in
        if query.isEmpty {
            return Observable.just([])
        }

        return searchGitHub(query)
            .catchErrorJustReturn([])
    }
    .observeOn(MainScheduler.instance)
searchResults
    .bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) {
        (index, repository: Repository, cell) in
        cell.textLabel?.text = repository.name
        cell.detailTextLabel?.text = repository.url
    }
    .addDisposableTo(disposeBag)

其直观的效果大概如下图所示:

图片 1

到这里可以看出,无论是从命令式编程与声明式编程的对比还是响应式编程的使用,我们开发时的关注点都慢慢转向了所谓的数据流。便如MVVM,虽然它还是双向数据流,但是其使用的Data-Binding也意味着开发人员不需要再去以命令地方式寻找元素,而更多地关注于应该给绑定的对象赋予何值,这也是数据流驱动的一个重要体现。而Unidirectional
Architecture采用了类似于Event
Source的方式,更是彻底地将组件之间、组件与功能模块之间的关联交于数据流操控。

3、如何构建MVVM应用程序

构建MVVM框架首先要具体了解各个模块的分工,接下来我们来讲解View,ViewModel,Model
的它们各自的职责所在。

  • ViewView层做的就是和UI相关的工作,我们只在XML和Activity或Fragment写View层的代码,View层不做和业务相关的事,也就是我们的Activity
    不写和业务逻辑相关代码,也不写需要根据业务逻辑来更新UI的代码,因为更新UI通过Binding实现,更新UI在ViewModel里面做(更新绑定的数据源即可),Activity
    要做的事就是初始化一些控件(如控件的颜色,添加 RecyclerView
    的分割线),Activity可以更新UI,但是更新的UI必须和业务逻辑和数据是没有关系的,只是单纯的根据点击或者滑动等事件更新UI(如
    根据滑动颜色渐变、根据点击隐藏等单纯UI逻辑),Activity是可以处理UI事件,但是处理的只是处理UI自己的事情,View层只处理View层的事。简单的说:View层不做任何业务逻辑、不涉及操作数据、不处理数据、UI和数据严格的分开。
  • ViewModelViewModel层做的事情刚好和View层相反,ViewModel
    只做和业务逻辑和业务数据相关的事,不做任何和UI、控件相关的事,ViewModel
    层不会持有任何控件的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。ViewModel就是专注于业务的逻辑处理,操作的也都是对数据进行操作,这些个数据源绑定在相应的控件上会自动去更改UI,开发者不需要关心更新UI的事情。DataBinding
    框架已经支持双向绑定,这使得我们在可以通过双向绑定获取View层反馈给ViewModel层的数据,并进行操作。关于对UI控件事件的处理,我们也希望能把这些事件处理绑定到控件上,并把这些事件统一化,方便ViewModel对事件的处理和代码的美观。为此我们通过BindingAdapter
    对一些常用的事件做了封装,把一个个事件封装成一个个Command,对于每个事件我们用一个ReplyCommand<T>去处理就行了,ReplyCommand<T>会把可能你需要的数据带给你,这使得我们处理事件的时候也只关心处理数据就行了,具体见MVVM
    Light Toolkit 使用指南的 Command 部分
    。再强调一遍ViewModel
    不做和UI相关的事。
  • **Model **Model 的职责很简单,基本就是实体模型同时包括Retrofit
    的Service ,ViewModel 可以根据Model
    获取一个Bean的Observable<Bean>,然后做一些数据转换操作和映射到ViewModel
    中的一些字段,最后把这些字段绑定到View层上。

关于协作,我们先来看下面的一张图:

图片 2
1

上图反应了MVVM框架中各个模块的联系和数据流的走向,由上图可知View和Model
直接是解耦的,是没有直接联系的,也就是我之前说到的View
不做任何和业务逻辑和数据处理相关的事。我们从每个模块一一拆分来看。那么我们重点就是下面的三个协作。

  • ViewModel与View的协作
  • ViewModel与Model的协作

  • ViewModel与ViewModel的协作

  • ViewModel与View的协作

图片 3
2

图 2 中ViewModel 和View
是通过绑定的方式连接在一起的,绑定的一种是数据绑定,一种是命令绑定。数据的绑定
DataBinding
已经提供好了,简单的定义一些ObservableField就能把数据和控件绑定在一起了(如TextView的text属性),但是DataBinding框架提供的不够全面,比如说如何让一个URL绑定到一个ImageView让这个ImageView能自动去加载url指定的图片,如何把数据源和布局模板绑定到一个ListView,让ListView可以不需要去写Adapter和ViewHolder
相关的东西,而只是通过简单的绑定的方式把ViewModel的数据源绑定到Xml的控件里面就能快速的展示列表呢?这些就需要我们做一些工作和简单的封装。MVVM
Light Toolkit 已经帮我们做了一部分的工作,详情可以查看MVVM Light
Toolkit 使用指南
。关于事件绑定也是一样,MVVM Light Toolkit
做了简单的封装,对于每个事件我们用一个ReplyCommand<T>去处理就行了,ReplyCommand<T>会把可能你需要的数据带给你,这使得我们处理事件的时候也只关心处理数据就行了。

图 1
中ViewModel的模块中我们可以看出ViewModel类下面一般包含下面5个部分:

  • Context
  • Model
  • Data Field
  • Command
  • Child ViewModel (子ViewModel)

我们先来看下示例代码,然后在一一讲解5个部分是干嘛用的:

//contextprivate Activity context;//modelprivate NewsService.News news;private TopNewsService.News topNews;//数据绑定(data field)public final ObservableField<String> imageUrl = new ObservableField<>();public final ObservableField<String> html = new ObservableField<>();public final ObservableField<String> title = new ObservableField<>();// 一个变量包含了所有关于View Style 相关的字段public final ViewStyle viewStyle = new ViewStyle();//命令绑定public final ReplyCommand onRefreshCommand = new ReplyCommand<> -> { })public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<> -> { });//Child ViewModelpublic final ObservableList<NewItemViewModel> itemViewModel = new ObservableArrayList<>();/** * ViewStyle 关于控件的一些属性和业务数据无关的Style 可以做一个包裹,这样代码比较美观,ViewModel 页面也不会有太多的字段。 **/public static class ViewStyle { public final ObservableBoolean isRefreshing = new ObservableBoolean; public final ObservableBoolean progressRefreshing = new ObservableBoolean;}
  • ContextContext
    是干嘛用的呢,为什么每个ViewModel都最好需要持了一个Context的引用呢?ViewModel
    不做和UI相关的事,不操作控件,也不更新UI,那为什么要有Context呢?原因主要有以下两点,当然也有其他用处,调用工具类、帮助类可能需要context参数等:
  • 通过图1中,我们发现ViewModel 通过传参给Model
    然后得到一个Observable<Bean>,其实这就是网络请求部分,做网络请求我们必须把Retrofit
    Service返回的Observable<Bean>绑定到Context的生命周期上,防止在请求回来时Activity已经销毁等异常,其实这个Context的目的就是把网络请求绑定到当前页面的生命周期中。
  • 在图1中,我们可以看到两个ViewModel
    之间的联系是通过Messenger来做,这个Messenger
    是需要用到Context,这个我们后续会讲解。
  • ModelModel
    是什么呢,其实就是数据原型,也就是我们用Json转过来的Java
    Bean,我们可能都知道,ViewModel要把数据映射到View中可能需要大量对Model的数据拷贝,拿Model
    的字段去生成对应的ObservableField(我们不会直接拿Model的数据去做展示),这里其实是有必要在一个ViewModel
    保留原始的Model引用,这对于我们是非常有用的,因为可能用户的某些操作和输入需要我们去改变数据源,可能我们需要把一个Bean
    从列表页点击后传给详情页,可能我们需要把这个model
    当做表单提交到服务器。这些都需要我们的ViewModel持有相应的model。

  • Data Field Data Field
    就是需要绑定到控件上的ObservableField字段,
    无可厚非这是ViewModel的必须品。这个没有什么好说,但是这边有一个建议:这些字段是可以稍微做一下分类和包裹的,比如说可能一些字段绑定到控件的一些Style属性上(如果说:长度,颜色,大小)这些根据业务逻辑的变化而动态去更改的,对于着一类针对View
    Style的的字段可以声明一个ViewStyle类包裹起来,这样整个代码逻辑会更清晰一些,不然ViewModel里面可能字段泛滥,不易管理和阅读性较差。而对于其他一些字段,比如说title,imageUrl,name这些属于数据源类型的字段,这些字段也叫数据字段,是和业务逻辑息息相关的,这些字段可以放在一块。

  • Command Command
    说白了就是对事件的处理(下拉刷新,加载更多,点击,滑动等事件处理),我们之前处理事件是拿到UI控件的引用,然后设置Listener,这些Listener
    其实就是Command,但是考虑到在一个ViewModel 写各种Listener
    并不美观,可能实现一个Listener就需要实现多个方法,但是我们可能只想要其中一个有用的方法实现就好了。同时实现Listener
    会拿到UI的引用,可能会去做一些和UI相关的事情,这和我们之前说的ViewModel
    不持有控件的引用,ViewModel不更改UI
    有相悖。更重要一点是实现一个Listener
    可能需要写一些UI逻辑才能最终获取我们想要的,简单一点的比如说,你想要监听ListView滑到最底部然后触发加载更多的事件,这时候你就要在ViewModel里面写一个OnScrollListener,然后在里面的onScroll方法中做计算,计算什么时候ListView滑动底部了,其实ViewModel的工作并不想去处理这些事件,它专注做的应该是业务逻辑和数据处理,如果有一个东西它不需要你自己去计算是否滑到底部,而是在滑动底部自动触发一个Command,同时把当前列表的总共的item数量返回给你,方便你通过
    page=itemCount/LIMIT+1去计算出应该请求服务器哪一页的数据那该多好啊。MVVM
    Light Toolkit 帮你实现了这一点:

public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((itemCount) -> { int page=itemCount/LIMIT+1; loadData(page.LIMIT)});

接着在XML 布局文件中通过bind:onLoadMoreCommand绑定上去就行了

<android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>

具体想了解更多请查看 MVVM Light Toolkit
使用指南
,里面有比较详细的讲解Command的使用。当然Command并不是必须的,你完全可以依照你的习惯和喜好在ViewModel
写Listener,不过使用Command 可以使你的ViewModel
更简洁易读,你也可以自己定义更多的Command,自己定义其他功能Command,那么ViewModel的事件处理都是托管ReplyCommand<T>来处理,这样的代码看起来会特别美观和清晰。

  • Child ViewModel (子ViewModel)子ViewModel
    的概念就是在ViewModel
    里面嵌套其他的ViewModel,这种场景还是很常见的。比如说你一个Activity里面有两个Fragment,ViewModel
    是以业务划分的,两个Fragment做的业务不一样,自然是由两个ViewModel来处理,Activity
    本身可能就有个ViewModel
    来做它自己的业务,这时候Activity的这个ViewModel里面可能包含了两个Fragment分别的ViewModel。这就是嵌套的子ViewModel。还有另外一种就是对于AdapterView
    如ListView RecyclerView,ViewPager等。
//Child ViewModelpublic final ObservableList<ItemViewModel> itemViewModel = new ObservableArrayList<>();

它们的每个Item 其实就对应于一个ViewModel,然后在当前的ViewModel
通过ObservableList<ItemViewModel>持有引用,这也是很常见的嵌套的子ViewModel。我们其实还建议,如果一个页面业务非常复杂,不要把所有逻辑都写在一个ViewModel,可以把页面做业务划分,把不同的业务放到不同的ViewModel,然后整合到一个总的ViewModel,这样做起来可以使我们的代码业务清晰,简短意赅,也方便后人的维护。

总得来说ViewModel 和View
之前仅仅只有绑定的关系,View层需要的属性和事件处理都是在xml里面绑定好了,ViewModel层不会去操作UI,只会操作数据,ViewModel只是根据业务要求处理数据,这些数据自动映射到View层控件的属性上。关于ViewModel类中包含哪些模块和字段,这个需要开发者自己去衡量,这边建议ViewModel
不要引入太多的成员变量,成员变量最好只有上面的提到的5种(context、model、…),能不进入其他类型的变量就尽量不要引进来,太多的成员变量对于整个代码结构破坏很大,后面维护的人要时刻关心成员变量什么时候被初始化,什么时候被清掉,什么时候被赋值或者改变,一个细节不小心可能就出现潜在的Bug。太多不清晰定义的成员变量又没有注释的代码是很难维护的。

2016
8月25日更新
:我们会把UI控件的属性和事件都通过xml里面(如bind:text=@{…})绑定,但是如果一个业务逻辑要弹一个Dialog,但是你又不想在ViewModel里面做弹窗的事(ViewModel
不做UI相关的事)或者说改变ActionBar上面的图标的颜色,改变ActionBar按钮是否可点击,这些都不是写在xml里面(都是用java
初始化话),如何对这些控件的属性做绑定呢?我们先来看下代码:

public class MainViewModel implements ViewModel {....//true的时候弹出Dialog,false的时候关掉dialogpublic final ObservableBoolean isShowDialog = new ObservableBoolean();.........}// 在View层做一个对isShowDialog改变的监听public class MainActivity extends RxBasePmsActivity {private MainViewModel mainViewModel;@Overrideprotected void onCreate(Bundle savedInstanceState) {..... mainViewModel.isShowDialog.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(android.databinding.Observable sender, int propertyId) { if (mainViewModel.isShowDialog.get { dialog.show(); } else { dialog.dismiss; } ...}

简单的说你可以对任意的ObservableField做监听,然后根据数据的变化做相应UI的改变,业务层ViewModel
只要根据业务处理数据就行,以数据来驱动UI。

  • ViewModel与Model的协作 从图1 中,Model 是通过Retrofit
    去获取网络数据的,返回的数据是一个Observable<Bean>,Model
    层其实做的就是这些。那么ViewModel
    做的就是通过传参数到Model层获取到网络数据然后把Model的部分数据映射到ViewModel的一些字段(ObservableField),并在ViewModel
    保留这个Model的引用,我们来看下这一块的大致代码(代码涉及到简单RxJava,如看不懂可以查阅入门一下):

 //Model private NewsDetail newsDetail; private void loadData { // Observable<Bean> 用来获取网络数据 Observable<Notification<NewsDetailService.NewsDetail>> newsDetailOb = RetrofitProvider.getInstance() .create(NewsDetailService.class) .getNewsDetail .subscribeOn(Schedulers.io .observeOn(AndroidSchedulers.mainThread // 将网络请求绑定到Activity 的生命周期 .compose(((ActivityLifecycleProvider) context).bindToLifecycle //变成 Notification<Bean> 使我们更方便处理数据和错误 .materialize; // 处理返回的数据 newsDetailOb.filter(Notification::isOnNext) .map(n -> n.getValue // 给成员变量newsDetail 赋值,之前提到的5种变量类型中的一种 .doOnNext(m -> newsDetail = m) .subscribe(m -> initViewModelField; // 网络请求错误处理 NewsListHelper.dealWithResponseError( newsDetailOb.filter(Notification::isOnError) .map(n -> n.getThrowable;}//Model -->ViewModelprivate void initViewModelField(NewsDetail newsDetail) { viewStyle.isRefreshing.set; imageUrl.set(newsDetail.getImage; Observable.just(newsDetail.getBody .map(s -> s + "<style type="text/css">" + newsDetail.getCssStr .map(s -> s + "</style>") .subscribe(s -> html.set; title.set(newsDetail.getTitle; }

以上代码基本把注释补全了,基本思路比较清晰,,Rxjava涉及的操作符都是比较基本的,如有不懂,可以稍微去入门,之后的源码里面ViewModel数据逻辑处理都是用Rxjava做,所以需要提前学习一下方便你看懂源码。

注:我们推荐使用MVVM 和
RxJava一块使用,虽然两者皆有观察者模式的概念,但是我们RxJava不使用在针对View的监听,更多是业务数据流的转换和处理。DataBinding框架其实是专用于View-ViewModel的动态绑定的,它使得我们的ViewModel
只需要关注数据,而RxJava
提供的强大数据流转换函数刚好可以用来处理ViewModel中的种种数据,得到很好的用武之地,同时加上Lambda表达式结合的链式编程,使ViewModel
的代码非常简洁同时易读易懂。

  • ViewModel与ViewModel的协作 在图 1 中 我们看到两个ViewModel
    之间用一条虚线连接着,中间写着Messenger,Messenger
    可以理解是一个全局消息通道,引入messenger最主要的目的就实现ViewModel和ViewModel的通信,也可以用做View和ViewModel的通信,但是并不推荐这样做。ViewModel主要是用来处理业务和数据的,每个ViewModel都有相应的业务职责,但是在业务复杂的情况下,可能存在交叉业务,这时候就需要ViewModel和ViewModel交换数据和通信,这时候一个全局的消息通道就很重要的。关于Messenger
    的详细使用方法可以参照 MVVM Light Toolkit 使用指南的 Messenger
    部分
    ,这边给出一个简单的例子仅供参考:场景是这样的,你的MainActivity对应一个MainViewModel,MainActivity
    里面除了自己的内容还包含一个Fragment,这个Fragment
    的业务处理对应于一个FragmentViewModel,FragmentViewModel请求服务器并获取数据,刚好这个数据MainViewModel也需要用到,我们不可能在MainViewModel重新请求数据,这样不太合理,这时候就需要把数据传给MainViewModel,那么应该怎么传,彼此没有引用或者回调。那么只能通过全局的消息通道Messenger。

FragmentViewModel 获取消息后通知MainViewModel 并把数据传给它:

combineRequestOb.filter(Notification::isOnNext) .map(n -> n.getValue .map(p -> p.first) .filter(m -> !m.getTop_stories().isEmpty .doOnNext(m ->Observable.just(NewsListHelper.isTomorrow.filter(b -> b).subscribe(b -> itemViewModel.clear // 上面的代码可以不看,就是获取网络数据 ,通过send把数据传过去.subscribe(m -> Messenger.getDefault().send(m, TOKEN_TOP_NEWS_FINISH));

MainViewModel 接收消息并处理:

Messenger.getDefault().register(activity, NewsViewModel.TOKEN_TOP_NEWS_FINISH, TopNewsService.News.class,  -> {// to something....}

在MainActivity onDestroy 取消注册就行了

@Overrideprotected void onDestroy() { super.onDestroy(); Messenger.getDefault().unregister;}

当然上面的例子也只是简单的说明下,Messenger可以用在很多场景,通知,广播都可以,不一定要传数据,在一定条件下也可以用在View层和ViewModel
上的通信和广播。运用范围特别广,需要开发者结合实际的业务中去做更深层次的挖掘。

Model Driven UI

这概念谁说的来着,好像是Polymer。其实在12年的某个项目里,我就在尝试这个方式,当然,举步维艰。

Testability:可测试性

可测试性是保证软件工程质量的重要手段之一,也是保证产品可用性的重要途径。在传统的GUI程序开发中,特别是对于界面的测试常常设置于状态或者运行环境,并且很多与用户交互相关的测试很难进行场景重现,或者需要大量的人工操作去模拟真实环境。

1、概述

Databinding
是一种框架,MVVM是一种模式,两者的概念是不一样的。我的理解DataBinding是一个实现数据和UI绑定的框架,只是一个实现MVVM模式的工具。ViewModel和View可以通过DataBinding来实现单向绑定和双向绑定,这套UI和数据之间的动态监听和动态更新的框架Google已经帮我们做好了。在MVVM模式中ViewModel和View是用绑定关系来实现的,所以有了DataBinding
使我们构建Android MVVM 应用程序成为可能。
之前看了很多关于DataBinding的博客和相关的一些Demo,大多数就是往xml布局文件传入一些数据,然后把这些数据绑定到控件上(
如TextView binding:text=“@{user.name} ),接着在这些控件上(如Button
binding:setOnClickListener=”@{user.listener}”)
设置一些事件到控件上,基本讲述都是DataBinding的基本用法。但是并没有人告诉你把一个onClickListener
写到一个类并把这个listener绑定到xml里面上是不是不太好,也没有人告诉你这个和xml布局绑定的ViewModel类应该放哪些数据,应该做什么事?应该如何设计?更是很少有博文来告诉你在Android
中如何通过Data Binding 去构建MVVM
的应用框架。这也就是是本篇文章的重点。接下来,我们先来看看什么是MVVM,然后在一步一步来设计整个应用程序框架。

源码地址

一个很糙的方式

当时的主要矛盾是,我们也实现了单向数据流,所有UI操作都调用Business层(相当于Controller)的接口,UI保持对Model的严格只读。但Business层修改完了Model之后,下一步就非常难了,为啥难呢?因为“Model变了,Drive不起UI来”

如果Model只有一个简单粗暴的change事件,那么UI就倒了八辈子的大霉了,它根本不知道到底变了什么,没法做最小的UI更新,那么性能上基本先Say
Goodbye了。

于是实践上的问题就来了,Business层在修改Model的时候需要如履薄冰地触发一个“合理地小”的事件——不能太大,这样UI大面积做无用的更新;不能太碎,这样UI还需要做一个batch更新机制。
这样的结果肯定就是事件的种类会随着use
case增多而大幅度增多,而可怕的就是UI必须对这些新增的事件一一作出响应,哪怕它跟之前某一个事件差别相当之小。

这当中自然也就隐含了Model对UI的间接依赖,逻辑代码需要对UI有比较深入的了解,才会知道怎样去触发一个事件它才会“合理地小”。

有了batch
update,可以把Model的change做到字段级别的CRUD事件了,但UI需要关心的事件就会呈一个数量级的增加。等于原本在逻辑代码里集中更新UI,变为了在UI里(借助batch
update)分散更新——事儿没变少,就是换了个人在干。

至少是解决了一个依赖倒置的问题,UI通过字段来访问Model,通过事件来订阅更新自己,而Model则几乎不会对UI产生直接依赖了,极端一些,Model对于UI是不是DOM都可以不关心了。

MVVM

图片 4

import UIKit



struct Person { // Model

    let firstName: String

    let lastName: String

}



protocol GreetingViewModelProtocol: class {

    var greeting: String? { get }

    var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change

    init(person: Person)

    func showGreeting()

}



class GreetingViewModel : GreetingViewModelProtocol {

    let person: Person

    var greeting: String? {

        didSet {

            self.greetingDidChange?(self)

        }

    }

    var greetingDidChange: ((GreetingViewModelProtocol) -> ())?

    required init(person: Person) {

        self.person = person

    }

    func showGreeting() {

        self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName

    }

}



class GreetingViewController : UIViewController {

    var viewModel: GreetingViewModelProtocol! {

        didSet {

            self.viewModel.greetingDidChange = { [unowned self] viewModel in

                self.greetingLabel.text = viewModel.greeting

            }

        }

    }

    let showGreetingButton = UIButton()

    let greetingLabel = UILabel()



    override func viewDidLoad() {

        super.viewDidLoad()

        self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)

    }

    // layout code goes here

}

// Assembling of MVVM

let model = Person(firstName: "David", lastName: "Blaine")

let viewModel = GreetingViewModel(person: model)

let view = GreetingViewController()

view.viewModel = viewModel
  • Distribution:在Cocoa
    MVVM中,View相对于MVP中的View担负了更多的功能,譬如需要构建数据绑定等等

  • Testability:ViewModel拥有View中的所有数据结构,因此很容易就可以进行测试

  • 易用性:相对而言有很多的冗余代码

4、总结和源码##

  • 本篇博文讲解主要是一些个人开发过程中总结的Android
    MVVM构建思想,更多是理论上各个模块如何分工,代码如何设计,虽然现在业界使用Android
    MVVM模式开发还比较少,但是随着DataBinding 1.0 的发布,相信在Android
    MVVM
    这块领域会更多的人来尝试,刚好最近用MVVM开发了一段时间,有点心得,写出来仅供参考。

  • 文中讲解的过程代码比较少,代码用到了自己开发的一个MVVM Light Toolkit
    库,而且还是RxJava + Lambda
    的代码,估计很多人看着都晕菜了,这边会把源码公布出来。如果你还没有尝试过用RxJava+Retrofit+DataBinding
    构建Android MVVM
    应用程序,那么你可以试着看一下这边的源码并且做一下尝试,说不定你会喜欢上这样的开发框架。

  • 关于MVVM Light Toolkit
    只是一个工具库,主要目的是更快捷方便的构建Android
    MVVM应用程序,在里面添加了一些控件额外属性和做了一些事件的封装,同时引进了全局消息通道Messenger,用起来确实非常方便,你可以尝试一下,当然还有不少地方没有完善和优化,后续也会不断更新和优化,如果不能达到你的业务需求时,你也可以自己添加自己需要的属性和事件。如果想更深入了解MVVM
    Light Toolkit 请看我这篇博文 MVVM Light Toolkit 使用指南

  • 源码地址

  • library —> library是MVVM Light Toolkit
    的源码,源码很简单,感兴趣的同学可以看看,没什么多少的技术难度,可以根据自己的需求,添加更多的控件的属性和事件绑定。

  • sample —> 本文涉及的代码均处出于这个项目,sample
    一个知乎日报的App的简单实现,代码包含了一大部分 MVVM Light Toolkit
    的使用场景,(Data、Command、Messenger均有涉及),同时sample严格按照博文阐述的MVVM的设计思想开发的,对理解本文有很大的帮助,欢迎clone下来看看。Sample
    截图

    图片 5

  • 源码涉及 RxJava+Retrofit+Lambda
    如有不懂或没接触过,花点时间入门一下,用到都是比较简单的东西。

希望这篇博客在如何构建Android
MVVM应用程序对你有所帮助,如有任何疑问,可以给我留言,欢迎大家共同探讨,如果对MVVM
Light Toolkit 有任何问题,也可以反馈给我。

这么做的问题

一句话

UI被设计为依赖Model,Model不应该依赖UI。

如果实现成贫血Model层,就会在逻辑代码里面去进行上面的query-update操作,如果是充血Model层那可能就在Model里。不论怎样,这样做都违背了上述依赖关系。

很简单,当UI发生变化(这种变化在迭代当中非常频繁)的时候,不仅需要修改UI本身,也需要去修改逻辑代码或者Model层,比方说#name这个ID换掉了,得换个选择器;比方说span变成了textbox,得把.html()换成.val();比方说整个UI层重新换了一套CSS命名规范,或者上了一个className混淆方案,可能让所有的addClass/removeClass/hasClass全瞎;比方说运营需要“重要的事情说三遍”于是同一个字段要被连续展现3次;比方说相册改版,啥没变,惟独从井字格变成轮播图了……

这些本身应该是UI的事儿——毫无业务逻辑在里面——却需要去改逻辑代码,依赖关系颠倒过来了,形成了anti-pattern。

所以现在流行说“单向数据流”,它是对上面所说的依赖关系的一个形象描述。

Supervising Controller MVP

简化Presenter的部分功能,使得Presenter只起到需要复杂控制或者调解的操作,而简单的Model展示转化直接由View与Model进行交互:

图片 6

2、MVC、MVP、MVVM

首先,我们先大致了解Android开发中常见的模式,以便我们更深入了解MVVM
模式。

View:对应于xml布局文件Model:实体模型Controllor:对应于Activity业务逻辑,数据处理和UI处理

从上面看起来各个组件的职责视乎还挺耦合MVC的,但是打开Android的一个Activity文件,一看一言难尽,
Android中经常会出现数千行的Activity代码,究其原因,Android中纯粹作为View的各个XML视图功能太弱,Activity基本上都是View和Controller的合体,既要负责视图的显示又要加入控制逻辑,承担的功能过多,代码量大也就不足为奇。所有更贴切的目前常规的开发说应该是View-Model
模式,大部分都是通过Activity的协调,连接,和处理逻辑的。

**View:
**对应于Activity和xml,负责View的绘制以及与用户交互**Model:
**依然是实体模型**Presenter:
**负责完成View于Model间的交互和业务逻辑

在Android开发中MVP的设计思想用得比较多,利用MVP的设计模型可以把部分的逻辑的代码从Fragment和Activity业务的逻辑移出来,在Presenter中持有View(Activity或者Fragment)的引用,然后在Presenter调用View暴露的接口对视图进行操作,这样有利于把视图操作和业务逻辑分开来。MVP能够让Activity成为真正的View而不是View和Control的合体,Activity只做UI相关的事。但是这个模式还是存在一些不好的地方,比较如说:

  • Activity需要实现各种跟UI相关的接口,同时要在Activity中编写大量的事件,然后在事件处理中调用presenter的业务处理方法,View和Presenter只是互相持有引用并互相做回调,代码不美观。

  • 这种模式中,程序的主角是UI,通过UI事件的触发对数据进行处理,更新UI就有考虑线程的问题。而且UI改变后牵扯的逻辑耦合度太高,一旦控件更改(比较TextView
    替换 EditText等)牵扯的更新UI的接口就必须得换。

  • 复杂的业务同时会导致presenter层太大,代码臃肿的问题。

**View:
**对应于Activity和xml,负责View的绘制以及与用户交互**Model:
**实体模型**ViewModel:
**负责完成View于Model间的交互,负责业务逻辑

MVVM的目标和思想MVP类似,利用数据绑定(Data Binding)、依赖属性(Dependency
Property)、命令、路由事件(Routed
Event)等新特性,打造了一个更加灵活高效的架构。

  • 数据驱动在MVVM中,以前开发模式中必须先处理业务数据,然后根据的数据变化,去获取UI的引用然后更新UI,通过也是通过UI来获取用户输入,而在MVVM中,数据和业务逻辑处于一个独立的View
    Model中,ViewModel只要关注数据和业务逻辑,不需要和UI或者控件打交道。由数据自动去驱动UI去自动更新UI,UI的改变又同时自动反馈到数据,数据成为主导因素,这样使得在业务逻辑处理只要关心数据,方便而且简单很多。
  • 低耦合度MVVM模式中,数据是独立于UI的,ViewModel只负责处理和提供数据,UI想怎么处理数据都由UI自己决定,ViewModel
    不涉及任何和UI相关的事也不持有UI控件的引用,即使控件改变(TextView
    换成 EditText)ViewModel
    几乎不需要更改任何代码,专注自己的数据处理就可以了,如果是MVP遇到UI更改,就可能需要改变获取UI的方式,改变更新UI的接口,改变从UI上获取输入的代码,可能还需要更改访问UI对象的属性代码等等。
  • 更新 UI在MVVM中,我们可以在工作线程中直接修改View
    Model的数据(只要数据是线程安全的),剩下的数据绑定框架帮你搞定,很多事情都不需要你去关心。
  • 团队协作MVVM的分工是非常明显的,由于View和View
    Model之间是松散耦合的。一个是处理业务和数据,一个是专门的UI处理。完全有两个人分工来做,一个做UI(xml
    和 Activity)一个写ViewModel,效率更高。
  • 可复用性一个View
    Model复用到多个View中,同样的一份数据,用不同的UI去做展示,对于版本迭代频繁的UI改动,只要更换View层就行,对于如果想在UI上的做AbTest
    更是方便的多。
  • 单元测试View
    Model里面是数据和业务逻辑,View中关注的是UI,这样的做测试是很方便的,完全没有彼此的依赖,不管是UI的单元测试还是业务逻辑的单元测试,都是低耦合的。

通过上面对MVVM的简述和其他两种模式的对比,我们发现MVVM对比MVC和MVP来说还是存在比较大的优势,虽然目前Android开发中可能真正在使用MVVM的很少,但是是值得我们去做一些探讨和调研。

“传统”方式

用一种“传统”的思路,我们要更新页面某一个部分的UI,应该这么做:

JavaScript

$.get(‘url’, function(data) { ui.find(‘#name’).html(data.name) })

1
2
3
$.get(‘url’, function(data) {
  ui.find(‘#name’).html(data.name)
})

这个例子应该是一个典型的场景

  • 拉数据
  • 找元素
  • 改属性

为什么核心在于“找元素”呢?由于要尽可能的优化UI的性能,只能做最小更新操作,那么就需要找到发生变化的那个字段所需要的元素,单独对其进行操作。

所以jQuery的核心就在于query,首当其冲就是它能最快捷的帮我们query出需要的元素来,很好的满足了一个JS库的核心需求。当然它的另一个优势就是它的API设计得太简便了,简直是不会JS都能用,入门成本之低令人发指。

MVC:Monolithic Controller

相信每一个程序猿都会宣称自己掌握MVC,这个概念浅显易懂,并且贯穿了从GUI应用到服务端应用程序。MVC的概念源自Gamma,
Helm, Johnson
以及Vlissidis这四人帮在讨论设计模式中的Observer模式时的想法,不过在那本经典的设计模式中并没有显式地提出这个概念。我们通常认为的MVC名词的正式提出是在1979年5月Trygve
Reenskaug发表的Thing-Model-View-Editor这篇论文,这篇论文虽然并没有提及Controller,但是Editor已经是一个很接近的概念。大概7个月之后,Trygve
Reenskaug在他的文章Models-Views-Controllers中正式提出了MVC这个三元组。上面两篇论文中对于Model的定义都非常清晰,Model代表着
an abstraction in the form of data in a computing system.
,即为计算系统中数据的抽象表述,而View代表着 capable of showing one or
more pictorial representations of the Model on screen and on hardcopy.
,即能够将模型中的数据以某种方式表现在屏幕上的组件。而Editor被定义为某个用户与多个View之间的交互接口,在后一篇文章中Controller则被定义为了
a special controller … that permits the user to modify the information
that is presented by the view.
,即主要负责对模型进行修改并且最终呈现在界面上。从我的个人理解来看,Controller负责控制整个界面,而Editor只负责界面中的某个部分。Controller协调菜单、面板以及像鼠标点击、移动、手势等等很多的不同功能的模块,而Editor更多的只是负责某个特定的任务。后来,Martin
Fowler在2003开始编写的著作Patterns of Enterprise Application
Architecture中重申了MVC的意义: Model View Controller (MVC) is one of
the most quoted (and most misquoted) patterns around.
,将Controller的功能正式定义为:响应用户操作,控制模型进行相应更新,并且操作页面进行合适的重渲染。这是非常经典、狭义的MVC定义,后来在iOS以及其他很多领域实际上运用的MVC都已经被扩展或者赋予了新的功能,不过笔者为了区分架构演化之间的区别,在本文中仅会以这种最朴素的定义方式来描述MVC。

根据上述定义,我们可以看到MVC模式中典型的用户场景为:

  • 用户交互输入了某些内容

  • Controller将用户输入转化为Model所需要进行的更改

  • Model中的更改结束之后,Controller通知View进行更新以表现出当前Model的状态

图片 7

根据上述流程,我们可知经典的MVC模式的特性为:

  • View、Controller、Model中皆有ViewLogic的部分实现

  • Controller负责控制View与Model,需要了解View与Model的细节。

  • View需要了解Controller与Model的细节,需要在侦测用户行为之后调用Controller,并且在收到通知后调用Model以获取最新数据

  • Model并不需要了解Controller与View的细节,相对独立的模块

扯扯“Model Driven UI”

2016/02/03 · 基础技术 ·
UI

原文出处:
刘骥(@刘骥-JimLiu)   

为什么我认为对于构建应用程序而言,MVVM/React是比jQuery更容易的方式?

文章比较浅,科普性质,大神们别嫌弃。

MV* in Android

此部分完整代码在 这里 ,笔者在这里节选出部分代码方便对照演示。Android中的Activity的功能很类似于iOS中的UIViewController,都可以看做MVC中的Controller。在2010年左右经典的Android程序大概是这样的:

TextView mCounterText;

Button mCounterIncrementButton;



int mClicks = 0;



public void onCreate(Bundle b) {

  super.onCreate(b);



  mCounterText = (TextView) findViewById(R.id.tv_clicks);

  mCounterIncrementButton = (Button) findViewById(R.id.btn_increment);



  mCounterIncrementButton.setOnClickListener(new View.OnClickListener() {

    public void onClick(View v) {

      mClicks++;

      mCounterText.setText(""+mClicks);

    }

  });

}

后来2013年左右出现了 ButterKnife 这样的基于注解的控件绑定框架,此时的代码看上去是这样的:

@Bind(R.id.tv_clicks) mCounterText;

@OnClick(R.id.btn_increment)

public void onSubmitClicked(View v) {

    mClicks++;

    mCounterText.setText("" + mClicks);

}

后来Google官方也推出了数据绑定的框架,从此MVVM模式在Android中也愈发流行:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

   <data>

       <variable name="counter" type="com.example.Counter"/>

       <variable name="counter" type="com.example.ClickHandler"/>

   </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="@{counter.value}"/>

       <Buttonandroid:layout_width="wrap_content"

           android:layout_height="wrap_content"

           android:text="@{handlers.clickHandle}"/>

   </LinearLayout>

</layout>

后来 Anvil 这样的受React启发的组件式框架以及Jedux这样借鉴了Redux全局状态管理的框架也将Unidirectional
架构引入了Android开发的世界。

没那么糙的方式

现在有了MVVM和Virtual-DOM了,batch
update也都是标配,Business层可以肆无忌惮的对Model进行任何粒度的CRUD。UI也不需要监听Model上的各种事件了——简单的说来,虽然整个数据流没有变,但是每一个环节都变简单了。

所以MVVM和Virtual-DOM解决的问题是数据绑定/数据展现吗?是,也不全是。更深究地说,它们解决的问题是帮助UI和Model之间“脏活累活谁来干”的问题——都没人干,于是只能让框架干了。从此以后,

对于Model而言:“老子就管写,你爱读不读。反正我的值是对的,用户看到展现不对那都赖你。”

对于UI而言:“老子就歇着,你爱咋样就来弄我两下,但是活儿得好,别让我太累,用户嫌卡那就怪你。”

至于Model如何Drive
UI,Angular(脏检查)、React(Virtual-DOM)用的办法是主动的发现Model的变化,然后去推动UI更新;Avalon、Vue基于property
getter的做法是被动的等Model发生变化。
除了Virtual-DOM以外,都需要对UI进行预处理,解析出一个UI Element ->
property之间的依赖关系,知道每一个Element依赖了Model的哪个字段。把这张图反过来,就知道当一个property被修改时,它会影响那些个Element,从而实现最小更新。
而Virtual-DOM的最小化patch方案是通过tree-diff计算出来的,基于现代浏览器“老子for循环跑的飞快”的霸气,执行tree-diff的速度很理想。于是就直接不需要构建依赖关系,用起来更简单粗暴;进而在需要的时候有一定的优化空间,可以通过immutable这种方式来快速跳过tree-diff当中的某些环节。
所以在精心优化的情况下,Virtual-DOM应该最快的无疑,property
getter有更强的适应性,天生就很快,但从外部去优化它很难。
React另一个优势是它的启动速度,由于不需要构建依赖关系,甚至是连parse模板都不需要(这一步相当于直接在构建JSX的时候已经做好了),它启动步骤就短多了,夸张地说,直接render就出来了。
使用property
getter的方案对于Model层有非常微弱的侵入性(相比Knockout那是低多了),使用脏检查和Virtual-DOM对Model层都几乎没有侵入性。
当然上面所说的性能差异其实都没有那么大啦……只是因为我自己写过virtual-dom玩具,也看了Vue的源码,一点小结而已。

Model-View-Update

又被称作 Elm
Architecture ,上面所讲的Redux就是受到Elm的启发演化而来,因此MVU与Redux之间有很多的相通之处。MVU使用函数式编程语言Elm作为其底层开发语言,因此该架构可以被看做更纯粹的函数式架构。MVU中的基本组成部分有:

  • Model:定义状态数据结构的类型

  • View:纯函数,将状态渲染为界面

  • Actions:以Mailbox的方式传递用户事件的载体

  • Update:用于更新状态的纯函数

图片 8

根据上述流程,我们可知Elm模式的特性为:

  • 到处可见的层次化组合:Redux只是在View层允许将组件进行层次化组合,而MVU中在Model与Update函数中也允许进行层次化组合,甚至Actions都可以包含内嵌的子Action

  • Elm属于Fractal架构:因为Elm中所有的模块组件都支持层次化组合,即都可以被单独地导出使用

个人的感受

  • 程序怎么写,还得看活儿
  • 做Web App和做Web Page,取舍还是差别大
  • 怎么算Web App怎么算Web Page,还得看老板怎么想
  • 如若无所谓模式,无所谓架构,那一切都是白说,反正It works
  • 面向工资编程,终究还是为了出活儿快、下班早,需求变时别骂娘,早日升职加薪,当上总经理,迎娶白富美,走上人生巅峰

    1 赞 1 收藏
    评论

图片 9

界面的组件化

A component is a small piece of the user interface of our application, a
view, that can be composed with other components to make more advanced
components.

何谓组件?一个组件即是应用中用户交互界面的部分组成,组件可以通过组合封装成更高级的组件。组件可以被放入层次化的结构中,即可以是其他组件的父组件也可以是其他组件的子组件。根据上述的组件定义,笔者认为像Activity或者UIViewController都不能算是组件,而像ListView或者UITableView可以看做典型的组件。

图片 10

我们强调的是界面组件的Composable&Reusable,即可组合性与可重用性。当我们一开始接触到Android或者iOS时,因为本身SDK的完善度与规范度较高,我们能够很多使用封装程度较高的组件。譬如ListView,无论是Android中的RecycleView还是iOS中的UITableView或者UICollectionView,都为我们提供了。凡事都有双面性,这种较高程度的封装与规范统一的API方便了我们的开发,但是也限制了我们自定义的能力。同样的,因为SDK的限制,真正意义上可复用/组合的组件也是不多,譬如你不能将两个ListView再组合成一个新的ListView。在React中有所谓的controller-view的概念,即意味着某个React组件同时担负起MVC中Controller与View的责任,也就是JSX这种将负责ViewLogic的JavaScript代码与负责模板的HTML混编的方式。

界面的组件化还包括一个重要的点就是路由,譬如Android中的 AndRouter 、iOS中的 JLRoutes都是集中式路由的解决方案,不过集中式路由在Android或者iOS中并没有大规模推广。iOS中的StoryBoard倒是类似于一种集中式路由的方案,不过更偏向于以UI设计为核心。笔者认为这一点可能是因为Android或者iOS本身所有的代码都是存放于客户端本身,而Web中较传统的多页应用方式还需要用户跳转页面重新加载,而后在单页流行之后即不存在页面级别的跳转,因此在Web单页应用中集中式路由较为流行而Android、iOS中反而不流行。

无状态的组件

无状态的组件的构建函数是纯函数(pure
function)并且引用透明的(refferentially
transparent),在相同输入的情况下一定会产生相同的组件输出,即符合 View =
f(State,Template)
公式。笔者觉得Android中的ListView/RecycleView,或者iOS中的UITableView,也是无状态组件的典型。譬如在Android中,可以通过动态设置Adapter实例来为RecycleView进行源数据的设置,而作为View层以IoC的方式与具体的数据逻辑解耦。

组件的可组合性与可重用性往往最大的阻碍就是状态,一般来说,我们希望能够重用或者组合的组件都是

Generalization,而状态往往是Specification,即领域特定的。同时,状态也会使得代码的可读性与可测试性降低,在有状态的组件中,我们并不能通过简单地阅读代码就知道其功能。如果借用函数式编程的概念,就是因为副作用的引入使得函数每次回产生不同的结果。函数式编程中存在着所谓Pure
Function,即纯函数的概念,函数的返回值永远只受到输入参数的影响。譬如
(x)=>x*2
这个函数,输入的x值永远不会被改变,并且返回值只是依赖于输入的参数。而Web开发中我们也经常会处于带有状态与副作用的环境,典型的就是Browser中的DOM,之前在jQuery时代我们会经常将一些数据信息缓存在DOM树上,也是典型的将状态与模板混合的用法。这就导致了我们并不能控制到底应该何时去进行重新渲染以及哪些状态变更的操作才是必须的,

var Header = component(function (data) {
  // First argument is h1 metadata
  return h1(null, data.text);
});

// Render the component to our DOM
render(Header({text: 'Hello'}), document.body);

// Some time later, we change it, by calling the
// component once more.
setTimeout(function () {
  render(Header({text: 'Changed'}), document.body);
}, 1000);

var hello = Header({ text: 'Hello' }); var bye   = Header({ text: 'Good Bye' });

MVP

  • 将Presenter与View绑定,并且将用户响应事件绑定到Presenter中

//Set up presenter

        presenter = new MainPresenter();

        presenter.attachView(this);

        ...



        // Set up search button

        searchButton = (ImageButton) findViewById(R.id.button_search);

        searchButton.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                presenter.loadRepositories(editTextUsername.getText().toString());

            }

        });
  • Presenter中调用Model更新数据,并且调用View中进行重新渲染

public void loadRepositories(String usernameEntered) {

        String username = usernameEntered.trim();

        if (username.isEmpty()) return;



        mainMvpView.showProgressIndicator();

        if (subscription != null) subscription.unsubscribe();

        ArchiApplication application = ArchiApplication.get(mainMvpView.getContext());

        GithubService githubService = application.getGithubService();

        subscription = githubService.publicRepositories(username)

                .observeOn(AndroidSchedulers.mainThread())

                .subscribeOn(application.defaultSubscribeScheduler())

                .subscribe(new Subscriber<List<Repository>>() {

                    @Override

                    public void onCompleted() {

                        Log.i(TAG, "Repos loaded " + repositories);

                        if (!repositories.isEmpty()) {

                            mainMvpView.showRepositories(repositories);

                        } else {

                            mainMvpView.showMessage(R.string.text_empty_repos);

                        }

                    }



                    @Override

                    public void onError(Throwable error) {

                        Log.e(TAG, "Error loading GitHub repos ", error);

                        if (isHttp404(error)) {

                            mainMvpView.showMessage(R.string.error_username_not_found);

                        } else {

                            mainMvpView.showMessage(R.string.error_loading_repos);

                        }

                    }



                    @Override

                    public void onNext(List<Repository> repositories) {

                        MainPresenter.this.repositories = repositories;

                    }

                });

        }

笔者在撰写本文的时候也不可避免的带了很多自己的观点,在漫长的GUI架构模式变迁过程中,很多概念其实是交错复杂,典型的譬如MVP与MVVM的区别,笔者按照自己的理解强行定义了二者的区分边界,不可避免的带着自己的主观想法。另外,鉴于笔者目前主要进行的是Web方面的开发,因此在整体倾向上是支持Unidirectional
Architecture并且认为集中式的状态管理是正确的方向。但是必须要强调,GUI架构本身是无法脱离其所依托的平台,下文笔者也会浅述由于Android与iOS本身SDK
API的特殊性,生搬硬套其他平台的架构模式也是邯郸学步,沐猴而冠。不过总结而言,它山之石,可以攻玉,本身我们所处的开发环境一直在不断变化,对于过去的精华自当应该保留,并且与新的环境相互印证,触类旁通。

Unidirectional User Interface Architecture:单向数据流

Unidirectional User Interface
Architecture架构的概念源于后端常见的CROS/Event
Sourcing模式,其核心思想即是将应用状态被统一存放在一个或多个的Store中,并且所有的数据更新都是通过可观测的Actions触发,而所有的View都是基于Store中的状态渲染而来。该架构的最大优势在于整个应用中的数据流以单向流动的方式从而使得有用更好地可预测性与可控性,这样可以保证你的应用各个模块之间的松耦合性。与MVVM模式相比,其解决了以下两个问题:

  • 避免了数据在多个ViewModel中的冗余与不一致问题

  • 分割了ViewModel的职责,使得ViewModel变得更加Clean

Overview

  • Martin Fowler-GUI
    Architectures

  • Comparison-of-Architecture-presentation-patterns

MV*:Fragmentary State 碎片化的状态与双向数据流

MVC模式将有关于渲染、控制与数据存储的概念有机分割,是GUI应用架构模式的一个巨大成就。但是,MVC模式在构建能够长期运行、维护、有效扩展的应用程序时遇到了极大的问题。MVC模式在一些小型项目或者简单的界面上仍旧有极大的可用性,但是在现代富客户端开发中导致职责分割不明确、功能模块重用性、View的组合性较差。作为继任者MVP模式分割了View与Model之间的直接关联,MVP模式中也将更多的ViewLogic转移到Presenter中进行实现,从而保证了View的可测试性。而最年轻的MVVM将ViewLogic与View剥离开来,保证了View的无状态性、可重用性、可组合性以及可测试性。总结而言,MV*模型都包含了以下几个方面:

  • Models:负责存储领域/业务逻辑相关的数据与构建数据访问层,典型的就是譬如
    Person 、 PersonDataProvider 。

  • Views:负责将数据渲染展示给用户,并且响应用户输入

  • Controller/Presenter/ViewModel:往往作为Model与View之间的中间人出现,接收View传来的用户事件并且传递给Model,同时利用从Model传来的最新模型控制更新View

谈到架构,我们关心哪些方面?

当我们谈论所谓客户端开发的时候,我们首先会想到怎么保证向后兼容、怎么使用本地存储、怎么调用远程接口、如何有效地利用内存/带宽/CPU等资源,不过最核心的还是怎么绘制界面并且与用户进行交互,关于这部分详细的知识点纲要推荐参考笔者的 我的编程之路——知识管理与知识体系这篇文章或者 这张知识点列表思维脑图 。

图片 11

而当我们提纲挈领、高屋建瓴地以一个较高的抽象的视角来审视总结这个知识点的时候会发现,我们希望的好的架构,便如在引言中所说,即是有好的代码组织方式/合理的职责划分粒度。笔者脑中会出现如下这样的一个层次结构,可以看出,最核心的即为View与ViewLogic这两部分:

图片 12

实际上,对于富客户端的 代码组织/职责划分 ,从具体的代码分割的角度,即是 功能的模块化 、界面的组件化 、 状态管理 这三个方面。最终呈献给用户的界面,笔者认为可以抽象为如下等式:
View = f(State,Template)
。而ViewLogic中对于类/模块之间的依赖关系,即属于代码组织,譬如MVC中的View与Controller之间的从属关系。而对于动态数据,即所谓应用数据的管理,属于状态管理这一部分,譬如APP从后来获取了一系列的数据,如何将这些数据渲染到用户界面上使得用户可见,这样的不同部分之间的协同关系、整个数据流的流动,即属于状态管理。

发表评论

电子邮件地址不会被公开。 必填项已用*标注