Бабич Татьяна

Руководитель Frontend отдела комании Simbirsoft

Наши проекты

Что такое архитектура?

  • Принцип единственной ответственности (Single responsibility)
  • Принцип открытости/закрытости (Open-closed)
  • Принцип подстановки Барбары Лисков (Liskov substitution)
  • Принцип разделения интерфейса (Interface segregation)
  • Принцип инверсии зависимостей (Dependency Invertion)

S.O.L.I.D.

1. Готова ли ваша архитектура к повторному использованию кода уже сейчас?

2. Сколько модулей в вашей системе зависит от других модулей?

3. Приложение будет работать дальше, если его отдельная часть сломается?

4. Насколько легко вы можете тестировать отдельные модули?

А как у вас дела?

На пути к модульности

Критерии модульности

Приватность

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

 

Тестирование кода

 

Слабая связанность

Модули никак не связаны между собой

 

Загрузка по требованию

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

Структура приложения, принцип LIFT

  1. Возможность легко найти нужный код
  2. Понять назначение файла
  3. Простая структура проекта
  4. Не повторять свой код

I

F

T

-locate

-identify

-flat

-Try to stay DRY

Организация файлов

css/
img/
js/
    app.js
    controllers.js
    directives.js
    filters.js
    services.js
lib/
partials/

по типу файлов

Организация файлов

controllers/
    LoginController.js
    RegistrationController.js
    ProductDetailController.js
    SearchResultsController.js
directives.js
filters.js
models/
    CartModel.js
    ProductModel.js
    SearchResultsModel.js
    UserModel.js
services/
    CartService.js
    UserService.js
    ProductService.js

 каталог для некоторых сущностей

Организация файлов

product/
    search/
        SearchResultsController.js
        SearchResultsModel.js
    ProductDetailController.js
    ProductModel.js
    ProductService.js
user/
    LoginController.js
    RegistrationController.js
    UserModel.js
    UserService.js

Модульность

Правила присваивания имен

profile.controller.js

ИЛИ

ProfileController.js

функционал.тип.js

Принцип единой ответственности

angular
   .module('app', ['ngRoute'])
   .controller('SomeController' , SomeController)
   .factory('someFactory' , someFactory);
 
function SomeController() { }
 
function someFactory() { }
// app.module.js
angular
   .module('app', ['ngRoute']);


// someController.js
angular
   .module('app')
   .controller('SomeController' , SomeController);

function SomeController() { }

// someFactory.js
angular
   .module('app')
   .factory('someFactory' , someFactory);

function someFactory() { }

IIFE

// logger.js
angular
    .module('app')
    .factory('logger', logger);

// Функция logger добавилась 
// в глобальную область видимости
function logger() { }

 // storage.js
angular
    .module('app')
    .factory('storage', storage);

// Функция storage добавилась 
// в глобальную область видимости  
function storage() { }
// logger.js
(function() {
    'use strict';

    angular
        .module('app')
        .factory('logger', logger);

    function logger() { }
})();

// storage.js
(function() {
   'use strict';

   angular
       .module('app')
       .factory('storage', storage);

   function storage() { }
})();

Модули 

var app = angular.module('app', [
   'ngAnimate',
   'ngRoute',
   'app.shared',
   'app.dashboard'
]);
angular
   .module('app', [
       'ngAnimate',
       'ngRoute',
       'app.shared',
       'app.dashboard'
   ]);

Сеттеры

Геттеры

var app = angular.module('app');
app.controller('SomeController' , SomeController);

function SomeController() { }
angular
   .module('app')
   .controller('SomeController' , SomeController);

function SomeController() { }

Именованные / Анонимные функции

angular
   .module('app')
   .controller('Dashboard', function() { });
   .factory('logger', function() { });
// dashboard.js
angular
   .module('app')
   .controller('Dashboard', Dashboard);

function Dashboard() { }

// logger.js
angular
   .module('app')
   .factory('logger', logger);

function logger() { }

Контролеры :: controllerAs 

<div ng-controller="Customer">
   {{ name }}
</div>
<div ng-controller="Customer as customer">
  {{ customer.name }}
</div>

 в шаблонах:

function Customer($scope) {
   $scope.name = {};
   $scope.sendMessage = function() { };
}
function Customer() {
   this.name = {};
   this.sendMessage = function() { };
}

 для контролеров:

function Customer() {
   this.name = {};
   this.sendMessage = function() { };
}
function Customer() {
   var vm = this;
   vm.name = {};
   vm.sendMessage = function() { };
}

vm - ViewModel

Связанные переменные должны располагаться наверху

function Sessions() {
   var vm = this;

   vm.gotoSession = function() {
     /* ... */
   };
   vm.refresh = function() {
     /* ... */
   };
   vm.search = function() {
     /* ... */
   };
   vm.sessions = [];
   vm.title = 'Sessions';
}
function Sessions() {
   var vm = this;

   vm.gotoSession = gotoSession;
   vm.refresh = refresh;
   vm.search = search;
   vm.sessions = [];
   vm.title = 'Sessions';

   ////////////

   function gotoSession() {
     /* */
   }

   function refresh() {
     /* */
   }

   function search() {
     /* */
   }
}

Объявляйте функции для скрытия деталей логики

function Avengers(dataservice, logger) {
   var vm = this;
   vm.avengers = [];
   vm.title = 'Avengers';

   var activate = function() {
       return getAvengers().then(function() {
           logger.info('Activated Avengers View');
       });
   }

   var getAvengers = function() {
       return dataservice.getAvengers()
        .then(function(data) {
           vm.avengers = data;
           return vm.avengers;
       });
   }

   vm.getAvengers = getAvengers;

   activate();
}
function Avengers(dataservice, logger) {
   var vm = this;
   vm.avengers = [];
   vm.getAvengers = getAvengers;
   vm.title = 'Avengers';

   activate();

   function activate() {
       return getAvengers().then(function() {
           logger.info('Activated Avengers View');
       });
   }

   function getAvengers() {
       return dataservice.getAvengers()
        .then(function(data) {
           vm.avengers = data;
           return vm.avengers;
       });
   }
}

Перемещайте логику контролера

function Order($http, $q) {
   var vm = this;
   vm.checkCredit = checkCredit;
   vm.total = 0;

   function checkCredit() { 
       var orderTotal = vm.total;
       return $http.get('api/creditcheck')
            .then(function(data) {
               var remaining = data.remaining;
               return $q.when(!!(remaining > orderTotal));
       });
   };
}
function Order(creditService) {
   var vm = this;
   vm.checkCredit = checkCredit;
   vm.total = 0;

   function checkCredit() { 
      return creditService.check();
   };
}

Разделяйте контролеры

// route-config.js
angular
   .module('app')
   .config(config);

function config($routeProvider) {
   $routeProvider
       .when('/avengers', {
         templateUrl: 'avengers.html'
       });
}


<!-- avengers.html -->
<div ng-controller="Avengers as vm">
</div>
// route-config.js
angular
   .module('app')
   .config(config);

function config($routeProvider) {
   $routeProvider
       .when('/avengers', {
           templateUrl: 'avengers.html',
           controller: 'Avengers',
           controllerAs: 'vm'
       });
}

<!-- avengers.html -->
<div></div>

Службы::Singleton

// service

angular
   .module('app')
   .service('logger', logger);

function logger() {
 this.logError = function(msg) {
   /* */
 };
}

// factory
angular
   .module('app')
   .factory('logger', logger);

function logger() {
   return {
       logError: function(msg) {
         /* */
       }
  };
}
  • фабрики должны отвечать за одну функцию. Как только вам требуется новый функционал, лучше создать новую фабрику.

  • фабрики - singleton, возвращающие объект, который содержит свойства и методы службы.

  • Все службы AngularJS - одиночки

  • располагайте публичные свойства и методы службы вверху файла.

Фабрики

function dataService() {
 var someValue = '';
 function save() { 
   /* */
 };
 function validate() { 
   /* */
 };

 return {
     save: save,
     someValue: someValue,
     validate: validate
 };
}
function dataService() {
   var someValue = '';
   var service = {
       save: save,
       someValue: someValue,
       validate: validate
   };
   return service;

   ////////////

   function save() { 
       /* */
   };

   function validate() { 
       /* */
   };
}

Объявление функций для скрытия деталей реализации

function dataservice($http, $location, $q, exception, logger) {
   var isPrimed = false;
   var primePromise;

   var getAvengers = function() {
      // implementation details go here
   };

   var getAvengerCount = function() {
       // implementation details go here
   };

   var getAvengersCast = function() {
      // implementation details go here
   };

   var prime = function() {
      // implementation details go here
   };

   var ready = function(nextPromises) {
       // implementation details go here
   };

   var service = {
       getAvengersCast: getAvengersCast,
       getAvengerCount: getAvengerCount,
       getAvengers: getAvengers,
       ready: ready
   };

   return service;
}
function dataservice($http, $location, $q, exception, logger) {
   var isPrimed = false;
   var primePromise;

   var service = {
       getAvengersCast: getAvengersCast,
       getAvengerCount: getAvengerCount,
       getAvengers: getAvengers,
       ready: ready
   };

   return service;

   ////////////

   function getAvengers() {
      // реализация функции
   }

   function getAvengerCount() {
       // реализация функции
   }

   function getAvengersCast() {
      // реализация функции
   }

   function prime() {
       // реализация функции
   }

   function ready(nextPromises) {
       // реализация функции
   }
}

Службы данных

// dataservice factory
angular
   .module('app.core')
   .factory('dataservice', dataservice);

dataservice.$inject = ['$http', 'logger'];

function dataservice($http, logger) {
   return {
       getAvengers: getAvengers
   };

   function getAvengers() {
       return $http.get('/api/maa')
           .then(getAvengersComplete)
           .catch(getAvengersFailed);

       function getAvengersComplete(response) {
           return response.data.results;
       }

       function getAvengersFailed(error) {
           logger.error('XHR Failed for getAvengers.' 
                        + error.data);
       }
   }
}
// controller calling the dataservice factory
angular
   .module('app.avengers')
   .controller('Avengers', Avengers);

Avengers.$inject = ['dataservice', 'logger'];

function Avengers(dataservice, logger) {
   var vm = this;
   vm.avengers = [];

   activate();

   function activate() {
       return getAvengers().then(function() {
           logger.info('Activated Avengers View');
       });
   }

   function getAvengers() {
       return dataservice.getAvengers()
           .then(function(data) {
               vm.avengers = data;
               return vm.avengers;
           });
   }
}     

Возвращайте promise

activate();

function activate() {
   /**
     * Step 1
     * Ask the getAvengers function for the
     * avenger data and wait for the promise
     */
   return getAvengers().then(function() {
       /**
         * Step 4
         * Perform an action on resolve of final promise
         */
       logger.info('Activated Avengers View');
   });
}

function getAvengers() {
     /**
       * Step 2
       * Ask the data service for the data and wait
       * for the promise
       */
     return dataservice.getAvengers()
         .then(function(data) {
             /**
               * Step 3
               * set the data and resolve the promise
               */
             vm.avengers = data;
             return vm.avengers;
     });
}

Директивы

/* directives.js */

angular
   .module('app.widgets')

   /* order directive that is specific to the order module */
   .directive('orderCalendarRange', orderCalendarRange)

   /* sales directive that can be used anywhere across the sales app */
   .directive('salesCustomerInfo', salesCustomerInfo)

   /* spinner directive that can be used anywhere across apps */
   .directive('sharedSpinner', sharedSpinner);

function orderCalendarRange() {
   /* implementation details */
}

function salesCustomerInfo() {
   /* implementation details */
}

function sharedSpinner() {
   /* implementation details */
}
/* calendarRange.directive.js */

angular
   .module('sales.order')
   .directive('acmeOrderCalendarRange', orderCalendarRange);

function orderCalendarRange() {
   /* implementation details */
}

/* customerInfo.directive.js */
   
angular
   .module('sales.widgets')
   .directive('acmeSalesCustomerInfo', salesCustomerInfo);

function salesCustomerInfo() {
   /* implementation details */
}

/* spinner.directive.js */

angular
   .module('shared.widgets')
   .directive('acmeSharedSpinner', sharedSpinner);

function sharedSpinner() {
   /* implementation details */
}

Элемент или атрибут

<div class="my-calendar-range"></div>

angular
   .module('app.widgets')
   .directive('myCalendarRange', myCalendarRange);

function myCalendarRange() {
   var directive = {
       link: link,
       templateUrl: '/template/here.html',
       restrict: 'C'
   };
   return directive;

   function link(scope, element, attrs) {
     /* */
   }
}
<my-calendar-range></my-calendar-range>
<div my-calendar-range></div>

angular
   .module('app.widgets')
   .directive('myCalendarRange', myCalendarRange);

function myCalendarRange() {
   var directive = {
       link: link,
       templateUrl: '/template/here.html',
       restrict: 'EA'
   };
   return directive;

   function link(scope, element, attrs) {
     /* */
   }
}

Директивы и ControllerAs

promise в контролере

function Avengers(dataservice) {
   var vm = this;
   vm.avengers = [];
   vm.title = 'Avengers';

   dataservice.getAvengers().then(function(data) {
       vm.avengers = data;
       return vm.avengers;
   });
}
function Avengers(dataservice) {
   var vm = this;
   vm.avengers = [];
   vm.title = 'Avengers';

   activate();

   ////////////

   function activate() {
       return dataservice.getAvengers()
        .then(function(data) {
           vm.avengers = data;
           return vm.avengers;
       });
   }
}

Аннотация внедрения зависимостей

angular
   .module('app')
   .controller('Dashboard', Dashboard);

function Dashboard(common, dataservice) {
}
angular
   .module('app')
   .controller('Dashboard', 
       ['$location', '$routeParams', 'common', 'dataservice', 
           function Dashboard($location, $routeParams, 
                common, dataservice) {}
       ]);      
angular
 .module('app')
 .controller('Dashboard', 
    ['$location', '$routeParams', 'common', 'dataservice', Dashboard]);

function Dashboard($location, $routeParams, common, dataservice) {
}

Объявляйте зависимости вручную

angular
   .module('app')
   .controller('Dashboard', Dashboard);

Dashboard.$inject = ['$location', '$routeParams', 
                    'common', 'dataservice'];

function Dashboard($location, $routeParams, 
                    common, dataservice) {
}
angular.module('app').controller('Dashboard', d);function d(a, b) { }

Не безопасно!

Минификация и аннотация

angular
   .module('app')
   .controller('Avengers', Avengers);

/* @ngInject */
function Avengers(storageService, avengerService) {
   var vm = this;
   vm.heroSearch = '';
   vm.storeHero = storeHero;

   function storeHero(){
       var hero = avengerService.find(vm.heroSearch);
       storageService.save(hero.name, hero);
   }
}
angular
   .module('app')
   .controller('Avengers', Avengers);

/* @ngInject */
function Avengers(storageService, avengerService) {
   var vm = this;
   vm.heroSearch = '';
   vm.storeHero = storeHero;

   function storeHero(){
       var hero = avengerService.find(vm.heroSearch);
       storageService.save(hero.name, hero);
   }
}

Avengers.$inject = ['storageService', 'avengerService'];

 ng-annotate  /** @ngInject */

Gulp или Grunt с ng-annotate

gulp.task('js', ['jshint'], function() {
   var source = pkg.paths.js;
   return gulp.src(source)
       .pipe(sourcemaps.init())
       .pipe(concat('all.min.js', {newLine: ';'}))
       // Annotate before uglify so the code get's min'd properly.
       .pipe(ngAnnotate({
           // true helps add where @ngInject is not used. It infers.
           // Doesn't work with resolve, so we must be explicit there
           add: true
       }))
       .pipe(bytediff.start())
       .pipe(uglify({mangle: true}))
       .pipe(bytediff.stop())
       .pipe(sourcemaps.write('./'))
       .pipe(gulp.dest(pkg.paths.dev));
});

Анализ кода

JSHint + jsDoc

 +

ТЕСТЫ

Генерация приложения

generator-gulp-angular

https://www.npmjs.com/package/generator-gulp-angular

  • browserSync: Live Reload инструмент
  • ngAnnotate
  • angular-templatecache: конвертация HTML в JS
  • ESLint: проверка кода
  • watch: отслеживание изменение и перекомпиляция
  • useref: парсит блоки и конкатенирует описанные в них стили и скрипты
  • uglify: оптимизация JS
  • clean-css: оптимизация CSS
  • rev: добавляет хеш в имена файлов
  • karma: юнит-тесты
  • protractor: e2e - тесты

Спасибо за внимание

и

Ваши вопросы?

https://github.com/johnpapa/angular-styleguide

http://largescalejs.ru/

https://github.com/johnpapa/ng-demos

В поисках идеальной архитектуры

By tatyana_babich

В поисках идеальной архитектуры

  • 1,732