Одной из важнейших частей написания кода является практика, которой мы следуем при структурировании кода. Очевидно, что в JAVA легче следовать принципам чистой архитектуры, так как он чисто объектно-ориентирован, но в случае с JavaScript нет четких инструкций как таковых. Гибкость, которую предлагает JavaScript, сама по себе является одновременно и благом, и проклятием. В этой статье мы рассмотрим способы написания монолитного js-приложения, вдохновленного чистой архитектурой дяди Боба.

Что такое чистая архитектура?

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

Преимущества заключаются в следующем.

  1. Независимость от любого базового фреймворка.
  2. Свобода подключения и работы с любой базой данных.
  3. Улучшить тестируемость кода.
  4. Помогает легко поддерживать большую кодовую базу.

Если вы видите на этой диаграмме, у нас есть концентрические круги, которые представляют слои любого приложения.

Основное правило, благодаря которому эта архитектура работает, называется правилом зависимости. Это правило гласит, что зависимости исходного кода могут указывать только внутрь. Ничто во внутреннем круге не может вообще ничего знать о чем-то во внешнем круге.

Теперь вопрос заключается в способе связи между слоями, поскольку они изолированы друг от друга, и как это влияет на работу приложения. Этого можно добиться с помощью чего-то, известного как внедрение зависимостей. Обратитесь к приведенному ниже примеру для ясного понимания.

interface IEngine {
  start: () => void
  stop: () => void
  ... 
}

class PowerfulEngine implements IEngine {
  start() {}
  stop() {}
  // ...
}

class MorePowerfulEngine implements IEngine {
  start() {}
  stop() {}
  // ...
}

class CarBody {
  constructor(private engine: IEngine) {}
  // ...
}

const car1 = CarBody(new PowerfulEngine())
const car2 = CarBody(new MorePowerfulEngine())

из этого примера видно, насколько легко поменять зависимость с помощью внедрения зависимостей.

Поскольку это обширная и широкая тема для глубокого погружения, я прошу вас прочитать эту статью, чтобы лучше понять слои.

Структурирование нашего приложения Node.JS

В этом сегменте мы обсудим, как приложения узла могут быть структурированы с использованием принципов чистой архитектуры с помощью приложения Todo (ссылка на репозиторий github).

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

Мы будем использовать MongoDB, Node, TypeScript, express и inversify для создания приложения.

Что такое Инверсия?

С помощью inversify вы можете автоматизировать инъекцию зависимостей, вместо того, чтобы каждый раз передавать зависимость вручную, вы можете создать контейнер, а с помощью декораторов TS вы можете легко передать зависимость в свои объекты, вот пример передачи зависимости с инверсией и без нее

с инверсией 👇

interface ITodoService {...}

@injectable()
class TodoService implements ITodoService {
  @inject(Types.Todo_REPOSITORY)
  private todoRepository: ITodoRepository;
  ...
}

interface ITodoRepository {...}
@injectable()
class TodoRepository implements ITodoRepository {
  @inject(Types.ToDo_TABLE)
  private todoTable: TodoAppDataSource<ITodoModel>;
  ...
}


export interface ITodoController {...}

@injectable()
class TodoController implements ITodoController {
  @inject(Types.Todo_SERVICE)
  private todoService: ITodoService;
  ...
}

// using todo controller
const todoController = dIContainer.get<ITodoController>(Types.Todo_CONTROLLER);

без инверсии 👇

interface ITodoService {...}

class TodoService implements ITodoService {
  constructor(private todoRepository: ITodoRepository) {...}
  ...
}

interface ITodoRepository {...}

class TodoRepository implements ITodoRepository {
  constructor(private todoTable: TodoAppDataSource<ITodoModel>) {...}
  ...
}

export interface ITodoController {...}

class TodoController implements ITodoController {
  constructor(private todoService: ITodoService) {...}
  ...
}

// using todo controller
const todoController = new TodoController(new TodoService(new TodoRepository()))

Таким образом, вы можете видеть из обоих примеров, как инверсия упрощает нашу жизнь без инверсии каждый раз, когда вы хотите создать экземпляр TodoController, вам нужно вручную передать две другие зависимости, которые могут быть утомительной задачей по мере того, как ваше приложение становится больше, поэтому лучше используйте inversify или какой-либо другой инструмент автоматизации DI.

Слои приложений

Наше приложение разделено на 5 слоев, вот подробное объяснение каждого слоя.

Уровень 1 (маршруты)

Маршруты — это самый внутренний слой, потому что очень редко вы меняете какое-либо имя маршрута, поэтому оно будет ядром приложения.

export default function TodoRoutes() {
  const router = express.Router();
  const todoController = dIContainer.get<ITodoController>(
    Types.Todo_CONTROLLER
  );

  router
    .route("/")
    .post(todoController.createNewTodo)
    .get(todoController.getAllTodo)
    .patch(todoController.updateTodo)
    .delete(todoController.deleteTodo);

  return router;
}

Здесь, если вы видите, единственная обязанность этого уровня — регистрировать маршруты и перенаправлять входящие запросы на соответствующий контроллер.

Уровень 2 (контроллер)

Контролер отвечает за

  1. Обработка входящего запроса.
  2. Проверка тела запроса.
  3. Отправка ответа клиенту.
@injectable()
export class TodoController implements ITodoController {
  @inject(Types.Todo_SERVICE)
  private todoService: ITodoService;

  public createNewTodo = async (req: AuthenticatedRequest, res: Response) => {
    ...
  };

  public getAllTodo = async (
    req: AuthenticatedRequest,
    res: Response
  ): Promise<Response> => {
    ...
  };

  public updateTodo = async (req: AuthenticatedRequest, res: Response) => {
   ...
  };
  public deleteTodo = async (req: AuthenticatedRequest, res: Response) => {
   ...
  };
}

Здесь, если вы видите, что наш TodoController зависит от нашего TodoService и реализует интерфейс ITodoService, это означает, что все классы, реализующие интерфейс ITodoService, могут быть внедрены в TodoController, и это можно использовать для подкачки бизнес-логики, ему просто нужно реализовать интерфейс ITodoService и вот и все, нашему TodoController не нужно ничего знать о TodoService, он просто адаптируется к типу переданного в него объекта.

Уровень 3 (Сервис)

Сервис — это наш третий уровень, который отвечает за обработку нашей бизнес-логики, он зависит от TodoRepository, и любой класс, удовлетворяющий интерфейсу ITodoRepository, может быть внедрен в TodoService.

Уровень 4 (Репозиторий)

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

@injectable()
class TodoRepository implements ITodoRepository {
  @inject(Types.ToDo_TABLE)
  private todoTable: TodoAppDataSource<ITodoModel>;

  private getParameterObj = (
    content: string,
    userId: string
  ): Omit<ITodoModel, "_id"> => ({
    ...
  });

  createNewEntry = async (
    content: string,
    userId: string
  ): Promise<ITodoModel> => { ... };

  getAllUserTodo = async (userId: string) => { ... };

  deleteTodo = async (
    userId: string, 
    todoId: string
  ): Promise<ITodoModel> =>{ ... };

  updateTodoDetails = async (
    userId: string,
    todoId: string,
    todoDetails: Partial<ITodoModel>
  ): Promise<ITodoModel> => { ... };
}

Как вы можете видеть из приведенного выше примера кода, наша todoTable внедряется в TodoRepository, так же как и любой класс, удовлетворяющий этому TodoAppDataSource‹ITodoModel›, может быть внедрен в TodoRepository.

Уровень 5 (база данных)

Под номером 5 мы сохранили наш слой данных, он инкапсулирует все наши запросы к БД и предоставляет их с помощью единого интерфейса, существует один базовый класс для каждого типа БД, см. пример, показанный ниже для mongoDB и postgresql.

Общий интерфейс

export interface TodoAppDataSource<T> {
  create(data: T): Promise<T>;
  findOne(filter: Partial<T>, project?: Projection): Promise<T>;
  findMany(filter: Partial<T>, project?: Projection): Promise<T[]>;
  findOneAndUpdate(filter: Partial<T>, updates: Partial<T>): Promise<T>;
}

Базовый класс MongoDB

export class MongoDataSource<T> implements TodoAppDataSource<T> {
  private table: mongoose.Model<T>;
  constructor(tableName: DB_TABLES) {
    this.table = ALL_TABLES[tableName] as mongoose.Model<T>;
  }

  public async findOne<T>(
    selectQuery: Partial<T>,
    project: Projection = {}
  ): Promise<T> {
    return this.table.findOne(selectQuery as FilterQuery<T>, project);
  }

  public async create<T>(data: T): Promise<T> {
    const newRecord = new this.table(data);
    return newRecord.save() as Promise<T>;
  }

  public async findOneAndUpdate<T>(
    selectQuery: Partial<T>,
    updates: Partial<T>
  ): Promise<T> {
    return this.table.findOneAndUpdate(selectQuery as FilterQuery<T>, updates, {
      new: true,
    });
  }

  public findMany = async (
    filter: Partial<T>,
    project?: Projection
  ): Promise<Array<T>> => {
    const result = await this.table.find(filter as FilterQuery<T>, project);
    return result as unknown as Promise<Array<T>>;
  };
}

export class UserTable extends MongoDataSource<IUserModel> {
  constructor() {
    super(DB_TABLES.USER);
  }
}

Подобно приведенным выше запросам mongoDB, мы можем создать класс для любого типа базы данных и можем внедрить этот уровень (уровень 5 — БД) в другой уровень (уровень 4 — репозиторий), просто позаботившись о том, чтобы он реализовал единый интерфейс для всех базовых классов БД.

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

Заключение

Исследование — это формализованное любопытство, это целенаправленное тыкание и подглядывание. Не существует жесткого и быстрого правила настройки или эффективного структурирования репозитория кода. Следовательно, исследовать, исследовать и экспериментировать — лучший способ.

Краткий способ написания кода и структурирования — единственная причина этой статьи. Любые предложения или исправления в этой статье приветствуются. Вернусь с еще одной интересной статьей. Удачного кодирования до тех пор!

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.