Подтвердить что ты не робот

Угловые контроллеры, шаблон проектирования для СУХОГО кода

Я создал полный пример с целью описания этой проблемы. Мое фактическое приложение даже больше, чем представленная демонстрация, и есть больше сервисов и директив, управляемых каждым контроллером. Это приводит к еще большему количеству повторений кода. Я попытался поместить некоторые комментарии кода для разъяснений, 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();   
})

В основном я переместил всю логику, я мог, на фабрики и директивы. Но теперь в каждом контроллере, который использует определенную директиву, мне нужно, например, поле, которое сохраняет значение, которое изменяет директива. И это настройки. Позже мне нужно схожее поле, чтобы хранить данные, поступающие из службы данных, а сам вызов (метод) тот же.

Это приводит к большому количеству повторений.


Графически я вижу, что текущий пример выглядит следующим образом:

Текущий проект

Хотя я считаю, что правильный дизайн должен выглядеть примерно так:

Ожидаемый дизайн


Я попытался найти какое-то решение здесь, но, похоже, никто не подтвердил. Что я нашел:

Вы не расширяете контроллеры. Если они выполняют одни и те же основные функции, то эти функции необходимо перенести в службу. Эта служба может быть введена в ваши контроллеры.

И даже когда я это сделал, все еще много повторений. Или так оно и должно быть? Как и John Papa sais (http://www.johnpapa.net/angular-app-structuring-guidelines/):

Попробуйте остаться сухим (не повторяйте себя) или T-DRY

Вы столкнулись с подобной проблемой? Каковы варианты?

4b9b3361

Ответ 1

С точки зрения всего дизайна я не вижу большой разницы между украшением контроллера и расширением контроллера. В конце концов, это как форма миксинов, так и не наследование. Так что дело доходит до того, с чем вам лучше всего работать. Одно из решений большого дизайна сводится не только к тому, как передавать функциональность только всем контроллерам, но также и передавать функциональность, чтобы сказать 2 из 3 контроллеров.

Factory Decorator

Один из способов сделать это, как вы упомянули, - передать вашу $scope или vm в factory, который украсит ваш контроллер дополнительными методами и полями. Я не вижу в этом грязного решения, но я понимаю, почему некоторые люди хотели бы отделить фабрики от их $scope, чтобы разделить проблемы с их кодом. Если вам нужно добавить дополнительную функциональность в сценарий 2 из 3, вы можете перейти на дополнительные фабрики. Я сделал пример plunker этого.

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.

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-маршрутизатор с вложенными состояниями

Поскольку вы используете ui-router, вы также можете добиться аналогичных результатов в состояниях вложенности. Одно из предостережений заключается в том, что область $не передается от родительского к дочернему контроллеру. Поэтому вместо этого вы должны добавить дубликат кода в $rootScope. Я использую это, когда есть функции, которые я хочу передать через всю программу, например, функцию для проверки, если мы находимся на мобильном телефоне, который не зависит от каких-либо контроллеров. Вот пример plunker.

Ответ 2

Вы можете уменьшить количество своего шаблона, используя директиву. Я создал простой, чтобы заменить все ваши контроллеры. Вы просто передаете данные по конкретным страницам через свойства, и они будут привязаны к вашей области.

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

И это в значительной степени. Я развернул ваш Plunk, чтобы продемонстрировать. http://plnkr.co/edit/NEqXeD?p=preview

РЕДАКТИРОВАТЬ: Забыл добавить, что вы также можете настроить директиву по своему усмотрению. Забыл добавить это в Plunk, когда я удалял избыточный код.

Ответ 3

Я не могу ответить в комментарии, но вот что я буду делать:

У меня будет A ConfigFactory, содержащий карту зависимых от страницы переменных:

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

Тогда у меня будет LogicFactory с помощью метода newInstance(), чтобы каждый раз, когда мне это нужно, получить соответствующий объект. Функция logicFactory получит все общие данные/методы, используемые между контроллерами. Для этого LogicFactory я дам данные, относящиеся к просмотру. и представление должно будет привязываться к этому Factory.

И чтобы получить данные, специфичные для представления, я передам ключ моей карты конфигурации в маршрутизаторе.

так что скажем, маршрутизатор дает вам # current = theOne, я буду делать в контроллере:

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

Надеюсь, что это поможет

Я ретуширую ваш пример, вот результат: http://plnkr.co/edit/ORzbSka8YXZUV6JNtexk?p=preview

Изменить. Чтобы сказать это, вы можете загрузить определенную конфигурацию с удаленного сервера, обслуживающего данные определенного вида

Ответ 4

Я столкнулся со всеми теми же проблемами, что и вы описали. Я очень большой сторонник сохранения вещей СУХОЙ. Когда я начал использовать Angular, не было предписанного или рекомендованного способа сделать это, поэтому я только что переработал свой код, когда пошел. Как и во многих вещах, я не думаю, что это правильный или неправильный способ сделать это, поэтому используйте тот метод, с которым вам удобно. Итак, вот что я использовал, и это хорошо мне помогло.

В моих приложениях я обычно имею три типа страниц:

  • Страница списка - список таблиц определенного ресурса. Ты можешь поиск/фильтрация/сортировка данных.
  • Страница формы - создание или редактирование ресурса.
  • Страница отображения - подробная страница просмотра только ресурса/данных.

Я обнаружил, что в (1) и (2), как правило, много повторяющегося кода, и я не имею в виду функции, которые следует извлечь службе. Поэтому для решения этой проблемы я использую следующую иерархию наследования:

  • Список страниц

    • BaseListController
      • loadNotification()
      • поиск()
      • advancedSearch()
      • и т.д....
    • ResourceListController
      • любой ресурсоемкий материал
  • Страницы форм

    • BaseFormController
      • setServerErrors()
      • clearServerErrors()
      • прочее, как предупреждение, пользователь переходит от этой страницы до сохранения формы и любых других общих функций.
    • AbstractFormController
      • сохранить()
      • processUpdateSuccess()
      • processCreateSuccess()
      • processServerErrors()
      • установить любые другие общие параметры
    • ResourceFormController
      • любой ресурсоемкий материал

Чтобы включить это, вам понадобятся некоторые соглашения. Обычно у меня есть только один шаблон представления для каждого ресурса для страниц форм. Используя функцию router resolve, я передаю переменную, чтобы указать, используется ли форма для целей Create или Edit, и я публикую ее на моем vm. Затем это можно использовать внутри вашего AbstractFormController для вызова или сохранения в службе данных.

Чтобы реализовать наследование контроллера, я использую функцию Angulars $injector.invoke, проходящую в this как экземпляр. Поскольку $injector.invoke является частью инфраструктуры DIU с угловым выражением, он отлично работает, поскольку он будет обрабатывать любые зависимости, которые необходимы классам базового контроллера, и я могу предоставить любые конкретные переменные экземпляра, как мне нравится.

Вот небольшой фрагмент того, как все это реализовано:

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-интерфейсе сервера, вы можете пройти очень длинный путь с базовым factory/репозиторием/классом или тем, что вы хотите назвать. То, как я достигаю этого в AngularJs, - использовать AngularJs factory, который возвращает класс базового репозитория, т.е. factory возвращает функцию класса 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 или $ресурсу или любой другой библиотеке, которую вы используете.

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.

Ответ 5

Подводя итог предыдущим ответам:

  • Декорирующие контроллеры: как вы сказали, это грязное решение; Представьте себе, что у разных фабрик, украшающих один и тот же контроллер, будет очень сложно (особенно для других разработчиков) предотвратить столкновение свойств, а также трудно проследить, какие factory добавили какие свойства. На самом деле это похоже на наличие множественного наследования в ООП, что большинство современных языков предотвращает дизайн по тем же причинам.

  • Использование директивы: это может быть отличным решением, если все ваши контроллеры будут иметь одни и те же html-представления, но кроме этого вам придется включать довольно сложную логику в свои представления, которые могут быть трудно отлаживать.


Подход, который я предлагаю, - это использование композиции (вместо наследования с декораторами). Отделите всю повторяющуюся логику на фабриках и оставите только создание фабрик в контроллере.

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), и все очевидно в коде контроллера.

Вы можете сделать свой контроллер еще меньше, обернув все фабрики в более крупный factory (фасад), но это может сделать их более плотно связанными.