AngularJS控制器,DRY代码的设计模式

时间:2015-11-25 14:13:26

标签: javascript angularjs design-patterns dry angularjs-controller

为了描述这个问题,我创建了一个完整的示例。我的实际应用程序甚至比演示的演示版还要大,每个控制器都有更多的服务和指令。这导致更多的代码重复。我试着把一些代码注释用于澄清, PLUNKER http://plnkr.co/edit/781Phn?p=preview

重复部分

routerApp.controller('page1Ctrl', function(pageFactory) {
  var vm = this;

  // page dependent
  vm.name = 'theOne';
  vm.service = 'oneService';
  vm.seriesLabels = ['One1', 'Two1', 'Three1'];

  // these variables are declared in all pages
  // directive variables,
  vm.date = {
    date: new Date(),
    dateOptions: {
      formatYear: 'yy',
      startingDay: 1
    },
    format: 'dd-MMMM-yyyy',
    opened: false
  };

  vm.open = function($event) {
    vm.date.opened = true;
  };

  // dataservice
  vm.data = []; // the structure can be different but still similar enough
  vm.update = function() {
      vm.data = pageFactory.get(vm.service);
    }

  //default call
  vm.update();   
})

基本上我把所有的逻辑都移到了工厂和指令。但现在在每个使用我需要的指令的控制器中,例如,一个字段保存指令正在修改的值。这是设置。后来我需要类似的字段来保存来自dataservice的数据,并且调用本身(方法)也是一样的。

这导致了很多重复。

以图形方式我看到当前示例如下所示:

The current design

虽然我认为正确的设计应该更像这样:

The expected design

我试图在这里找到一些解决方案,但似乎都没有得到证实。我发现了:

  1. AngularJS DRY controller structure,建议我传递$ scope或vm并使用额外的方法和字段来装饰它。但许多消息人士称这是肮脏的解决方案
  2. What's the recommended way to extend AngularJS controllers?使用angular.extend,但在使用controller as语法时会出现问题。
  3. 然后我也找到了答案(在上面的链接中):
  4.   

    您不会扩展控制器。如果它们执行相同的基本功能,则需要将这些功能移动到服务中。该服务可以注入您的控制器。

    即使我做了,仍然会有很多重复。或者它是否必须如此?像John Papa sais(http://www.johnpapa.net/angular-app-structuring-guidelines/):

      

    尽量保持干燥(不要重复自己)或T-DRY

    你有没有遇到过类似的问题?有什么选择?

5 个答案:

答案 0 :(得分:8)

从整体设计的角度来看,我没有看到装饰控制器和扩展控制器之间的区别。最后,这些都是mixins而不是继承的形式。因此,它真正归结为您最熟悉的工作。其中一个重大的设计决策不仅仅涉及如何将功能传递给所有控制器,还包括如何传递功能来说明3个控制器中的2个。

工厂装饰工程师

正如您所提到的,实现此目的的一种方法是将$ scope或vm传递到工厂,该工厂使用额外的方法和字段来装饰您的控制器。我不认为这是一个肮脏的解决方案,但我可以理解为什么有些人会想要将工厂从他们的范围中分离出来以分离他们代码的问题。如果您需要为3个场景中的2个添加其他功能,则可以传入其他工厂。我做了plunker example of this

dataservice.js

routerApp.factory('pageFactory', function() {

    return {
      setup: setup
    }

    function setup(vm, name, service, seriesLabels) {
      // page dependent
      vm.name = name;
      vm.service = service;
      vm.seriesLabels = seriesLabels;

      // these variables are declared in all pages
      // directive variables,
      vm.date = {
        date: moment().startOf('month').valueOf(),
        dateOptions: {
          formatYear: 'yy',
          startingDay: 1
        },
        format: 'dd-MMMM-yyyy',
        opened: false
      };

      vm.open = function($event) {
        vm.date.opened = true;
      };

      // dataservice
      vm.data = []; // the structure can be different but still similar enough
      vm.update = function() {
        vm.data = get(vm.service);
      }

      //default call
      vm.update();
    }

});

page1.js

routerApp.controller('page1Ctrl', function(pageFactory) {
    var vm = this;
    pageFactory.setup(vm, 'theOne', 'oneService', ['One1', 'Two1', 'Three1']);
})

扩展控制器

您提到的另一个解决方案是扩展控制器。这可以通过创建一个混合到正在使用的控制器的超级控制器来实现。如果您需要为特定控制器添加其他功能,您可以将其他超级控制器与特定功能混合使用。这是一个plunker example

ParentPage

routerApp.controller('parentPageCtrl', function(vm, pageFactory) {

    setup()

    function setup() {

      // these variables are declared in all pages
      // directive variables,
      vm.date = {
        date: moment().startOf('month').valueOf(),
        dateOptions: {
          formatYear: 'yy',
          startingDay: 1
        },
        format: 'dd-MMMM-yyyy',
        opened: false
      };

      vm.open = function($event) {
        vm.date.opened = true;
      };

      // dataservice
      vm.data = []; // the structure can be different but still similar enough
      vm.update = function() {
        vm.data = pageFactory.get(vm.service);
      }

      //default call
      vm.update();
    }

})

page1.js

routerApp.controller('page1Ctrl', function($controller) {
    var vm = this;
    // page dependent
    vm.name = 'theOne';
    vm.service = 'oneService';
    vm.seriesLabels = ['One1', 'Two1', 'Three1'];
    angular.extend(this, $controller('parentPageCtrl', {vm: vm}));
})

嵌套状态UI-Router

由于您使用的是ui-router,因此您还可以通过嵌套状态获得类似的结果。有一点需要注意的是$ scope不会从父控制器传递给子控制器。因此,您必须在$ rootScope中添加重复的代码。当我想要传递整个程序的函数时,我会使用它,例如测试我们是否在移动电话上的功能,不依赖于任何控制器。这是plunker example

答案 1 :(得分:3)

您可以使用指令减少很多样板。我创建了一个简单的替换所有控制器。您只需通过属性传递特定于页面的数据,它们就会绑定到您的范围。

routerApp.directive('pageDir', function() {
  return {
    restrict: 'E',
    scope: {},
    controller: function(pageFactory) {
      vm = this;
      vm.date = {
        date: moment().startOf('month').valueOf(),
        dateOptions: {
          formatYear: 'yy',
          startingDay: 1
        },
        format: 'dd-MMMM-yyyy',
        opened: false
      };

      vm.open = function($event) {
        vm.date.opened = true;
      };

      // dataservice
      vm.data = []; // the structure can be different but still similar enough
      vm.update = function() {
        vm.data = pageFactory.get(vm.service);
      };

      vm.update();
    },
    controllerAs: 'vm',
    bindToController: {
      name: '@',
      service: '@',
      seriesLabels: '='
    },
    templateUrl: 'page.html',
    replace: true
  }
});

正如您所看到的,它与您的控制器没有多大区别。不同的是,要使用它们,您将在路由的template属性中使用该指令来初始化它。像这样:

    .state('state1', {
        url: '/state1',
        template: '<page-dir ' +
          'name="theOne" ' +
          'service="oneService" ' +
          'series-labels="[\'One1\', \'Two1\', \'Three1\']"' +
          '></page-dir>'
    })

这就是它。我分叉你的普兰克来演示。 http://plnkr.co/edit/NEqXeD?p=preview

编辑:忘了添加你也可以根据需要设置指令的样式。当我删除冗余代码时,忘了将它添加到Plunk中。

答案 2 :(得分:2)

我无法回答评论,但我会做什么:

我将拥有一个ConfigFactory,其中包含页面因变量的映射:

{
  theOne:{
      name: 'theOne',
      service: 'oneService',
      seriesLabels: ['One1', 'Two1', 'Three1']
  },
  ...
}

然后我将有一个带有newInstance()方法的LogicFactory,以便在每次需要时获取正确的对象。 logicFactory将获得控制器共享的所有数据/方法。 对于这个LogicFactory,我将提供特定于视图的数据。并且视图必须绑定到此Factory。

要检索特定于视图的数据,我将在路由器中传递配置图的密钥。

所以说路由器给你#current = theOne,我将在控制器中执行:

var specificData = ServiceConfig.get($location.search().current);
this.logic = LogicFactory.newInstance(specificData);

希望有所帮助

我修饰你的例子,结果如下:http://plnkr.co/edit/ORzbSka8YXZUV6JNtexk?p=preview

  

编辑:就是这样说,您可以从为您提供特定视图数据的远程服务器加载特定配置

答案 3 :(得分:2)

我遇到了与你描述的完全相同的问题。我是保持干燥的非常大的支持者。当我开始使用Angular时,没有规定的或推荐的方法来做到这一点,所以我只是在我进行时重构了我的代码。和许多事情一样,我不认为他们做这些事情是对或错的,所以使用你觉得舒服的方法。以下是我最终使用的内容,它对我有好处。

在我的应用程序中,我通常有三种类型的页面:

  1. 列表页面 - 特定资源的表格列表。您可以 搜索/过滤/排序您的数据。
  2. 表单页面 - 创建或编辑资源。
  3. 显示页面 - 资源/数据的详细查看显示页面。
  4. 我发现在(1)和(2)中通常有很多重复代码,而我并没有提到应该提取给服务的功能。所以要解决我使用以下继承层次结构的问题:

    1. 列出页面

      • BaseListController
        • loadNotification()
        • 搜索()
        • 填写AdvancedSearch()
        • 等...
      • 资源 ListController
        • 任何资源特定的东西
    2. 表单页

      • BaseFormController
        • setServerErrors()
        • clearServerErrors()
        • 警告用户在保存表单和任何其他常规功能之前导航离开此页面。
      • 的AbstractFormController
        • 保存()
        • processUpdateSuccess()
        • processCreateSuccess()
        • processServerErrors()
        • 设置任何其他共享选项
      • 资源的FormController
        • 任何资源特定的东西
    3. 要启用此功能,您需要一些约定。对于表单页面,我通常每个资源只有一个视图模板。使用路由器resolve功能,我传入一个变量来指示表单是用于创建还是编辑目的,然后将其发布到我的vm。然后,可以在AbstractFormController内使用此功能来调用数据服务上的保存或更新。

      为了实现控制器继承,我使用以$injector.invoke作为实例传入的Angulars this函数。由于$injector.invoke是Angulars DI基础结构的一部分,因此它可以很好地处理基本控制器类所需的任何依赖项,并且我可以提供任何特定的实例变量。

      以下是一个如何实现的小片段:

      Common.BaseFormController = function (dependencies....) {
          var self = this;
          this.setServerErrors = function () {
          };
          /* .... */
      };
      
      Common.BaseFormController['$inject'] = [dependencies....];
      
      Common.AbstractFormController = function ($injector, other dependencies....) {
          $scope.vm = {};
          var vm = $scope.vm;
          $injector.invoke(Common.BaseFormController, this, { $scope: $scope, $log: $log, $window: $window, alertService: alertService, any other variables.... });
         /* ...... */
      }
      
      Common.AbstractFormController['$inject'] = ['$injector', other dependencies....];
      
      CustomerFormController = function ($injector, other dependencies....) {
          $injector.invoke(Common.AbstractFormController, this, {
                  $scope: $scope,
                  $log: $log,
                  $window: $window,
                  /* other services and local variable to be injected .... */
              });
      
          var vm = $scope.vm;
          /* resource specific controller stuff */
      }
      
      CustomerFormController['$inject'] = ['$injector', other dependencies....];
      

      为了更进一步,我发现通过我的数据访问服务实现大量减少了重复代码。对于数据层,约定是王道。我发现,如果你在服务器API上保持一个共同的约定,那么你可以使用基础工厂/存储库/类或任何你想要它的方式走很长的路。我在AngularJs中实现这一点的方法是使用一个返回基本存储库类的AngularJs工厂,即工厂返回一个带有原型定义而不是对象实例的javascript类函数,我称之为abstractRepository。然后,对于每个资源,我为原型继承自abstractRepository的特定资源创建一个具体的存储库,因此我从abstractRepository继承了所有共享/基本功能,并为具体存储库定义了任何特定于资源的功能。

      我认为一个例子会更清楚。让我们假设您的服务器API使用以下URL约定(我不是最纯粹的REST,因此我们将约定留给您想要实现的任何内容):

      GET  -> /{resource}?listQueryString     // Return resource list
      GET  -> /{resource}/{id}                // Return single resource
      GET  -> /{resource}/{id}/{resource}view // Return display representation of resource
      PUT  -> /{resource}/{id}                // Update existing resource
      POST -> /{resource}/                    // Create new resource
      etc.
      

      我个人使用Restangular,所以下面的示例基于它,但您应该能够轻松地将其调整为$ http或$ resource或您正在使用的任何库。

      <强> AbstractRepository

      app.factory('abstractRepository', [function () {
      
          function abstractRepository(restangular, route) {
              this.restangular = restangular;
              this.route = route;
          }
      
          abstractRepository.prototype = {
              getList: function (params) {
                  return this.restangular.all(this.route).getList(params);
              },
              get: function (id) {
                  return this.restangular.one(this.route, id).get();
              },
              getView: function (id) {
                  return this.restangular.one(this.route, id).one(this.route + 'view').get();
              },
              update: function (updatedResource) {
                  return updatedResource.put();
              },
              create: function (newResource) {
                  return this.restangular.all(this.route).post(newResource);
              }
              // etc.
          };
      
          abstractRepository.extend = function (repository) {
              repository.prototype = Object.create(abstractRepository.prototype);
              repository.prototype.constructor = repository;
          };
      
          return abstractRepository;
      }]);
      

      具体的存储库,让我们以客户为例:

      app.factory('customerRepository', ['Restangular', 'abstractRepository', function (restangular, abstractRepository) {
      
          function customerRepository() {
              abstractRepository.call(this, restangular, 'customers');
          }
      
          abstractRepository.extend(customerRepository);
          return new customerRepository();
      }]);
      

      现在我们有了数据服务的通用方法,可以很容易地在Form和List控制器基类中使用。

答案 4 :(得分:1)

总结以前的答案:

  1. 装饰控制器:如你所说,这是一个肮脏的解决方案;想象一下,如果让不同的工厂装饰同一个控制器,就很难(特别是对于其他开发人员)防止属性碰撞,同样难以追踪哪个工厂添加了哪些属性。它实际上就像在OOP中具有多重继承,这是大多数现代语言出于同样的原因而设计出来的。

  2. 使用指令:如果所有控制器都具有相同的html视图,这可能是一个很好的解决方案,但除此之外,您必须在视图中包含相当复杂的逻辑,这些逻辑很难调试

  3. 我建议的方法是使用合成(而不是使用装饰器继承)。将工厂中的所有重复逻辑分开,只留下控制器中工厂的创建。

    routerApp.controller('page1Ctrl', function (Page, DateConfig, DataService) {
        var vm = this;
    
        // page dependent
        vm.page = new Page('theOne', 'oneService', ['One1', 'Two1', 'Three1']);
    
        // these variables are declared in all pages
        // directive variables,
        vm.date = new DateConfig()
    
        // dataservice
        vm.dataService = new DataService(vm.page.service);
    
        //default call
        vm.dataService.update();
    
    })
    
    .factory('Page', function () {
    
        //constructor function
        var Page = function (name, service, seriesLabels) {
            this.name = name;
            this.service = service;
            this.seriesLabels = seriesLabels;
        };
    
        return Page;
    
    })
    
    
    .factory('DateConfig', function () {
    
        //constructor function
        var DateConfig = function () {
            this.date = new Date();
            this.dateOptions = {
                formatYear: 'yy',
                startingDay: 1
            };
            this.format = 'dd-MMMM-yyyy';
            this.opened = false;
            this.open = function ($event) {
                this.opened = true;
            };
        };
    
        return DateConfig;
    
    })
    

    此代码未经过测试,但我只想提出一个想法。这里的关键是分离工厂中的代码,并将它们作为属性添加到控制器中。这样,实现不会重复(DRY),并且控制器代码中的一切都很明显。

    您可以通过将所有工厂包装在更大的工厂(外观)中来使控制器更小,但这可能会使它们更紧密耦合。