Когда нужно сделать код в проекте гибким и удобным, на помощь приходит разделение архитектуры на несколько слоев. Рассмотрим подробнее этот подход и альтернативы, а также поделимся рекомендациями, которые могут быть полезны как начинающим, так и опытным разработчикам 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, глобальными обработчиками ошибок – смотря что вам будет удобно. Таким образом, вы сделаете компоненты маленькими и простыми, а логика будет храниться там, где ее легко будет найти и переиспользовать в любом месте проекта.
Спасибо за внимание! Будем рады ответить на ваши вопросы.