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

Kate

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

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

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

ec9a1718daa78ed829756c2fece0ea2a.gif

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

1. Выход есть​

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

118b35c0ed8a3e33a06eb2340786fcfe.gif

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

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

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

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

d69d7c0347e2b02706525f974ef83033.png

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>
76b3d52302ba04c13f621d61fd47bbdc.png

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

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

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

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


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

yinsushi

Active member
http://audiobookkeeper.ruhttp://cottagenet.ruhttp://eyesvision.ruhttp://eyesvisions.comhttp://factoringfee.ruhttp://filmzones.ruhttp://gadwall.ruhttp://gaffertape.ruhttp://gageboard.ruhttp://gagrule.ruhttp://gallduct.ruhttp://galvanometric.ruhttp://gangforeman.ruhttp://gangwayplatform.ruhttp://garbagechute.ruhttp://gardeningleave.ruhttp://gascautery.ruhttp://gashbucket.ruhttp://gasreturn.ruhttp://gatedsweep.ruhttp://gaugemodel.ruhttp://gaussianfilter.ruhttp://gearpitchdiameter.ruhttp://geartreating.ruhttp://generalizedanalysis.ru http://generalprovisions.ruhttp://geophysicalprobe.ruhttp://geriatricnurse.ruhttp://getintoaflap.ruhttp://getthebounce.ruhttp://habeascorpus.ruhttp://habituate.ruhttp://hackedbolt.ruhttp://hackworker.ruhttp://hadronicannihilation.ruhttp://haemagglutinin.ruhttp://hailsquall.ruhttp://hairysphere.ruhttp://halforderfringe.ruhttp://halfsiblings.ruhttp://hallofresidence.ruhttp://haltstate.ruhttp://handcoding.ruhttp://handportedhead.ruhttp://handradar.ruhttp://handsfreetelephone.ruhttp://hangonpart.ruhttp://haphazardwinding.ruhttp://hardalloyteeth.ruhttp://hardasiron.ru http://hardenedconcrete.ruhttp://harmonicinteraction.ruhttp://hartlaubgoose.ruhttp://hatchholddown.ruhttp://haveafinetime.ruhttp://hazardousatmosphere.ruhttp://headregulator.ruhttp://heartofgold.ruhttp://heatageingresistance.ruhttp://heatinggas.ruhttp://heavydutymetalcutting.ruhttp://jacketedwall.ruhttp://japanesecedar.ruhttp://jibtypecrane.ruhttp://jobabandonment.ruhttp://jobstress.ruhttp://jogformation.ruhttp://jointcapsule.ruhttp://jointsealingmaterial.ruhttp://journallubricator.ruhttp://juicecatcher.ruhttp://junctionofchannels.ruhttp://justiciablehomicide.ruhttp://juxtapositiontwin.ruhttp://kaposidisease.ru http://keepagoodoffing.ruhttp://keepsmthinhand.ruhttp://kentishglory.ruhttp://kerbweight.ruhttp://kerrrotation.ruhttp://keymanassurance.ruhttp://keyserum.ruhttp://kickplate.ruhttp://killthefattedcalf.ruhttp://kilowattsecond.ruhttp://kingweakfish.ruhttp://kinozones.ruhttp://kleinbottle.ruhttp://kneejoint.ruhttp://knifesethouse.ruhttp://knockonatom.ruhttp://knowledgestate.ruhttp://kondoferromagnet.ruhttp://labeledgraph.ruhttp://laborracket.ruhttp://labourearnings.ruhttp://labourleasing.ruhttp://laburnumtree.ruhttp://lacingcourse.ruhttp://lacrimalpoint.ru http://lactogenicfactor.ruhttp://lacunarycoefficient.ruhttp://ladletreatediron.ruhttp://laggingload.ruhttp://laissezaller.ruhttp://lambdatransition.ruhttp://laminatedmaterial.ruhttp://lammasshoot.ruhttp://lamphouse.ruhttp://lancecorporal.ruhttp://lancingdie.ruhttp://landingdoor.ruhttp://landmarksensor.ruhttp://landreform.ruhttp://landuseratio.ruhttp://languagelaboratory.ruhttp://largeheart.ruhttp://lasercalibration.ruhttp://laserlens.ruhttp://laserpulse.ruhttp://laterevent.ruhttp://latrinesergeant.ruhttp://layabout.ruhttp://leadcoating.ruhttp://leadingfirm.ru http://learningcurve.ruhttp://leaveword.ruhttp://machinesensible.ruhttp://magneticequator.ruhttp://magnetotelluricfield.ruhttp://mailinghouse.ruhttp://majorconcern.ruhttp://mammasdarling.ruhttp://managerialstaff.ruhttp://manipulatinghand.ruhttp://manualchoke.ruhttp://medinfobooks.ruhttp://mp3lists.ruhttp://nameresolution.ruhttp://naphtheneseries.ruhttp://narrowmouthed.ruhttp://nationalcensus.ruhttp://naturalfunctor.ruhttp://navelseed.ruhttp://neatplaster.ruhttp://necroticcaries.ruhttp://negativefibration.ruhttp://neighbouringrights.ruhttp://objectmodule.ruhttp://observationballoon.ru http://obstructivepatent.ruhttp://oceanmining.ruhttp://octupolephonon.ruhttp://offlinesystem.ruhttp://offsetholder.ruhttp://olibanumresinoid.ruhttp://onesticket.ruhttp://packedspheres.ruhttp://pagingterminal.ruhttp://palatinebones.ruhttp://palmberry.ruhttp://papercoating.ruhttp://paraconvexgroup.ruhttp://parasolmonoplane.ruhttp://parkingbrake.ruhttp://partfamily.ruhttp://partialmajorant.ruhttp://quadrupleworm.ruhttp://qualitybooster.ruhttp://quasimoney.ruhttp://quenchedspark.ruhttp://quodrecuperet.ruhttp://rabbetledge.ruhttp://radialchaser.ruhttp://radiationestimator.ru http://railwaybridge.ruhttp://randomcoloration.ruhttp://rapidgrowth.ruhttp://rattlesnakemaster.ruhttp://reachthroughregion.ruhttp://readingmagnifier.ruhttp://rearchain.ruhttp://recessioncone.ruhttp://recordedassignment.ruhttp://rectifiersubstation.ruhttp://redemptionvalue.ruhttp://reducingflange.ruhttp://referenceantigen.ruhttp://regeneratedprotein.ruhttp://reinvestmentplan.ruhttp://safedrilling.ruhttp://sagprofile.ruhttp://salestypelease.ruhttp://samplinginterval.ruhttp://satellitehydrology.ruhttp://scarcecommodity.ruhttp://scrapermat.ruhttp://screwingunit.ruhttp://seawaterpump.ruhttp://secondaryblock.ru http://secularclergy.ruhttp://seismicefficiency.ruhttp://selectivediffuser.ruhttp://semiasphalticflux.ruhttp://semifinishmachining.ruhttp://spicetrade.ruhttp://spysale.ruhttp://stungun.ruhttp://tacticaldiameter.ruhttp://tailstockcenter.ruhttp://tamecurve.ruhttp://tapecorrection.ruhttp://tappingchuck.ruhttp://taskreasoning.ruhttp://technicalgrade.ruhttp://telangiectaticlipoma.ruhttp://telescopicdamper.ruhttp://temperateclimate.ruhttp://temperedmeasure.ruhttp://tenementbuilding.rutuchkashttp://ultramaficrock.ruhttp://ultraviolettesting.ru
 
Сверху