Проект API с точки зрения разработчика Android

Проект API с точки зрения разработчика Android

Бэкендер

По словам знакомых бэкенд-разработчиков, проще всего в моем случае начать писать серверное приложение на NodeJS с использованием TypeScript.

Любопытно то, что платформа NodeJS, предназначенная только для работы с JavaScript, не выполняет код TypeScript. В связи с этим требуется преобразовать код TypeScript в JavaScript. Такой процесс называется транспиляцией.

Что касается IDE, я поэкспериментировал с 2 инструментами: Visual Studio Code и WebStorm. В итоге отдал предпочтение второму варианту. Как оказалось, мне намного проще работать с WebStorm, поскольку я привык к Android Studio. Оба эти инструмента входят в число продуктов JetBrains, поэтому обладают схожими принципами разработки.

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

Существует множество способов организации файлов в серверном приложении. Рассмотрим один из них, более соответствующий проекту Android:

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


Прокомментируем эту схему, проводя терминологические параллели с разработкой Android.

  • package.json  — это своего рода синтез build.gradle и AndroidManifest.xml, если посмотреть на проект с точки зрения разработчика Android. В этом файле мы определяем: имя и версию приложения; главный файл, в котором оно запускается; скрипты, например задачи Gradle; зависимости; зависимости разработки и другое.
  • __test__  — каталог для размещения модульных тестов. На стороне сервера в качестве фреймворка используется Jest, аналог JUnit в разработке Android.
  • jest.config.js  — файл, в котором мы определяем конфигурацию тестов и способ их выполнения. 
  • src  — каталог для группировки кода, что-то наподобие главного модуля приложения main.

Архитектура 

Архитектура


Точки входа 

На стороне сервера в качестве точек входа используются не Activity и Fragments, а маршруты (англ. routes). У нас нет UI, которого может коснуться пользователь. Приложения, которые в данном случае являются пользователями, вызывают разные маршруты: GET, POST, PUT и другие. Рассмотрим пример маршрута.

Пример 1. Базовая конфигурация для применения Express с пользовательским объектом Router.

import express, { Request, Response, NextFunction } from "express";
require('express-async-errors');
import { historyRoutes } from "./routes/history.routes";

const app = express();

app.use(express.json());
app.use("/history", historyRoutes);

Express  — это фреймворк, упрощающий процесс создания маршрутов. Как видно, файл app.ts импортирует historyRoutes, уже определенный файлом historic.routes.ts. Рассмотрим пример метода POST, делегированного определенному контроллеру, который перенаправляет запрос в конкретный UseCase. 

Пример 2. Объект Controller обрабатывает запрос POST.

import { Router } from "express";
import { CreateNewHistoricEntryController } from "../../controllers/CreateNewHistoricEntryController";

const historyRoutes = Router();

const createNewHistoricEntryController = new CreateNewHistoricEntryController();
historyRoutes.post("/historic", createNewHistoricEntryController.handle);

export { historyRoutes };

Внедрение зависимостей

Если вы с удовольствием работаете с Koin и Kodein в мире Android, то с легкостью освоите TSyringe в качестве библиотеки для внедрения зависимостей.

Следующий код отображает файл server.ts, в котором мы регистрируем ссылку на Singleton для FirestoreDataSource, реализующего интерфейс ISisOrgRepository. Этот файл подобен классу Application и является точкой входа приложения.

Пример 3. Добавление экземпляра репозитория Application, который будет доступен для всего приложения.

import { container } from "tsyringe";
...
function setup() {
  container.registerSingleton<ISisOrgRepository>(
    "ISisOrgRepository",
    FirestoreDataSource
  );
}
...
app.listen(port, () => {
  setup();
  console.log("Server is running...");
});

Библиотека TSyringe знает, что если кому-то потребуется ссылка на ISisOrgRepository, она обязана предоставить экземпляр FirestoreDataSource. В этом случае класс CreateNewHistoricEntryController должен создать CreateNewHistoricEntryUseCase. Но для этого необходимо попросить TSyringe разрешить зависимости (аргумент конструктора ISisOrgRepository).

Пример 4. UseCase с аннотациями, помогающими библиотеке для внедрения зависимостей.

@injectable()
class CreateNewHistoricEntryUseCase {
  constructor(
    @inject("ISisOrgRepository")
    private sisOrgRepository: ISisOrgRepository
  ) {
  }

  async execute(request: IRequest): Promise<IHistoricEntry[]> {
    const historicEntries: IHistoricEntry[] = [];
    ...
    await this.sisOrgRepository.createHistoricEntry(historicEntries);
    return historicEntries;
  }
}

export { CreateNewHistoricEntryUseCase };

Внутри CreateNewHistoricEntryController мы можем попросить библиотеку для внедрения зависимостей о содействии в создании объекта CreateNewHistoricEntryUseCase.

Пример 5. Контроллер запрашивает ссылку на контейнер UseCase.

import { Request, Response } from "express";
import { container } from "tsyringe";
import { CreateNewHistoricEntryUseCase } from "../domain/useCases/CreateNewHistoricEntryUseCase";

class CreateNewHistoricEntryController {
  async handle(request: Request, response: Response): Promise<Response> {
    const { beds, activity, crop, variety, resultIndicator, input } = request.body;

    const useCase = container.resolve(CreateNewHistoricEntryUseCase);
    const result = await useCase.execute({
      beds, activity, crop, variety, resultIndicator: resultIndicator, input
    });
    return response.status(201).json(result);
  }
}

export { CreateNewHistoricEntryController };

Тесты 

Я опробовал две очень похожие библиотеки: Jasmine и Jest. В итоге выбрал Jest, поскольку она предоставляет отличный формат вывода результатов. 

Пример 6. Отчет о результатах выполнения от Jest.

Синтаксис сильно отличается от тестов JUnit, использующих Kotlin. Однако все проясняется, если понимать describe как имя набора тестов, а it  — как имя модульного теста. Часть, касающаяся утверждений, не представляет сложности и напоминает принцип работы с библиотекой Truth. 

Пример 7. Тест с применением Jest в TypeScript — класс TimeHelper.

mport { TimeHelper } from "util/TimeHelper"

describe("Time Helper", () => {
    it("should get total minutes from 3h", () => {
        const totalMinutes = TimeHelper.getTotalMinutes("3h")
        expect(totalMinutes).toBe(180)
    })

    it("should get total minutes from 3h 15min", () => {
        const totalMinutes = TimeHelper.getTotalMinutes("3h 15min")
        expect(totalMinutes).toBe(195)
    })
})

Дополнительные рекомендации 

  • Heroku  — отличная платформа для размещения API. Она предоставляет удобный CLI. Если package.json в порядке, то при каждой отправке кода CLI автоматически выполняет скрипты (транспиляцию, установку зависимостей, запуск сервера). 
  • Insomnia  — превосходный инструмент для тестирования маршрутов. С его помощью можно создавать различные среды (разработки, продакшн и т.д.) 
  • Swagger позволяет создавать содержательную документацию, в которой можно запускать маршруты (пример по ссылке). 

Заключение 

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


Источник: https://nuancesprog.ru/p/16526/

Report Page