AngularJS:了解设计模式

AngularJS: Understanding design pattern

提问人:Artem Platonov 提问时间:11/29/2013 最后编辑:duplodeArtem Platonov 更新时间:6/4/2017 访问量:98443

问:

在 AngularJS 负责人 Igor Minar 的这篇文章中:

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

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

能够对框架进行分类并将其放入其中一个 MV* 存储桶中具有一些优势。 它可以帮助开发人员更熟悉它的 API,方法是让它 更容易创建代表应用程序的心智模型 正在使用该框架构建。它还可以帮助建立 开发人员使用的术语。

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

Angular 为您提供了很大的灵活性,可以很好地分离演示文稿 业务逻辑和表示状态中的逻辑。请使用燃料 您的生产力和应用程序可维护性,而不是加热 讨论在一天结束时无关紧要的事情 多。

是否有任何建议或指南在客户端应用程序中实现 AngularJS MVW(模型-视图-随因)设计模式?

JavaScript AngularJS 设计模式 架构 客户端

评论

0赞 Farhan stands with Palestine 8/23/2015
赞成...而不是看到他们浪费时间争论 MV* 废话。
1赞 usefulBee 9/11/2015
你不需要 Angular 来遵循词类设计模式。

答:

225赞 Artem Platonov 11/29/2013 #1

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


控制器

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

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

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

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

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

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


范围

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

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


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
  };
});

创建新实例

尽量避免让工厂返回一个新的 able 函数,因为这会开始破坏依赖注入,并且库会表现得很笨拙,尤其是对于第三方。

完成相同操作的更好方法是将工厂用作 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 指令一起使用时,这可能会导致意外行为,通常会乱扔范围并导致范围方法覆盖问题。


资源

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

应使用单一责任原则来实施。

在特定情况下,它是 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 应用程序结构的一个很好的例子是由 angular-app 实现的 - https://github.com/angular-app/angular-app/tree/master/client/src

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

评论

5赞 op1ekun 4/8/2014
我有一个担忧:“强烈建议避免在控制器中建立业务逻辑。它应该被移动到模型中。但是,从官方文档中,您可以读到:“一般来说,控制器不应该尝试做太多事情。它应该只包含单个视图所需的业务逻辑。我们在谈论同一件事吗?
3赞 Artem Platonov 4/8/2014
我会说 - 将控制器视为视图模型。
1赞 Dmitri Zaitsev 4/21/2014
+1.这里有一些很好的建议!2. 不幸的是,这个例子没有遵循可重用性建议。最好通过服务导入常量。3. 任何解释这里是什么意思?searchModelconstantTry to avoid having a factory that returns a new able function
1赞 Dmitri Zaitsev 4/21/2014
此外,覆盖对象的属性会破坏继承,而是可以使用prototypeCar.prototype.save = ...
2赞 Artem Platonov 5/18/2015
@ChristianAichinger,这是关于 JavaScript 原型链的本质,它迫使您在双向绑定表达式中使用 以确保写入确切的属性或函数。如果使用作用域的直接属性(不带点),则在写入目标属性时,可能会将所需的目标属性与新创建的目标属性一起隐藏在原型链中最近的上层作用域中。这在Misko的演讲中得到了更好的解释objectsetter
5赞 Dmitri Zaitsev 4/21/2014 #2

与 Artem 的回答中的优秀建议相比,这是一个小问题,但就代码可读性而言,我发现最好完全在对象内部定义 API,以尽量减少在代码中来回查看是否定义了更详细的变量:return

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

47赞 Izhaki 6/10/2015 #3

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

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

让我们一点一点地做。

准则

视图

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

狐狸

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

不要:

  • 放任何逻辑。

这看起来很诱人、简短且无害:

ng-click="collapsed = !collapsed"

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

控制器

狐狸

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

不要:

  • 处理任何业务逻辑。

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

你可以争辩说指令是可重用的,但指令也是视图 (DOM) 的姊妹——它们从来都不是为了对应实体。

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

换言之,控制者应该专注于呈现——如果你把业务逻辑扔进去,你不仅可能最终得到一个膨胀的、难以管理的控制者,而且你也违反了关注点分离原则。

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

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

什么是模型?

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

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

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

真实模型必须持久化

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

后果

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

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

业务逻辑

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

  • 域逻辑 - 又名企业业务规则,独立于应用程序的逻辑。例如,给定一个具有 和 属性的模型,像 getter 这样的 getter 可以被认为是与应用程序无关的。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
  • 响应后,交互器将任何错误委托给控制器,由控制器处理它们。
  • 成功响应后,交互者将确保在需要时更新用户列表。

评论

0赞 BAD_SEED 9/10/2015
所以 AngularJS 被定义为 MVW(其中 W 代表什么),因为我可以选择在单独的服务中拥有一个带有 BL 的控制器(其中包含所有业务逻辑)或视图模型/表示器(没有业务逻辑,但只是一些代码来填充视图)?我说得对吗?
0赞 RPallas 9/18/2015
最佳答案。你在 GitHub 上有一个 4 层 angular 应用程序的真实示例吗?
1赞 Izhaki 9/18/2015
@RPallas,不,我没有(希望我有时间)。我们目前正在尝试一种架构,其中“应用程序逻辑”只是一个边界交互器;它与控制器之间的解析器,以及具有某种视图逻辑的视图模型。我们仍在试验中,所以不是 100% 的利弊。但是一旦完成,我希望在某个地方写一个博客。
1赞 Izhaki 7/27/2016
@heringer 基本上,我们引入了模型 - 表示领域实体的 OOP 构造。正是这些模型与资源通信,而不是与控制器通信。它们封装了域逻辑。控制器调用模型,而模型又调用资源。
1赞 Izhaki 12/3/2016
@alex440 编号虽然现在已经两个月了,一篇关于这个话题的严肃博客文章就在我的指尖。圣诞节快到了——可能到那时。
0赞 Ashutosh 3/4/2017 #4

AngularJS 没有以传统方式实现 MVC,而是实现了更接近 MVVM(Model-View-ViewModel)的东西,ViewModel 也可以称为 binder(在 angular 的情况下它可以是 $scope)。 模型--> 正如我们所知,angular 中的模型可以只是普通的旧 JS 对象或我们应用程序中的数据

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

ViewModel--> ViewModel 实际上是 angularJS 中视图和模型之间的绑定器/桥梁,它$scope,用于初始化和增强我们使用 Controller $scope。

如果我想总结答案:在angularJS应用程序中,$scope具有对数据的引用,控制器控制行为,而View通过与控制器交互来处理布局以采取相应的行为。

-1赞 Naveen Reddy 3/8/2017 #5

为了明确这个问题,Angular 使用了我们在常规编程中已经遇到的不同设计模式。 1) 当我们注册与我们的模块相关的控制器或指令、工厂、服务等时。在这里,它从全局空间中隐藏了数据。这是模块模式。 2) 当 angular 使用其脏检查来比较范围变量时,这里它使用 Observer Pattern。 3) 我们控制器中的所有父子作用域都使用 Prototypal 模式。 4)在注入服务的情况下,它使用工厂模式

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