Implémentation d'une architecture SOLIDE et en couches dans Node.js avec TypeScript et InversifyJS

Bonjour, Habr! J'attire votre attention sur une traduction de l'article Implémentation de SOLID et de l'architecture oignon dans Node.js avec TypeScript et InversifyJS par Remo H. Jansen


Dans cet article, nous examinerons l'architecture connue sous le nom d'oignon. Architecture en couches - une approche pour construire une architecture d'application qui adhère aux principes de SOLID . Il a été créé sous l'influence de DDD et de certains principes de programmation fonctionnelle, et applique également activement le principe de l'injection de dépendances.


Contexte


Cette section décrit certaines des approches et principes de développement logiciel nécessaires pour comprendre une architecture en couches.


Principe de partage des responsabilités


La responsabilité fait référence à divers aspects des fonctionnalités d'un logiciel. Par exemple, la «logique métier» et l'interface à travers laquelle elle est utilisée sont des responsabilités différentes.


La séparation des responsabilités permet d'isoler le code qui implémente chaque responsabilité. Par exemple, la modification de l'interface ne devrait pas nécessiter de changer le code de la logique métier, etc.


Principes SOLIDES


SOLID est un acronyme pour les cinq principes suivants:
image


Principe de responsabilité exclusive


Une classe ne devrait avoir qu'une seule responsabilité. (Note du traducteur: une formulation plus précise, à mon avis, est: «Une classe doit avoir une et une seule raison de changer»)

Le moyen le plus efficace de casser une application est de créer une classe divine.


La classe divine est une classe qui en sait et en fait trop. Cette approche est un bon exemple d'anti-modèle.

. , , , . , , .


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


: , .


image



. , . , Product , SQL Server.


, . , , , . , .



. . , , , .



, . MVC, ; . , , .


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


MVC , , . , , -. MVC — .


-


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


- . -, , - . - . - :


  • -.
  • -.
  • , .

. , . , , . .


image


. , , , , .


-. , , (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. :


image


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


AircraftRepository , , AircraftController AircraftRepository, :


image


, () ( ). , , .


image


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 ...
}

:


image


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 . :


image


, , 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 ...

}

, , :


image


: (, ) (, ).


, — — .


image


Cette approche architecturale a fonctionné pour moi dans de grands projets d'entreprise au cours des dix dernières années. De plus, j'ai fini de diviser l'énorme monolithe en couches en microservices qui suivent la même architecture. J'aime dire que dans le cas des microservices sur l'architecture en couches, nous avons un "sac d'ampoules".


J'espère que cet article vous a plu! Veuillez partager vos réflexions dans les commentaires ou directement à l'auteur @RemoHJansen .


Un bonus pour ceux qui ont lu jusqu'à la fin est un référentiel avec un exemple de code de travail.


All Articles