Node Сервис-ориентированная архитектура | Кодементор

Независимо от того, являетесь ли вы новичком или экспертом в Node.js, в начале каждого проекта необходимо создать надежный архитектурный ландшафт. Это позволит вам расширить проект, обеспечив при этом удобочитаемость, тестируемость и ремонтопригодность. (Просто назвать несколько нефункциональные требования).

Прочитав эту статью, вы сможете:

  1. Создайте интуитивно понятную и понятную структуру проекта
  2. понимать разницу между понятиями; контроллеры, загрузчики, услуги
  3. Создавайте чистые модульные тесты для вашей бизнес-логики

Оглавление

  1. Концепции
  2. Структура папки проекта
  3. Трехуровневая (сервисно-ориентированная) архитектура
  4. Сервисный уровень
  5. Модульное тестирование
  6. Уровень контроллера
  7. Погрузчики
  8. Конфигурации приложений
  9. Пример репозитория

Концепции

Вот несколько концепций, с которыми вам следует ознакомиться при прочтении этой статьи. Не беспокойтесь, если вы не являетесь экспертом в них, просто поймите их важность и то, как структура позволяет нам использовать эти концепции.

Структура папки проекта

Приведенная ниже структура — это то, что я использую в качестве шаблона почти во всех своих проектах 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. В частности, мы никогда хотим, чтобы наша бизнес-логика присутствовала в наших маршрутах или контроллерах. На картинке ниже вы точно увидите
как наше приложение будет течь.

  1. Контроллеры получают входящие запросы клиентов и используют службы
  2. Сервисы содержат всю бизнес-логику, а также могут выполнять вызовы уровня доступа к данным.
  3. Уровень доступа к данным взаимодействует с базой данных, выполняя запросы
  4. Результаты передаются обратно на сервисный уровень.
  5. Затем сервисный уровень может передать все контроллеру.
  6. Затем контроллер может ответить клиенту!

Трехуровневая архитектура

Вопрос: Почему я не могу просто разместить свою бизнес-логику внутри моего контроллера?

Это большой вопрос! Поскольку наши маршруты (в данном случае) создаются с использованием среды 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

Ссылки и ресурсы

Я не могу брать на себя ответственность за все здесь, и определенно получил некоторую помощь! Пожалуйста, ознакомьтесь с другими источниками, которые меня вдохновили.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *