AngularJS:了解设计模式控制器范围型资源服务业客户端结构准则什么是模型?业务逻辑3 层与 4 层架构具体示例建议的解决方案

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

MVC vs MVVM vs MVP.这是一个多么有争议的话题,许多开发人员可以花费数小时进行辩论和争论。

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

能够对框架进行分类并将其放入其中一个 MV* 存储桶具有一些优势。它可以帮助开发人员更轻松地创建一个表示使用框架构建的应用程序的心智模型,从而更熟悉其api。它还可以帮助建立开发人员使用的术语。

话虽如此,我宁愿看到开发人员构建精心设计并遵循关注点分离的应用程序,而不是看到他们浪费时间争论MV*胡说八道。出于这个原因,我特此声明AngularJSMVW框架 - Model-View-Whatever。凡事代表“凡事对你有用”。

Angular 为您提供了很大的灵活性,可以将表示逻辑与业务逻辑和表示状态很好地分开。请使用它来提高您的生产力和应用程序的可维护性,而不是对一天结束时无关紧要的事情进行激烈的讨论。

对于在客户端应用程序中实现 AngularJS MVW(Model-View-Whatever)设计模式,是否有任何建议或指南?


答案 1

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


控制器

  • 控制器应该只是模型和视图之间的夹层。尽量让它尽可能

  • 强烈建议避免在控制器中使用业务逻辑。应将其移动到模型。

  • 控制器可以使用方法调用(当孩子想要与父节点通信时可能)或$emit$broadcast$on方法与其他控制器进行通信。发出和广播的消息应保持在最低限度。

  • 控制器不应该关心演示或 DOM 操作。

  • 尽量避免嵌套控制器。在这种情况下,父控制器被解释为模型。而是将模型作为共享服务注入。

  • 控制器中的作用域应用于将模型与视图绑定,并
    像用于表示模型设计模式一样封装视图模型


范围

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

执行双向绑定(ng 模型)时,请确保不要直接绑定到作用域属性。


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

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

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

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

  • 模型应封装应用程序的数据,并提供 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中定义“common”属性,并在应用程序引导期间将其绑定到commonModel

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

您的所有全局方法都将位于“公共”属性中。这是某种命名空间

但不要直接在$rootScope中定义任何方法。当在视图范围内与 ngModel 指令一起使用时,这可能会导致意外行为,这通常会散落在您的范围内,并导致作用域方法覆盖问题。


资源

资源允许您与不同的数据源进行交互。

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

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

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

资源实施

创建资源对象的工厂,允许您与 RESTful 服务器端数据源进行交互。

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


服务业

模型和资源都是服务

服务是自包含的未关联、松散耦合的功能单元。

服务是Angular从服务器端为客户端Web应用程序带来的一项功能,服务已经普遍使用了很长时间。

Angular 应用中的服务是使用依赖关系注入连接在一起的可替换对象。

Angular附带不同类型的服务。每个都有自己的用例。有关详细信息,请阅读了解服务类型

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

一般来说,根据Web服务术语表

服务是一种抽象资源,它表示执行任务的能力,这些任务从提供者实体和请求者实体的角度形成一致的功能。要使用,服务必须由具体的提供者代理实现。


客户端结构

通常,应用程序的客户端被拆分为多个模块。每个模块都应该作为一个单元进行测试

尝试根据特性/功能视图(而不是类型)来定义模块。有关详细信息,请参阅Misko的演示文稿

模块组件可以按类型(如控制器、模型、视图、筛选器、指令等)进行常规分组。

但模块本身仍然是可重用的,可转移的可测试的

开发人员也更容易找到代码的某些部分及其所有依赖项。

有关详细信息,请参阅大型 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

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

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


答案 2

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

MVC及其衍生产品(MVP,PM,MVVM)在单个代理中都很好,但是服务器 - 客户端架构在所有方面都是双代理系统,人们经常如此痴迷于这些模式,以至于他们忘记了手头的问题要复杂得多。通过尝试遵守这些原则,他们实际上最终会得到一个有缺陷的架构。

让我们一点一点地做这件事。

准则

视图

在 Angular 上下文中,视图是 DOM。这些准则是:

狐狸

  • 显示范围变量(只读)。
  • 调用控制器以获取操作。

不要:

  • 放任何逻辑。

作为诱人,简短,无害的这看起来:

ng-click="collapsed = !collapsed"

这几乎意味着任何开发人员现在要了解系统的工作原理,他们需要检查Javascript文件和HTML文件。

控制器

狐狸

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

不要:

  • 处理任何业务逻辑。

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

你可以争辩说指令是可重用的,但指令也是视图(DOM)的姐妹 - 它们从未打算与实体相对应。

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

换句话说,控制器应该专注于表示 - 如果你把业务逻辑放进去,你不仅可能最终会得到一个膨胀的、几乎不可管理的控制器,而且还违反了关注点分离原则。

因此,Angular中的控制器实际上更像是表示模型MVVM

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

什么是模型?

您的客户模型通常是部分和陈旧的

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

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

真实模型必须持久存在

在传统的MCV中,模型是唯一被持久化的东西。每当我们谈论模型时,这些必须在某个时候持续存在。您的客户端可以随意操作模型,但在成功完成到服务器的往返之前,工作不会完成。

后果

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

因此,在客户端上下文中,使用小写字母可能是明智的 - 所以它实际上是mVCmVPmVVm。大是针对服务器的。MM

业务逻辑

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

  • 域逻辑 - 也称为企业业务规则,即独立于应用程序的逻辑。例如,给定一个具有 和 属性的模型,一个 getter like 可以被视为独立于应用程序的。firstNamesirNamegetFullName()
  • 应用程序逻辑 - 也称为应用程序业务规则,它是特定于应用程序的。例如,错误检查和处理。

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

问题仍然存在 - 您在有角度的应用程序中将它们放在何处?

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

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

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

具体示例

请考虑以下 Web 应用程序:

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

现在应该发生一些事情:

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

我们把这些都扔在哪里?

如果您的体系结构涉及调用 的控制器,则所有这些都将在控制器内发生。但是有一个更好的策略。$resource

建议的解决方案

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

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

所以我们在控制器之间添加一个层来$resource,这个层(我们称之为交互器):

  • 是一项服务。在用户的情况下,它可能被称为。UserInteractor
  • 它提供与用例相对应的方法,封装应用程序逻辑
  • 控制向服务器发出的请求。此层不是使用自由格式参数调用$resource的控制器,而是确保向服务器发出的请求返回域逻辑可以对其执行操作的数据。
  • 它使用域逻辑原型修饰返回的数据结构。

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

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