MVC

MVC: 巨石型控制器

相信每一个程序猿都会宣称自己掌握 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 的状态

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

  • View、Controller、Model 中皆有 ViewLogic 的部分实现
  • Controller 负责控制 View 与 Model,需要了解 View 与 Model 的细节。
  • View 需要了解 Controller 与 Model 的细节,需要在侦测用户行为之后调用 Controller,并且在收到通知后调用 Model 以获取最新数据
  • Model 并不需要了解 Controller 与 View 的细节,相对独立的模块

Observer Pattern:自带观察者模式的 MVC

上文中也已提及,MVC 滥觞于 Observer 模式,经典的 MVC 模式也可以与 Observer 模式相结合,其典型的用户流程为:

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

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

  • View 作为 Observer 会监听 Model 中的任意更新,一旦有更新事件发出,View 会自动触发更新以展示最新的 Model 状态

可知其与经典的 MVC 模式区别在于不需要 Controller 通知 View 进行更新,而是由 Model 主动调用 View 进行更新。这种改变提升了整体效率,简化了 Controller 的功能,不过也导致了 View 与 Model 之间的紧耦合。

iOS

Cocoa MVC 中往往会将大量的逻辑代码放入 ViewController 中,这就导致了所谓的 Massive ViewController,而且很多的逻辑操作都嵌入到了 View 的生命周期中,很难剥离开来。或许你可以将一些业务逻辑或者数据转换之类的事情放到 Model 中完成,不过对于 View 而言绝大部分时间仅起到发送 Action 给 Controller 的作用。ViewController 逐渐变成了几乎所有其他组件的 Delegate 与 DataSource,还经常会负责派发或者取消网络请求等等职责。你的代码大概是这样的:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)

上面这种写法直接将 View 于 Model 关联起来,其实算是打破了 Cocoa MVC 的规范的,不过这样也是能够减少些 Controller 中的中转代码呢。这样一个架构模式在进行单元测试的时候就显得麻烦了,因为你的 ViewController 与 View 紧密关联,使得其很难去进行测试,因为你必须为每一个 View 创建 Mock 对象并且管理其生命周期。另外因为整个代码都混杂在一起,即破坏了职责分离原则,导致了系统的可变性与可维护性也很差。经典的 MVC 的示例程序如下:

import UIKit

struct Person { // Model
 let firstName: String
 let lastName: String
}


class GreetingViewController : UIViewController { // View + Controller
 var person: Person!
 let showGreetingButton = UIButton()
 let greetingLabel = UILabel()

 override func viewDidLoad() {
  super.viewDidLoad()
  self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
 }

 func didTapButton(button: UIButton) {
  let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
  self.greetingLabel.text = greeting

 }
 // layout code goes here
}
// Assembling of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;

上面这种代码一看就很难测试,我们可以将生成 greeting 的代码移到 GreetingModel 这个单独的类中,从而进行单独的测试。不过我们还是很难去在 GreetingViewController 中测试显示逻辑而不调用 UIView 相关的譬如viewDidLoad、didTapButton等等较为费时的操作。再按照我们上文提及的优秀的架构的几个方面来看:

  • Distribution:View 与 Model 是分割开来了,不过 View 与 Controller 是紧耦合的
  • Testability:因为较差的职责分割导致貌似只有 Model 部分方便测试
  • 易用性:因为程序比较直观,可能容易理解。

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 开发的世界。

MVC

  • 声明 View 中的组件对象或者 Model 对象
 private Subscription subscription;
 private RecyclerView reposRecycleView;
 private Toolbar toolbar;
 private EditText editTextUsername;
 private ProgressBar progressBar;
 private TextView infoTextView;
 private ImageButton searchButton;
  • 将组件与 Activity 中对象绑定,并且声明用户响应处理函数
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  progressBar = (ProgressBar) findViewById(R.id.progress);
  infoTextView = (TextView) findViewById(R.id.text_info);
  //Set up ToolBar
  toolbar = (Toolbar) findViewById(R.id.toolbar);
  setSupportActionBar(toolbar);
  //Set up RecyclerView
  reposRecycleView = (RecyclerView) findViewById(R.id.repos_recycler_view);
  setupRecyclerView(reposRecycleView);
  // Set up search button
  searchButton = (ImageButton) findViewById(R.id.button_search);
  searchButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    loadGithubRepos(editTextUsername.getText().toString());
   }
  });
  //Set up username EditText
  editTextUsername = (EditText) findViewById(R.id.edit_text_username);
  editTextUsername.addTextChangedListener(mHideShowButtonTextWatcher);
  editTextUsername.setOnEditorActionListener(new TextView.OnEditorActionListener() {
   @Override
   public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    if (actionId == EditorInfo.IME_ACTION_SEARCH) {
     String username = editTextUsername.getText().toString();
     if (username.length() > 0) loadGithubRepos(username);
     return true;
    }
    return false;
   }
});
  • 用户输入之后的更新流程
progressBar.setVisibility(View.VISIBLE);
reposRecycleView.setVisibility(View.GONE);
infoTextView.setVisibility(View.GONE);
ArchiApplication application = ArchiApplication.get(this);
GithubService githubService = application.getGithubService();

subscription = githubService.publicRepositories(username)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(application.defaultSubscribeScheduler())
    .subscribe(new Subscriber<List<Repository>>() {
     @Override
     public void onCompleted() {
		progressBar.setVisibility(View.GONE);
		if (reposRecycleView.getAdapter().getItemCount() > 0) {
		 reposRecycleView.requestFocus();
		 hideSoftKeyboard();
		 reposRecycleView.setVisibility(View.VISIBLE);
		} else {
		 infoTextView.setText(R.string.text_empty_repos);
		 infoTextView.setVisibility(View.VISIBLE);
		}
     }


     @Override
     public void onError(Throwable error) {
		Log.e(TAG, "Error loading GitHub repos ", error);
		progressBar.setVisibility(View.GONE);
		if (error instanceof HttpException
		  && ((HttpException) error).code() == 404) {
		 infoTextView.setText(R.string.error_username_not_found);
		} else {
		 infoTextView.setText(R.string.error_loading_repos);
		}
		infoTextView.setVisibility(View.VISIBLE);
     }


     @Override
     public void onNext(List<Repository> repositories) {
		Log.i(TAG, "Repos loaded " + repositories);
		RepositoryAdapter adapter =
		  (RepositoryAdapter) reposRecycleView.getAdapter();
		adapter.setRepositories(repositories);
		adapter.notifyDataSetChanged();
     }
});
下一页