Implementando o SOLID e a arquitetura em camadas no Node.js com TypeScript e InversifyJS

Olá Habr! Trago à sua atenção uma tradução do artigo Implementando o SOLID e a arquitetura de cebola no Node.js com TypeScript e InversifyJS por Remo H. Jansen


Neste artigo, veremos a arquitetura conhecida como cebola. Arquitetura em camadas - uma abordagem para a construção de arquitetura de aplicativos que segue os princípios do SOLID . Foi criado sob a influência do DDD e alguns princípios de programação funcional, e também aplica ativamente o princípio da injeção de dependência.


fundo


Esta seção descreve algumas das abordagens e princípios de desenvolvimento de software necessários para entender uma arquitetura em camadas.


Princípio da divisão de responsabilidades


Responsabilidade refere-se a vários aspectos da funcionalidade de um software. Por exemplo, “lógica de negócios” e a interface pela qual é usada são responsabilidades diferentes.


A separação de responsabilidades permite isolar o código que implementa cada responsabilidade, por exemplo, alterar a interface não deve exigir a alteração do código da lógica de negócios e assim por diante.


Princípios do SOLID


SOLID é um acrônimo para os cinco princípios a seguir:
imagem


Princípio da responsabilidade exclusiva


Uma classe deve ter apenas uma responsabilidade. (Nota do tradutor: uma redação mais precisa, na minha opinião, é: "Uma turma deve ter um e apenas um motivo para mudanças")

A maneira mais eficaz de quebrar um aplicativo é criar uma classe divina.


A classe divina é uma classe que sabe e faz muito. Essa abordagem é um bom exemplo de um antipadrão.

. , , , . , , .


TypeScript . email, .


class Person {
    public name : string;
    public surname : string;
    public email : string;
    constructor(name : string, surname : string, email : string){
        this.surname = surname;
        this.name = name;
        if(this.validateEmail(email)) {
          this.email = email;
        }
        else {
            throw new Error("Invalid email!");
        }
    }
    validateEmail(email : string) {
        var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
        return re.test(email);
    }
    greet() {
        alert("Hi!");
    }
}

email Email:


class Email {
    public email : string;
    constructor(email : string){
        if(this.validateEmail(email)) {
          this.email = email;
        }
        else {
            throw new Error("Invalid email!");
        }        
    }
    validateEmail(email : string) {
        var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
        return re.test(email);
    }
}

class Person {
    public name : string;
    public surname : string;
    public email : Email;
    constructor(name : string, surname : string, email : Email){
        this.email = email;
        this.name = name;
        this.surname = surname;
    }
    greet() {
        alert("Hi!");
    }
}

, , , /.


/


.

, /:


class Rectangle {
    public width: number;
    public height: number;
}

class Circle {
    public radius: number;
}

function getArea(shapes: (Rectangle|Circle)[]) {
    return shapes.reduce(
        (previous, current) => {
            if (current instanceof Rectangle) {
                return current.width * current.height;
            } else if (current instanceof Circle) {
                return current.radius * current.radius * Math.PI;
            } else {
                throw new Error("Unknown shape!")
            }
        },
        0
    );
}

( ). , . , ( ), , getArea, , .


, , :


interface Shape {
    area(): number;
}

class Rectangle implements Shape {

    public width: number;
    public height: number;

    public area() {
        return this.width * this.height;
    }
}

class Circle implements Shape {

    public radius: number;

    public area() {
        return this.radius * this.radius * Math.PI;
    }
}

function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}

( ) ( ).



.

, , . :


function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}

Shape , . , getArea , Shape . , TypeScript, (, Shape area, . , , .



, , , .

.
, : Rectangle Circle. , , . Shape:


interface Shape {
    area(): number;
    serialize(): string;
}

class Rectangle implements Shape {

    public width: number;
    public height: number;

    public area() {
        return this.width * this.height;
    }

    public serialize() {
        return JSON.stringify(this);
    }
}

class Circle implements  Shape {

    public radius: number;

    public area() {
        return this.radius * this.radius * Math.PI;
    }

    public serialize() {
        return JSON.stringify(this);
    }

}

, :


function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}

, , , :


// ...
return rectangle.serialize();

, serialize Shape . -, — . .


, - , , , :


interface RectangleInterface {
    width: number;
    height: number;
}

interface CircleInterface {
    radius: number;
}

interface Shape {
    area(): number;
}

interface Serializable {
    serialize(): string;
}

, .


class Rectangle implements RectangleInterface, Shape {

    public width: number;
    public height: number;

    public area() {
        return this.width * this.height;
    }
}

class Circle implements CircleInterface, Shape {

    public radius: number;

    public area() {
        return this.radius * this.radius * Math.PI;
    }
}

function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}

, .


class RectangleDTO implements RectangleInterface, Serializable {
    public width: number;
    public height: number;

    public serialize() {
        return JSON.stringify(this);
    }
}

class CircleDTO implements CircleInterface, Serializable {
    public radius: number;

    public serialize() {
        return JSON.stringify(this);
    }
}

, ( ) ( , ).


, RectangleDTO Rectangle (DRY). , . , , . , .


DRY, DRY SOLID. , DRY , SOLID " " .



. - .

. , .


, SOLID D. , , SOLID. , SOLID . , , , :


  • , , .
  • ( ).
  • , , ( /).

SOLID . , , . , JavaScript ES5 ES6, SOLID . , TypeScript .


-- MVC


: , .


imagem



. , . , Product , SQL Server.


, . , , , . , .



. . , , , .



, . MVC, ; . , , .


MVC ( , ) . , . . — . - — . , . , , -.


MVC , , . , , -. MVC — .


-


MVC , - . , . -, . - , . , , -.


- . -, , - . - . - :


  • -.
  • -.
  • , .

. , . , , . .


imagem


. , , , , .


-. , , (Data Mapper) .


. , catalog , . , , SQL- Sharepoint (CAML). .



( ):



— . , , , , , .


DDD, "". :



, , - . . ( ) - . , , ( SQL) ( HTTP).


, , , . -. (, ) (, ) , , - .


: (), (). , — HTTP , , . AircraftController AircraftRepository:


import { inject } from "inversify";
import { response, controller, httpGet } from "inversify-express-utils";
import * as express from "express";
import { AircraftRepository } from "@domain/interfaces";
import { Aircraft } from "@domain/entities/aircraft";
import { TYPE } from "@domain/types";

@controller("/api/v1/aircraft")
export class AircraftController {

    @inject(TYPE.AircraftRepository) private readonly _aircraftRepository: AircraftRepository;

    @httpGet("/")
    public async get(@response() res: express.Response) {
        try {
            return await this._aircraftRepository.readAll();
        } catch (e) {
            res.status(500).send({ error: "Internal server error" });
        }

    }

    // ...

}

AircraftController HTTP AircraftRepository. AircraftRepository HTTP. :


imagem


. "comp" (composition) , AircraftRepository AircraftController. "ref" (reference) , AircraftController Aircraft.


AircraftRepository , , AircraftController AircraftRepository, :


imagem


, () ( ). , , .


imagem


AircraftRepository . , , - . "" InversifyJS. InversifyJS , @inject, . , :


@inject(TYPE.AircraftRepository) private readonly _aircraftRepository: AircraftRepository;

, InversifyJS :


container.bind<AircraftRepository>(TYPE.AircraftRepository).to(AircraftRepositoryImpl);

AircratRepository Repository<T>, .


import { Aircraft } from "@domain/entities/aircraft";

export interface Repository<T> {
    readAll(): Promise<T[]>;
    readOneById(id: string): Promise<T>;
    // ...
}

export interface AircraftRepository extends Repository<Aircraft> {
    // Add custom methods here ...
}

:


imagem


Repository<T> AircraftRepository:


  • Repository<T> Gene- ricRepositoryImpl<D, E>
  • AircraftRepository AircraftRepositoryImpl.

Repository<T>


import { injectable, unmanaged } from "inversify";
import { Repository } from "@domain/interfaces";
import { EntityDataMapper } from "@dal/interfaces";
import { Repository as TypeOrmRepository } from "typeorm";

@injectable()
export class GenericRepositoryImpl<TDomainEntity, TDalEntity> implements Repository<TDomainEntity> {

    private readonly _repository: TypeOrmRepository<TDalEntity>;
    private readonly _dataMapper: EntityDataMapper<TDomainEntity, TDalEntity>;

    public constructor(
        @unmanaged() repository: TypeOrmRepository<TDalEntity>,
        @unmanaged() dataMapper: EntityDataMapper<TDomainEntity, TDalEntity>
    ) {
        this._repository = repository;
        this._dataMapper = dataMapper;
    }

    public async readAll() {
        const entities = await this._repository.readAll();
        return entities.map((e) => this._dataMapper.toDomain(e));
    }

    public async readOneById(id: string) {
        const entity = await this._repository.readOne({ id });
        return this._dataMapper.toDomain(entity);
    }

    // ...

}

, EntityDataMapper TypeOrmRepository . .


, EntityDataMapper:


export interface EntityDataMapper<Domain, Entity> {

    toDomain(entity: Entity): Domain;
    toDalEntity(domain: Domain): Entity;
}

EntityDataMapper:


import { toDateOrNull, toLocalDateOrNull } from "@lib/universal/utils/date_utils";
import { Aircraft } from "@domain/entities/aircraft";
import { AircraftEntity } from "@dal/entities/aircraft";
import { EntityDataMapper } from "@dal/interfaces";

export class AircraftDataMapper implements EntityDataMapper<Aircraft, AircraftEntity> {

    public toDomain(entity: AircraftEntity): Aircraft {
        // ...
    }

    public toDalEntity(mortgage: Aircraft): AircraftEntity {
        // ...
    }
}

EntityDataMapper , TypeOrmRepository . :


imagem


, , AircraftRepository:


import { inject, injectable } from "inversify";
import { Repository as TypeOrmRepository } from "typeorm";
import { AircraftRepository } from "@domain/interfaces";
import { Aircraft } from "@domain/entities/aircraft";
import { GenericRepositoryImpl } from "@dal/generic_repository";
import { AircraftEntity } from "@dal/entities/aircraft";
import { AircraftDataMapper } from "@dal/data_mappers/aircraft";
import { TYPE } from "@dal/types";

@injectable()
export class AircraftRepositoryImpl
    extends GenericRepositoryImpl<Aircraft, AircraftEntity>
    implements AircraftRepository {

    public constructor(
        @inject(TYPE.TypeOrmRepositoryOfAircraftEntity) repository: TypeOrmRepository<AircraftEntity>
    ) {
        super(repository, new AircraftDataMapper())
    }

    // Add custom methods here ...

}

, , :


imagem


: (, ) (, ).


, — — .


imagem


Essa abordagem arquitetônica funcionou para mim em grandes projetos empresariais nos últimos dez anos. Além disso, terminei de dividir o enorme monólito em camadas em microsserviços que seguem a mesma arquitetura. Eu gosto de dizer que, no caso de microsserviços em arquitetura em camadas, temos um "saco de lâmpadas".


Espero que tenha gostado deste artigo! Compartilhe seus pensamentos nos comentários ou diretamente com o autor @RemoHJansen .


Um bônus para quem leu até o fim é um repositório com um exemplo de código funcional.


All Articles