AngularJS: 理解设计模式

这篇文章的背景下,AngularJS 的负责人 Igor Minar:

MVC vs MVVM vs MVP 可以花几个小时的时间争论。

几年来,AngularJS 更接近 MVC (或者更确切地说是它的一个 客户端变量) ,但随着时间的推移,并由于许多重构 和 api 的改进,它现在更接近于 MVVM-< em > $scope 对象 可以被认为是由一个 我们称之为 控制员的函数。

能够对框架进行分类并将其放入 MV * bucket 中有一些优势。 它可以帮助开发人员更好地使用 API 更容易创建一个心理模型,代表的应用程序 这个架构亦有助建立 开发人员使用的术语。

话虽如此,我还是更愿意看到开发人员开发出这样的应用程序 精心设计并遵循关注点分离,而不是浪费 时间争论 MV * 废话。因此,我在此宣布 AngularJS MVW 框架-模型-视图-随便什么 代表“ 只要对你有用就行”。

角度赋予了您很大的灵活性,可以很好地分离表示 来自业务逻辑和表示状态的逻辑。请使用它的燃料 您的生产力和应用程序的可维护性,而不是加热 讨论一些无关紧要的事情 很多。

对于在客户端应用程序中实现 AngularJS MVW (模型-视图-随便什么)设计模式,是否有任何建议或指导方针?

98111 次浏览

多亏了大量有价值的资源,我得到了一些在 AngularJS 应用程序中实现组件的一般性建议:


控制员

  • 控制器应该只是模型和视图之间的一个 夹层。尽量使它成为

  • 在控制器中强烈推荐使用 避免商业逻辑,它应该被移动到模型中。

  • 控制器可以使用方法调用与其他控制器通信(当子控制器需要与父控制器通信时可以使用)或者使用 $发射$广播$方法。应尽量减少发出和广播的讯息。

  • 控制器应该 不在乎表现或 DOM 操作。

  • 尝试使用 避免嵌套控制器。在这种情况下,父控制器被解释为 model。注入模型作为共享服务。

  • 控制器中的作用域 应用于具有视图和
    封装 视图模型作为 演示模型设计模式。


范围

作为 模板中的只读在控制器中只写处理范围。作用域的目的是引用模型,而不是作为模型。

在执行双向绑定(ng-model)时,请确保不直接绑定到范围属性。


模特

AngularJS 中的模型是由 服务定义的 单身

模型提供了一种分离数据和显示的优秀方法。

模型是单元测试的主要候选者,因为它们通常只有一个依赖项(某种形式的事件发射器,通常是 $rootScope) ,并且包含高度可测试的 领域逻辑领域逻辑

  • 模型应视为特定单元的实现。 它是以单一责任原则为基础的。单元是一个实例,它负责自己的相关逻辑的范围,这些逻辑可能代表现实世界中的单个实体,并在编程世界中用 数据和状态来描述它。

  • 模型应该封装应用程序的数据并提供一个 < strong > API 访问和操作这些数据

  • 型号应该是 便携式的,这样它可以很容易地运输到类似的 申请书

  • 通过隔离模型中的单元逻辑,您可以更容易地 定位、更新和维护

  • 模型可以使用通用的更一般的全局模型的方法 整个应用程序

  • 尽量避免使用依赖注入将其他模型组合到你的模型中,如果它不是真正依赖于减少组件耦合和增加单元 可测试性可用性

  • 尽量避免在模型中使用事件侦听器。这使得它们更难测试,并且通常根据单一责任原则扼杀模型。

模型实现

由于模型应该根据数据和状态封装某些逻辑,因此它应该在体系结构上限制对其成员的访问,从而保证松耦合。

在 AngularJS 应用程序中实现这一点的方法是使用 工厂服务类型定义它。这将允许我们非常容易地定义私有属性和方法,并且在单个位置返回公共可访问的属性和方法,这将使它对开发人员来说真正可读。

一个例子 :

angular.module('search')
.factory( 'searchModel', ['searchResource', function (searchResource) {


var itemsPerPage = 10,
currentPage = 1,
totalPages = 0,
allLoaded = false,
searchQuery;


function init(params) {
itemsPerPage = params.itemsPerPage || itemsPerPage;
searchQuery = params.substring || searchQuery;
}


function findItems(page, queryParams) {
searchQuery = queryParams.substring || searchQuery;


return searchResource.fetch(searchQuery, page, itemsPerPage).then( function (results) {
totalPages = results.totalPages;
currentPage = results.currentPage;
allLoaded = totalPages <= currentPage;


return results.list
});
}


function findNext() {
return findItems(currentPage + 1);
}


function isAllLoaded() {
return allLoaded;
}


// return public model API
return {
/**
* @param {Object} params
*/
init: init,


/**
* @param {Number} page
* @param {Object} queryParams
* @return {Object} promise
*/
find: findItems,


/**
* @return {Boolean}
*/
allLoaded: isAllLoaded,


/**
* @return {Object} promise
*/
findNext: findNext
};
});

创建新实例

尽量避免让工厂返回一个新的能力函数,因为这将开始打破依赖注入,而库将表现得很尴尬,尤其是对于第三方。

完成同样事情的一个更好的方法是使用工厂作为 API 来返回一个对象集合,其中附加了 getter 和 setter 方法。

angular.module('car')
.factory( 'carModel', ['carResource', function (carResource) {


function Car(data) {
angular.extend(this, data);
}


Car.prototype = {
save: function () {
// TODO: strip irrelevant fields
var carData = //...
return carResource.save(carData);
}
};


function getCarById ( id ) {
return carResource.getById(id).then(function (data) {
return new Car(data);
});
}


// the public API
return {
// ...
findById: getCarById
// ...
};
});

全球模式

一般来说,尽量避免这种情况,并正确设计您的模型,因此它可以被注入到控制器,并在您的视图中使用。

特别是在某些情况下,一些方法需要在应用程序中进行全局可访问性。 为了使它成为可能,您可以在 $rootScope中定义“ 很普通”属性,并在应用程序引导期间将其绑定到 CommonModel:

angular.module('app', ['app.common'])
.config(...)
.run(['$rootScope', 'commonModel', function ($rootScope, commonModel) {
$rootScope.common = 'commonModel';
}]);

所有的全局方法都将存在于“ 很普通”属性中。这是某种 命名空间

但是不要在 $rootScope中直接定义任何方法。这可能导致在视图范围内与 ngModel 指令一起使用时出现 意想不到的行为,通常会造成范围方法覆盖问题。


资源

资源允许您与不同的 资料来源交互。

应使用 单一责任原则单一责任原则实现。

在特殊情况下,它是 HTTP/JSON 端点的 可重复使用代理。

资源被注入到模型中,并提供了发送/检索数据的可能性。

资源实现

一个工厂,它创建一个资源对象,允许您与 RESTful 服务器端数据源交互。

返回的资源对象具有操作方法,这些方法提供高级行为,而不需要与低级 $http 服务交互。


服务

模型和资源都是服务

服务是独立的,松散耦合功能单元是自包含的。

服务是 Angular 从服务器端为客户端 Web 应用带来的一个特性,服务在服务器端已经普遍使用了很长时间。

角度应用中的服务是用依赖注入连接在一起的可替代对象。

角度有不同类型的服务。每一个都有自己的用例。详情请阅读 了解服务类型

尝试在应用程序中考虑 服务体系结构的主要原则

一般来说,根据 网上服务词汇:

服务是一种抽象资源,表示 执行任务,形成一个连贯的功能,从点 查看提供者实体和请求者实体 服务必须由具体的供应商代理实现。


客户端结构

一般来说,应用程序的客户端被划分为 模组。每个模块应该是 可测试的作为一个单元。

尝试根据 特性/功能风景定义模块,而不是按类型定义。 详情请参阅 米斯科的报告

模块组件通常可以按类型进行分组,如控制器、模型、视图、过滤器、指令等。

但模块本身仍然是 可重复使用可转让可测试的

对于开发人员来说,找到代码的某些部分及其所有依赖项也要容易得多。

详情请参阅 大型 AngularJS 和 JavaScript 应用中的代码组织

文件夹结构的一个例子 :

|-- src/
|   |-- app/
|   |   |-- app.js
|   |   |-- home/
|   |   |   |-- home.js
|   |   |   |-- homeCtrl.js
|   |   |   |-- home.spec.js
|   |   |   |-- home.tpl.html
|   |   |   |-- home.less
|   |   |-- user/
|   |   |   |-- user.js
|   |   |   |-- userCtrl.js
|   |   |   |-- userModel.js
|   |   |   |-- userResource.js
|   |   |   |-- user.spec.js
|   |   |   |-- user.tpl.html
|   |   |   |-- user.less
|   |   |   |-- create/
|   |   |   |   |-- create.js
|   |   |   |   |-- createCtrl.js
|   |   |   |   |-- create.tpl.html
|   |-- common/
|   |   |-- authentication/
|   |   |   |-- authentication.js
|   |   |   |-- authenticationModel.js
|   |   |   |-- authenticationService.js
|   |-- assets/
|   |   |-- images/
|   |   |   |-- logo.png
|   |   |   |-- user/
|   |   |   |   |-- user-icon.png
|   |   |   |   |-- user-default-avatar.png
|   |-- index.html

角度应用程序-https://github.com/angular-app/angular-app/tree/master/client/src实现了角度应用结构的一个很好的例子

现代应用程序生成器 https://github.com/yeoman/generator-angular/issues/109也考虑到了这一点

与 Artem 的建议相比,这是一个小问题,但在代码可读性方面,我发现最好完全在 return对象内部定义 API,以尽量减少在代码中来回查看定义了哪些变量:

angular.module('myModule', [])
// or .constant instead of .value
.value('myConfig', {
var1: value1,
var2: value2
...
})
.factory('myFactory', function(myConfig) {
...preliminary work with myConfig...
return {
// comments
myAPIproperty1: ...,
...
myAPImethod1: function(arg1, ...) {
...
}
}
});

如果 return对象看起来“太拥挤”,这是一个迹象,表明服务做得太多。

我相信伊戈尔对这个问题的看法,正如你提供的引文所示,只是一个更大问题的冰山一角。

MVC 及其衍生物(MVP、 PM、 MVVM)在单个代理中都很好用,但是服务器-客户端架构在任何情况下都是一个双代理系统,人们常常对这些模式如此着迷,以至于忘记了手头的问题要复杂得多。通过试图坚持这些原则,他们实际上最终得到了一个有缺陷的架构。

我们一点一点来吧。

指南

观点

在角度上下文中,视图是 DOM。准则如下:

做:

  • 当前范围变量(只读)。
  • 调用控制器进行操作。

不要:

  • 任何逻辑。

这看起来既诱人、简短又无害:

ng-click="collapsed = !collapsed"

这意味着任何开发人员现在都需要检查 Javascript 文件和 HTML 文件,以了解系统是如何工作的。

控制器

做:

  • 通过在作用域上放置数据,将视图绑定到“模型”。
  • 响应用户操作。
  • 处理表示逻辑。

不要:

  • 处理任何业务逻辑。

最后一条准则的原因是控制器是视图的姐妹,而不是实体; 它们也不可重用。

您可能会争辩说指令是可重用的,但是指令也是视图的姐妹(DOM)-它们从来没有打算对应于实体。

当然,有时视图代表实体,但这是一个相当特殊的情况。

换句话说,控制器应该把重点放在表示上——如果你把业务逻辑放进去,不仅你可能最终得到一个膨胀的、难以管理的控制器,而且你也违反了 分开关心原则。

因此,角度控制器实际上更多的是 演示模式 MVVM

因此,如果控制器不应该处理业务逻辑,那么谁应该呢?

什么是模型?

你的客户模型通常是片面和陈旧的

除非你正在编写一个离线 Web 应用程序,或者一个非常简单的应用程序(几个实体) ,否则你的客户端模型很可能是:

  • 只有一部分
    • 要么它没有所有的实体(如分页的情况)
    • 或者它没有所有的数据(比如分页)
  • 陈旧 -如果系统有多个用户,在任何时候都不能确定客户端持有的模型与服务器持有的模型相同。

真正的模式必须坚持下去

在传统的 MCV 中,模型是唯一的 坚持不懈。无论何时我们谈论模型,这些模型都必须在某个时刻保持不变。您的客户机可以随意操作模型,但是在成功完成到服务器的往返之前,这项工作还没有完成。

后果

以上两点应该作为一个警告-您的客户端所持有的模型只能涉及一部分,大部分是简单的业务逻辑。

因此,在客户端上下文中,使用小写的 M也许是明智的——所以它实际上是 车祸MVPMVVm。大 M用于服务器。

业务逻辑

也许关于商业模式最重要的概念之一就是你可以将它们细分为两种类型(我省略了第三种 视频业务,因为那是另一天的故事) :

  • 域逻辑 -aka 企业业务规则,独立于应用程序的逻辑。例如,给定一个具有 firstNamesirName属性的模型,像 getFullName()这样的 getter 可以被认为是独立于应用程序的。
  • 应用程序逻辑 -aka 应用程序业务规则,这是特定于应用程序的。

重要的是要强调,客户端上下文中的这两个都是 不是真正的商业逻辑——它们只处理对客户端重要的部分。应用程序逻辑(而不是域逻辑)应该负责促进与服务器和大多数用户交互的通信; 而域逻辑主要是小规模的、特定于实体的和表示驱动的。

问题仍然存在——在一个有角的应用程序中,你把它们扔到哪里?

3对4层架构

所有这些 MVW 框架使用3个层次:

Three circles. Inner - model, middle - controller, outer - view

但对于客户而言,这种做法存在两个根本问题:

  • 该模型是局部的、陈旧的,并且不能持久。
  • 没有放置应用程序逻辑的地方。

这一战略的另一种选择是 4层策略:

4 circles, from inner to outer - Enterprise business rules, Application business rules, Interface adapters, Frameworks and drivers

这里真正的问题是应用程序业务规则层(用例) ,它在客户机上经常出错。

这一层是由交互器(Bob 叔叔)实现的,这几乎就是 Martin Fowler 所说的 操作脚本服务层

具体的例子

考虑下列网上应用程式:

  • 应用程序显示用户的分页列表。
  • 用户点击“添加用户”。
  • 将打开一个模型,其中包含一个用于填充用户详细信息的表单。
  • 用户填写表单并点击提交。

现在应该发生一些事情:

  • 表单应该经过客户端验证。
  • 应向服务器发送请求。
  • 如果出现错误,应该进行处理。
  • 用户列表可能需要更新,也可能不需要(由于分页)更新。

我们要把这些都扔到哪里?

如果您的体系结构包含一个调用 $resource的控制器,那么所有这些都将在控制器中发生。但有一个更好的策略。

一个提议的解决方案

下图显示了如何通过在 Angular 客户端中添加另一个应用程序逻辑层来解决上述问题:

4 boxes - DOM points to Controller, which points to Application logic, which points to $resource

因此,我们在控制器和 $resource 之间添加了一个层(我们称之为 互动器) :

  • 是一个 服务。对于用户来说,它可以称为 UserInteractor
  • 它提供了对应于 用例封装应用程序逻辑的方法。
  • 控制向服务器发出的请求。这一层不是使用自由形式的参数调用 $resource 的控制器,而是确保向服务器发出的请求返回域逻辑可以操作的数据。
  • 它用 领域逻辑领域逻辑原型装饰返回的数据结构。

因此,根据上面具体例子的要求:

  • 用户点击“添加用户”。
  • 控制器请求交互器提供一个空白用户模型,该模型使用业务逻辑方法(如 validate())进行修饰
  • 在提交时,控制器调用模型 validate()方法。
  • 如果失败,控制器将处理该错误。
  • 如果成功,控制器将调用与 createUser()的交互器
  • 交互器调用 $resource
  • 在响应时,交互器将任何错误委托给控制器,控制器将处理这些错误。
  • 响应成功后,交互器确保如果需要,用户列表会更新。

AngularJS 没有以传统的方式实现 MVC,而是实现了一些更接近 MVVM 的东西(模型-视图-视图模型) ,视图模型也可以被称为绑定器(在有角度的情况下,它可以是 $scope)。 我们知道,角度模型可以是普通的旧 JS 对象,也可以是我们应用程序中的数据

视图—— > angularJS 中的视图是由 angularJS 通过应用指令或指令或绑定解析和编译的 HTML,这里的主要观点是角输入不仅仅是普通的 HTML 字符串(innerHTML) ,而是由浏览器创建的 DOM。

ViewModel —— > ViewModel 实际上是您的视图和模型之间的绑定器/桥梁,在 angularJS 的情况下是 $scope,用于初始化和扩展我们使用 Controller 的 $scope。

如果我想总结一下答案: 在 angularJS 应用程序中,$scope 引用数据,Controller 控制行为,View 通过与 Controller 交互来处理布局,从而做出相应的行为。

为了明确这个问题,Angular 使用了我们在常规编程中已经遇到过的不同的设计模式。 1)当我们注册我们的控制器或指令,工厂,服务等相对于我们的模块。在这里,它对全局空间隐藏了数据。也就是 模块模式。 2)当 angle 使用它的脏检查来比较作用域变量时,这里它使用 观察者模式。 3)我们控制器中的所有父子作用域都使用 原型模式。 4)在注入服务时,它使用 工厂模式

总的来说,它使用不同的已知设计模式来解决问题。