Node Сервис-ориентированная архитектура | Кодементор
Независимо от того, являетесь ли вы новичком или экспертом в Node.js, в начале каждого проекта необходимо создать надежный архитектурный ландшафт. Это позволит вам расширить проект, обеспечив при этом удобочитаемость, тестируемость и ремонтопригодность. (Просто назвать несколько нефункциональные требования).
Прочитав эту статью, вы сможете:
- Создайте интуитивно понятную и понятную структуру проекта
- понимать разницу между понятиями; контроллеры, загрузчики, услуги
- Создавайте чистые модульные тесты для вашей бизнес-логики
Оглавление
- Концепции
- Структура папки проекта
- Трехуровневая (сервисно-ориентированная) архитектура
- Сервисный уровень
- Модульное тестирование
- Уровень контроллера
- Погрузчики
- Конфигурации приложений
- Пример репозитория
Концепции
Вот несколько концепций, с которыми вам следует ознакомиться при прочтении этой статьи. Не беспокойтесь, если вы не являетесь экспертом в них, просто поймите их важность и то, как структура позволяет нам использовать эти концепции.
Структура папки проекта
Приведенная ниже структура — это то, что я использую в качестве шаблона почти во всех своих проектах Node.js. Это позволяет нам реализовать Разделение ответственности для нашего приложения.
src
│ index.js # Entry point for application
└───config # Application environment variables and secrets
└───controllers # Express controllers for routes, respond to client requests, call services
└───loaders # Handles all startup processes
└───middlewares # Operations that check or maniuplate request prior to controller utilizing
└───models # Database models
└───routes # Express routes that define API structure
└───services # Encapsulates all business logic
└───test # Tests go here
Трехуровневая архитектура
Построение по принципу Разделение ответственности о котором мы говорили ранее, цель состоит в том, чтобы полностью извлечь и отделить нашу бизнес-логику от нашего API. В частности, мы никогда хотим, чтобы наша бизнес-логика присутствовала в наших маршрутах или контроллерах. На картинке ниже вы точно увидите
как наше приложение будет течь.
- Контроллеры получают входящие запросы клиентов и используют службы
- Сервисы содержат всю бизнес-логику, а также могут выполнять вызовы уровня доступа к данным.
- Уровень доступа к данным взаимодействует с базой данных, выполняя запросы
- Результаты передаются обратно на сервисный уровень.
- Затем сервисный уровень может передать все контроллеру.
- Затем контроллер может ответить клиенту!
Вопрос: Почему я не могу просто разместить свою бизнес-логику внутри моего контроллера?
Это большой вопрос! Поскольку наши маршруты (в данном случае) создаются с использованием среды Express, в req
а также res
объекты. Если мы хотим проверить нашу бизнес-логику, теперь нам нужно создать макет всех этих объектов. ! Инкапсулируя всю нашу бизнес-логику внутри сервисов, мы можем протестировать ее без необходимости макетировать Express.
req
или же res
объекты ️!
Сервисный уровень
Сервисный уровень инкапсулирует и абстрагирует всю нашу бизнес-логику от остальной части приложения.
- Сервисный уровень ДОЛЖЕН:
- Содержать бизнес-логику
- Используйте уровень доступа к данным для взаимодействия с базой данных
- Будьте независимыми от фреймворка
- Сервисный уровень НЕ ДОЛЖЕН:
- Быть обеспеченным
req
или жеres
объекты - Управлять ответами клиентам
- Предоставьте все, что связано с транспортным уровнем HTTP; коды состояния, заголовки и т. д.
- Прямое взаимодействие с базой данных
- Быть обеспеченным
Пример
Здесь наш контроллер импортирует сервис, который мы будем использовать для создания постов.
Обратите внимание, что в этом файле нет никакой бизнес-логики!
const PostService = require( "../services/PostService" );
const PostServiceInstance = new PostService();
module.exports = { createCord };
async function createCord ( req, res ) {
try {
const createdCord = await PostServiceInstance.create( req.body );
return res.send( createdCord );
} catch ( err ) {
res.status( 500 ).send( err );
}
}
Наш сервис реализует всю нашу логику и может использовать уровень доступа к данным для
взаимодействуйте с базой данных! Как только наша логика достигает результата, мы возвращаем данные (или ошибку, если она произошла) контроллеру.
const MongooseService = require( "./MongooseService" );
const PostModel = require( "../models/post" );
class PostService {
constructor () {
this.MongooseServiceInstance = new MongooseService( PostModel );
}
async create ( postToCreate ) {
try {
const result = await this.MongooseServiceInstance.create( postToCreate );
return { success: true, body: result };
} catch ( err ) {
return { success: false, error: err };
}
}
}
module.exports = PostService;
Модульное тестирование
Создание тщательных тестов для вашего кода необходимо для гарантии того, что ваш код ремонтопригоден и надежен. Разработка через тестирование (TDD)то вам следует
создавать модульные тесты до вы начинаете писать любой код. Это позволяет нам гарантировать, что мы напишем минимальный объем кода, необходимый для удовлетворения имеющихся требований, и после завершения разработки наши тесты уже готовы!
Существует множество модулей и способов тестирования вашего кода, но в этом примере мы будем использовать комбинацию мокко, чайа также Нью-Йорк.
Это даст нам большую гибкость для создания модульных тестов, а также даст нам знать, какой объем кода покрыт нашими тестами!
1) Для начала добавьте эти 3 модуля в наш проект
npm i -s nyc mocha chai
2) Теперь создайте новый каталог под test
каталог для нашего Post
тесты
src
│ index.js # Entry point for application
... // Other directories
└───test # Tests go here
└─── Post # All tests for 'Posts' go here
| index.js
3) Открыть test/Post/index.js
и вставьте следующий код
const assert = require( "chai" ).assert;
const mocha = require( "mocha" );
const PostService = require( "../../services/PostService" );
mocha.describe( "Post Service", () => {
const PostServiceInstance = new PostService();
mocha.describe( "Create instance of service", () => {
it( "Is not null", () => {
assert.isNotNull( PostServiceInstance );
} );
it( "Exposes the createPost method", () => {
assert.isFunction( PostServiceInstance.create );
} );
} );
} );
4) Запустите тесты с помощью следующей команды
mocha test/* --reporter spec
Вы должны получить вывод, подобный этому:
Post Service
Create instance of service
√ Is not null
√ Exposes the createPost method
2 passing (29ms)
Теперь я знаю, что мы сделали это не в истинном стиле TDD; мы написали код до написание теста. Но поскольку эта статья в большей степени посвящена трехуровневой архитектуре, я счел важным сначала представить
концепция сервисов до модульных тестов. Просто знайте, что если вы будете применять это на практике самостоятельно, вам будет очень полезно написать тесты до кода. .
Теперь, когда вы написали свои первые тесты, пришло время изучить, как мы используем наши сервисы в контроллере!
Уровень контроллера
Уровень контроллера отвечает за обработку клиентских запросов и реагирование на них. Просто повторю очень важный момент, этот слой никогда не должен содержать бизнес-логику! Мы используем услуги только путем передачи необходимых им данных, а не req
или же res
сами объекты. Это позволяет нашим сервисам оставаться независимыми от фреймворка!
Выше я показал пример уровня контроллера, который вы также можете найти здесь. (не нужно заново изобретать велосипед).
async function createCord ( req, res ) {
try {
const createdCord = await PostServiceInstance.create( req.body );
return res.send( createdCord );
} catch ( err ) {
res.status( 500 ).send( err );
}
}
Погрузчики
Загрузчики абстрагируют все процессы запуска наших приложений в определенные модули. Это позволяет нам инкапсулировать и поддерживать разделение задач. Если вы сбрасываете все в точку входа приложения, она очень быстро загромождается.
Чтобы действительно понять эту мысль, сравните этот пример с приведенным ниже. Спросите себя, какой из них будет легче поддерживать, какой будет легче масштабировать и расширять, какой будет легче удалить позже, если он больше не нужен.
Что нельзя делать (ПЛОХО)
const bodyParser = require( 'body-parser' );
const config = require( './config' );
const express = require( 'express' );
const morgan = require( 'morgan' );
const path = require( 'path' );
const routes = require( './routes' );
const rfs = require( 'rotating-file-stream' );
const compression = require( 'compression' );
let fs = require( 'fs' ),
logDir = path.join( __dirname, config.logDir );
fs.access( logDir, ( err ) => {
if ( err ) {
fs.mkdirSync( logDir );
}
} );
let app = express(),
accessLogStream = rfs( 'access.log', {
interval : '1d',
path : logDir
} );
app.set( 'view engine', 'html' );
app.set( 'views', path.join( __dirname, 'public' ) );
app.use( express.static( path.join( __dirname, 'public' ) ) );
app.use( express.static( path.join( __dirname, 'node_modules' ) ) );
app.use( morgan( 'dev', { stream : accessLogStream } ) );
app.use( compression() );
app.use( bodyParser.urlencoded( {
extended : false,
limit : '20mb'
} ) );
app.use( bodyParser.json( { limit : '20mb' } ) );
routes( app );
app.listen( config.port, () => {
console.log( 'Now listening on', config.port );
} );
Что делать (хорошо)
const config = require( "./config" );
const mongoose = require( "mongoose" );
const logger = require( "./services/Logger" );
const mongooseOptions = {
useCreateIndex: true,
useNewUrlParser: true,
autoReconnect: true
};
mongoose.Promise = global.Promise;
mongoose.connect( config.dbUrl, mongooseOptions )
.then( () => {
logger.info( "Database connection successful" );
const ExpressLoader = require( "./loaders/Express" );
new ExpressLoader();
} )
.catch( err => {
console.error( err );
logger.error( err );
} );
Очень легко определить, какой файл более удобен в сопровождении, удобочитаем, масштабируем и т. д. Теперь покажу вам, как выглядит загрузчик. В приведенном выше примере вы заметите, что у нас есть один загрузчик, ExpressLoader
. Вот как устроен загрузчик
const bodyParser = require( "body-parser" );
const express = require( "express" );
const morgan = require( "morgan" );
const path = require( "path" );
const routes = require( "../routes" );
const compression = require( "compression" );
const logger = require( "../services/Logger" );
const config = require( "../config" );
class ExpressLoader {
constructor () {
const app = express();
app.use( ExpressLoader.errorHandler );
app.use( express.static( path.join( __dirname, "uploads" ) ) );
app.use( morgan( "dev" ) );
app.use( compression() );
app.use( bodyParser.urlencoded( {
extended: false,
limit: "20mb"
} ) );
app.use( bodyParser.json( { limit: "20mb" } ) );
routes( app );
this.server = app.listen( config.port, () => {
logger.info( `Express running, now listening on port ${config.port}` );
} );
}
get Server () {
return this.server;
}
static errorHandler ( error, req, res, next ) {
let parsedError;
try {
if ( error && typeof error === "object" ) {
parsedError = JSON.stringify( error );
} else {
parsedError = error;
}
} catch ( e ) {
logger.error( e );
}
logger.error( parsedError );
if ( res.headersSent ) {
return next( error );
}
res.status( 400 ).json( {
success: false,
error
} );
}
}
module.exports = ExpressLoader;
Что мы сделали, так это абстрагировали нашу логику запуска для экспресса в один файл. Это позволяет нам легко удалить/заменить фреймворк позже, если мы захотим. Это также значительно упрощает отладку или отслеживание проблем, вместо того, чтобы проверять их в нескольких местах или в большом файле.
Конфигурации приложений
Вы должны проявлять большую осторожность, чтобы никогда не раскрывать секреты и конфигурации вашего приложения. Последнее, что вам нужно, это чтобы злонамеренная сущность широко распахнула все задние двери, чтобы она могла делать все, что захочет! Есть
несколько фантастических модулей; дотенв который может дать вам удивительную функциональность для защиты секретов вашего приложения.
Но для простоты мы создадим index.js
файл в нашем config
каталог. В нем будут храниться все параметры конфигурации нашего приложения, которые мы можем использовать в других наших файлах.
const config = {
dbUrl: process.env.DBURL || "mongodb://localhost/test-db",
port: process.env.PORT || 3000,
env: process.env.NODE_ENV || "development",
logDir: process.env.LOGDIR || "logs",
viewEngine: process.env.VIEW_ENGINE || "html"
};
module.exports = config;
Это так просто! затем вы можете просто импортировать файл туда, куда вам нужно, и ссылаться на переменные.
Пример репозитория
Вы можете найти пример репозитория, который демонстрирует, как эта структура может быть реализована на мой GitHub
Ссылки и ресурсы
Я не могу брать на себя ответственность за все здесь, и определенно получил некоторую помощь! Пожалуйста, ознакомьтесь с другими источниками, которые меня вдохновили.