Vue.js и слоистая архитектура: вынесение бизнес-логики в сервисы

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Когда нужно сделать код в проекте гибким и удобным, на помощь приходит разделение архитектуры на несколько слоев. Рассмотрим подробнее этот подход и альтернативы, а также поделимся рекомендациями, которые могут быть полезны как начинающим, так и опытным разработчикам Vue.js, React.js, Angular. 

В старые времена, когда JQuery только появился, а о фреймворках для серверных языков лишь читали в редких новостях, веб-приложения реализовывали целиком на серверных языках. Зачастую для этого использовали модель MVC (Model-View-Controller): контроллер (controller) принимал запросы, отвечал за бизнес-логику и модели (model) и передавал данные в представление (view), которое рисовало HTML. 

Объектно-ориентированное программирование (ООП) на тот момент только начинало формироваться, поэтому разработчики зачастую интуитивно решали, где и какой код надо писать. Таким образом, в мире разработки зародилось такое понятие, как «Божественные объекты», которые первоначально отвечали практически за всю работу отдельных частей системы. Например, если в системе была сущность «Пользователь», то создавался класс User и в нем писалась вся логика, так или иначе связанная с пользователями. Без разбиения на какие-то ещё файлы. И если приложение было большим, то такой класс мог содержать тысячи строк кода.

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

1. Выход есть

Как известно, Vue.js, React.js и прочие подобные фреймворки основаны на компонентах. То есть, по большому счету, приложение состоит из множества компонентов, которые могут заключать в себе и бизнес-логику и представление и много чего еще. Таким образом, разработчики во многих проектах пишут всю логику в компонентах и эти компоненты, как правило, начинают напоминать те самые божественные классы из прошлого. То есть, если компонент описывает какую-то крупную часть функционала с большим количеством (возможно сложной) логики, то вся эта логика и остается в компоненте. Появляются десятки методов и тысячи строк кода. А если учесть то, что, например, во Vue.js еще есть такие понятия как computed, watch, mounted, created, то логику пишут еще и во все эти части компонента. В итоге, чтобы найти какую-то часть кода, отвечающую за клик по кнопке, надо перелистать десяток экранов js-кода, бегая между methods, computed и прочими частями компонента.

Примерно в 2008 году, применительно к backend, была предложена “слоистая” архитектура. Основная идея этой архитектуры заключается в том, что весь код приложения следует разбивать на определенные слои, которые выполняют определенную работу и не очень знают о других слоях.

С подобным разбиением приложение становится намного проще поддерживать, писать тесты, искать ответственные зоны и вообще читать код.

Вот о таком разбиении кода на слои и пойдет речь, но уже применительно к frontend-фреймворкам, таким как Vue.js, React.js и прочим. 

Изначальная теория “слоистой” архитектуры, применительно к backend, имеет много ограничений и правил. Идея же этой статьи в том, чтобы перенять именно разбиение кодовой базы на слои. Схематично ее можно изобразить примерно так.

2. Создание удобной архитектуры приложения

Рассмотрим пример, в котором вся логика находится в одном компоненте.

2.1. Логика в компоненте

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

methods: {
    duplicateCollage (collage) {
      this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: true })
      dataService.duplicateCollage(collage, false)
        .then(duplicate => {
          this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: false })
        })
        .catch(() => {
          this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: false })
          this.$store.dispatch('errorsSet', { api: `We couldn't duplicate collage. Please, try again later.` })
        })
    },
    deleteCollage (collage, index) {
      this.$store.dispatch('updateCollage', { id: collage.id, isDeleting: true })
      photosApi.deleteUserCollage(collage)
        .then(() => {
          this.$store.dispatch('updateCollage', {
            id: collage.id,
            isDeleting: false,
            isDeleted: true
          })
          this.$store.dispatch('setUserCollages', { total: this.userCollages.total - 1 })
          this.$store.dispatch('updateCollage', {
            id: collage.id,
            deletingTimer: setTimeout(() => {
              this.$store.dispatch('updateCollage', { id: collage.id, deletingTimer: null })
              this.$store.dispatch('setUserCollages', { items: this.userCollages.items.filter(userCollage => userCollage.id !== collage.id) })
 
              // If there is no one collages left - show templates
              if (!this.$store.state.editor.userCollages.total) {
                this.currentTabName = this.TAB_TEMPLATES
              }
            }, 3000)
          })
        })
    },
    restoreCollage (collage) {
      clearTimeout(collage.deletingTimer)
      photosApi.saveUserCollage({ collage: { deleted: false } }, collage.id)
        .then(() => {
          this.$store.dispatch('updateCollage', {
            id: collage.id,
            deletingTimer: null,
            isDeleted: false
          })
          this.$store.dispatch('setUserCollages', { total: this.userCollages.total + 1 })
        })
    }
}

2.2. Создание слоя сервисов для бизнес-логики

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

Один из классических способов хоть какого-то разбиения логики – это деление на сущности. Например, почти всегда в проекте есть сущность Пользователь или, как в описываемом примере, Коллаж. Таким образом, можно создать папку services и в ней – файлы user.js и collage.js. Такие файлы могут быть статическими классами или просто возвращать функции. Главное – чтобы вся бизнес-логика, связанная с сущностью, была в этом файле.

services
  |_collage.js
  |_user.js

В сервис collage.js следует поместить логику дублирования, восстановления и удаления коллажей.

export default class Collage {
  static delete (collage) {
    // ЛОГИКА УДАЛЕНИЯ КОЛЛАЖА
  }
 
  static restore (collage) {
    // ЛОГИКА ВОССТАНОВЛЕНИЯ  КОЛЛАЖА
  }
 
  static duplicate (collage, changeUrl = true) {
    // ЛОГИКА ДУБЛИРОВАНИЯ КОЛЛАЖА
  }
}

2.3. Использование сервисов в компоненте

Тогда в компоненте надо будет лишь вызвать соответствующие функции сервиса.

methods: {
  duplicateCollage (collage) {
    CollageService.duplicate(collage, false)
  },
  deleteCollage (collage) {
    CollageService.delete(collage)
  },
  restoreCollage (collage) {
    CollageService.restore(collage)
  }
}

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

Также многие разработчики на пути к удобной архитектуре выносят вызовы методов API в отдельный файл (файлы). Это как раз создание слоя вызовов API, которое также приводит к удобству и структурированности кода.

import axios from '@/plugins/axios'
 
export default class Api {
 
  static login (email, password) {
    return axios.post('auth/login', { email, password })
      .then(response => response.data)
  }
 
  static logout () {
    return axios.post('auth/logout')
  }
 
  static getCollages () {
    return axios.get('/collages')
      .then(response => response.data)
  }
  
  static deleteCollage (collage) {
    return axios.delete(`/collage/${collage.id}`)
      .then(response => response.data)
  }
  
  static createCollage (collage) {
    return axios.post(`/collage/${collage.id}`)
      .then(response => response.data)
  }
}

3. Что и куда выносить?

На вопрос, что же именно и куда выносить, однозначно ответить невозможно. Как вариант, можно разбить код на три условные части: бизнес-логика, логика и представление.

Бизнес-логика – это все то, что описано в требованиях к приложению. Например, ТЗ, документации, дизайны. То есть все то, что напрямую относится к предметной области приложения. Примером может быть метод UserService.login() или ListService.sort(). Для бизнес-логики можно создать сервисный слой с сервисами.

Логика – это тот код, который не имеет прямого отношения к предметной области приложения и его бизнес-логике. Например, создание уникальной строки или поиск некоего объекта в массиве. Для логики можно создать слой хэлперов: например, папку helpers и в ней файлы string.js, converter.js и прочие.

Представление – все то, что непосредственно связано с компонентом и его шаблоном. Например, изменение реактивных свойств, изменение состояний и прочее. Этот код пишется непосредственно в компонентах (methods, computed, watch и так далее).

login (email, password) {
  this.isLoading = true
  userService.login(email, password)
    .then(user => {
      this.user = user
      this.isLoading = false
    })
}

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

Если же сервисы или хэлперы начнут разрастаться, то сущности всегда можно разделить на другие сущности. К примеру, если у пользователя в приложении маленький функционал в 3-5 методов и пара методов про заказы пользователя, то разработчик может вынести всю эту бизнес-логику в сервис user.jsрешить всю эту бизнес-логику написать в сервисе user.js. Если же у сервиса пользователя сотни строк кода, то можно все, что относится к заказам, вынести в сервис order.js.

4. От простого к сложному

В идеале можно сделать архитектуру на ООП, в которой будут, помимо сервисов, еще и модели. Это классы, описывающие сущности приложения. Те же User или Collage. Но использоваться они будут вместо обычных объектов данных.

Рассмотрим список пользователей.

Классический способ вывода ФИО пользователей выглядит так.

<template>
<div class="users">
  <div
    v-for="user in users"
    class="user"
  >
    {{ getUserFio(user) }}
  </div>
</div>
</template>
 
<script>
import axios from '@/plugins/axios'
 
export default {
  data () {
    return {
      users: []
    }
  },
  mounted () {
    this.getList()
  },
  methods: {
    getList() {
      axios.get('/users')
        .then(response => this.users = response.data)
    },
    getUserFio (user) {
      return `${user.last_name} ${user.first_name} ${user.third_name}`
    }
  }
}
</script>

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

Для начала следует создать модель Пользователь.

export default class User {
  constructor (data = {}) {
    this.firstName = data.first_name
    this.secondName = data.second_name
    this.thirdName = data.third_name
  }
 
  getFio () {
    return `${this.firstName} ${this.secondName} ${this.thirdName}`
  }
}

Далее следует импортировать эту модель в компонент.

import UserModel from '@/models/user'

С помощью сервиса получить список пользователей и преобразовать каждый объект в массиве в объект класса (модели) User.

methods: {
   getList() {
     const users = userService.getList()
     users.forEach(user => {
       this.users.push(new UserModel(user))
     })
   },

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

<template>
<div class="users">
  <div
    v-for="user in users"
    class="user"
  >
    {{ user.getFio() }}
  </div>
</div>
</template>

К вопросу о том, какую логику выносить в модели, а какую в сервисы. Можно всю логику поместить в сервисы, а в моделях вызывать сервисы. А можно в моделях хранить логику, относящуюся непосредственно к сущности модели (тот же getFio()), а логику работы с массивами сущностей хранить в сервисах (тот же getList()). Как будет удобнее.

5. Заключение

Если в проекте большое количество логики хранится в компонентах, есть риск сделать их трудночитаемыми и осложнить дальнейшее переиспользование логики. В таких случаях можно ввести “слои” для вынесения этой логики: например, слой сервисов для бизнес-логики, слой хэлперов для остальной логики. Внутри компонента стоит оставить ту логику, которая относится непосредственно к нему и его шаблону.

Также для удобства можно создать слои для операций с сессиями, перехватчиками (interceptors) api, глобальными обработчиками ошибок – смотря что вам будет удобно. Таким образом, вы сделаете компоненты маленькими и простыми, а логика будет храниться там, где ее легко будет найти и переиспользовать в любом месте проекта.

Спасибо за внимание! Будем рады ответить на ваши вопросы. 

Источник: https://habr.com/ru/company/simbirsoft/blog/556270/


Интересные статьи

Интересные статьи

Сегодня я продолжу рассуждать о том, как мы пишем микросервисы. В прошлый раз упор был на теорию: нужно было вспомнить, как код писался раньше, понять сущность архитектур...
Выгрузка пользователей из 1C ЗУП в Битрикс24 или правдивая история о том как настроить интеграцию 1С-Битрикс24 с ЗУП без 1С-ника В жизни так бывает, причём бывает чаще чем хотелось б...
Знаете ли вы, что многим компаниям для публикации банальных новостей на своём сайте надо подключаться к специальной защищённой сети, и только через неё постить котят в соцсетях&...
С 16 марта наша команда ITSM 365 начала тестировать полноценную удаленную работу. Наши офисы в Екатеринбурге, Москве, Твери, Питере постепенно пустеют. Но мы, «переехав» домой, продолжаем оттачив...
У некоторых бизнес-тренеров в области е-коммерса и консультантов по увеличению интернет-продаж на многие вопросы часто можно слышать универсальную отмазку — «надо тестировать» или другую (чтобы не...