Cross-cutting concerns
В статье «Принцип разделения ответственностей» был рассмотрен один из базовых принципов разработки ПО. После его применения в приложении выделилось несколько ответственностей. Ниже будут рассмотрены виды ответственностей в более общем виде: с точки зрения требований к ПО.
Функциональность программы происходит от предъявляемых к ней требований (requirements). Этот термин важен для разделения видов ответственностей в ПО.
«Требование – спецификация того, что должно быть реализовано» – (https://datafinder.ru/products/razrabotka-trebovaniy-k-po-obshchie-ponyatiya)
Требования разделяют на две группы:
Функциональные (ФТ)
Они включают в себя бизнес-требования, пользовательские требования, т.е. то для чего программа разрабатывается. Какую ценность несет бизнесу и пользователям. О твечает на вопросы: что делает продукт и зачем он создается.
Нефункциональные (НФТ)
Содержат описания того как продукт должен работать. Налагаемые на него ограничения и его качества.
Например, от клиента поступило новое требование: добавить на страницу компонент с калькулятором. ФТ будет – работающий калькулятор. Калькулятор должен быстро отображать результат: не дольше 500 мс. Это требование накладывает ограничение и является НФТ. Оно тоже важно, т.к. медленно работающий калькулятор будет вызывать плохой опыт использования продукта (User Experience), что может привести к уходу клиента и в конченом итоге скажется на бизнес.
Рассмотрев, что такое требования и какими они бывают можно перейти к видам ответственностей.
Виды ответственностей
Выделяют два вида:
Core concerns (ответственности 1-го класса) – бизнес-логика приложения, следует из функциональных требований.
Данная ответственность является ядром приложения. Она несет ценность бизнесу и клиентам.
Cross-cutting concerns (CCC, ответственности 2-го класса, сквозная функ циональность) – остальная функциональность, не относящаяся к бизнес-логике.
Следует в основном из нефункциональных требований, а также из требований расширяемости, сопровождаемости и т.д. Примерами могут быть:
- логирование;
- обработка ошибок;
- кэширование;
- авторизация и проверка прав;
- обработка транзакций и т.д.
Это «вспомогательные» ответственности. Они не относятся к бизнес-логике напрямую. Под сквозной функциональностью понимается то, что данный код может встречаться во всех модулях разрабатываемого ПО. Он обладает свойством тесно «переплетаться» с основной функциональностью. Его сложно вынести в один определенный модуль.
Рассмотрим пример из статьи «Принцип разделения ответственностей», добавив в него ССС: логирование и обработку ошибок. Затем попробуем разделить ответственности 1-го и 2-го класса.
<template id="list">
<ul class="list"></ul>
</template>
<template id="item">
<li class="item"></li>
</template>
<script>
// ДОСТУП К ДАННЫМ
async function fetchHotels() {
// Обрабатываем ошибки получения данных по сети
try {
const hotelsReq = await fetch('/hotels')
return await hotelsReq.json()
} catch (error) {
// Логируем ошибку
console.error(error)
}
}
// БИЗНЕС-ЛОГИКА
async function main() {
const hotels = await fetchHotels()
// Логируем все данные
console.log(`All hotels: ${JSON.stringify(hotels)}`)
if (!hotels) {
return
}
const fiveStarHotels = getFiveStarHotels(hotels)
// Логируем данные после преобразований
console.log(`All hotels: ${JSON.stringify(hotels)}`)
showHotels(fiveStarHotels)
}
function getFiveStarHotels(hotels) {
return hotels.filter(({ stars }) => stars === 5)
}
// ПРЕДСТАВЛЕНИЕ
function showHotels(fiveStarHotels) {
const elems = showList(fiveStarHotels)
// Логируем получившееся представление
console.log(`Elems: ${elems}`)
document.body.append(elems)
}
function showList(items) {
const list = document.body.querySelector('#list')
const listClone = list.content.cloneNode(true)
const item = document.body.querySelector('#item')
const elems = items.reduce((acc, { title }) => {
const itemClone = item.content.cloneNode(true)
itemClone.querySelector('.item').textContent = title
acc.append(itemClone)
return acc
}, listClone.querySelector('.list'))
return elems
}
main()
</script>
Можно заметить, что код бизнес-логики стал «обрастать» дополнительной функциональностью. Cross-cutting concerns стали «переплетаться» с core concerns.
В таком небольшом примере эти изменения не выглядят пугающе, но с развитием приложения и появлением новых требований это становится проблемой.
Допустим, пришло требование оптимизировать время на выполнения сетевых запросов, из-за чего было добавлено кэширование или бизнес-аналитик попросил добавить Google-метрику для анализа действия пользователей. Новые cross-cutting concerns все больше «переплетаться» с основным кодом, увеличивают его объем. Код бизнес-логики начинает «теряться» за «вспомогательным» кодом. Это усложняет поддержку проекта и увеличивает время на чтение кода. О важности простоты чтения кода говорилось в статье «Правило 7±2».
Попробуем решить эту проблему, сделав более явную границу между cross-cutting concerns и core concerns.
Вынесем CCC в отдельный файл.
utils.js
export function catchErrors(target) {
return async (...args) => {
try {
return await target(...args)
} catch (error) {
return error
}
}
}
export function log(target) {
return (...args) => {
console.log(`Call ${target.name} with`, ...args)
const result = target(...args)
console.log(`Result of ${target.name}`, result)
return result
}
}
Функция catchErrors
содержит ответственность – обработку ошибок, log
– логирование. Если в каком-либо коде потребуется
эти CCC, необходимо вызывать соответствующие функции.
Один из способов добиться разделения ответственностей – применить структурный шаблон проектирования «Декоратор».
В функциональной парадигме программирования достаточно передать декорируемую функцию (параметр target
) в декоратор и
вернуть новую функцию, которая выполняет определенную функциональность и вызывает декорируемую ф-ю (target
), передав
в неё аргументы.
Таким образом, данные ответственности будут содержаться в одном месте и не будут дублироваться в разных участках кода.
Добавим получившиеся декораторы в core concerns.
Кодов ая база веб-приложений может содержать несколько тысяч строк кода. Перепишем исходный пример с применением модулей и сгруппируем их по типам ответственностей в несколько директорий.
business – бизнес-логика
business/getHotels.js
import { fetchHotelsDecorated as fetchHotels} from '../data/hotels.js'
import { getFiveStarHotelsDecorated as getFiveStarHotels } from './hotels.js'
import { showHotels } from '../presentation/hotels.js'
export async function getHotels() {
const hotels = await fetchHotels()
if (!hotels) {
return
}
const fiveStarHotels = getFiveStarHotels(hotels)
showHotels(fiveStarHotels)
}
Ф-я getHotels
теперь содержит только core concerns. В ней нет вспомогательного кода.
business/hotels.js
import { log } from '../utils.js'
export function getFiveStarHotels(hotels) {
return hotels.filter(({ stars }) => stars === 5)
}
export const getFiveStarHotelsDecorated = log(getFiveStarHotels)
В getFiveStarHotels
содержит только core concerns. Добавлена ф-я getFiveStarHotelsDecorated
, которая декорирует
основную функциональность.
data – доступ к данным
data/hotels.js
import { log, catchErrors } from '../utils.js'
export async function fetchHotels() {
const hotelsReq = await fetch('/hotels')
return await hotelsReq.json()
}
export const fetchHotelsDecorated = log(catchErrors(fetchHotels))
Добавлена ф-я fetchHotelsDecorated
, которая декорирует основную функциональность. Было применено два
декоратора. Аналогичным образом, возможно добавлять новые CCC, не меняя основной код.
presentation – представление
presentation/hotels.js
import { showListDecorated as showList } from './list.js'
export function showHotels(fiveStarHotels) {
const elems = showList(fiveStarHotels)
document.body.append(elems)
}
presentation/list.js
import { log } from '../utils.js'
export function showList(items) {
const list = document.body.querySelector('#list')
const listClone = list.content.cloneNode(true)
const item = document.body.querySelector('#item')
const elems = items.reduce((acc, { title }) => {
const itemClone = item.content.cloneNode(true)
itemClone.querySelector('.item').textContent = title
acc.append(itemClone)
return acc
}, listClone.querySelector('.list'))
return elems
}
export const showListDecorated = log(showList)
Используется декоратор логирования.
В данном примере, для разделения бизнес-логики и вспомогательного кода применен паттерн «Декоратор». Предполагаю, это не единственный вариант. Например, можно добавить в приложение поведенческий шаблон проектирования – «Наблюдатель», который позволит создавать различные события в core concerns. Вспомогательный код будет «слушать» их и выполнять некоторые cross-cutting concerns.
Разделение кода на ответственности 1-го и 2-го класса помогает в поддержке проекта. Таким образом, мы делаем явную границу между ними. Бизнес-логика на «разрастается» вспомогательным кодом, который увеличивает сложность кода и время на его чтение.
Вспомогательный код, который требуется во многих модулях, «выносится» в определенный модуль, отвечающий за эту ответственность и не дублируется во всех модулях приложения. Это может помочь в случае, когда потребуется заменить реализацию некоторой cross-cutting concerns: нужно заменить сам модуль, не меняя весь код проекта, где он используется.