Menerapkan arsitektur SOLID dan layered di Node.js dengan TypeScript dan InversifyJS

Halo, Habr! Saya membawa kepada Anda terjemahan dari artikel Menerapkan SOLID dan arsitektur bawang di Node.js dengan TypeScript dan InversifyJS oleh Remo H. Jansen


Pada artikel ini, kita akan melihat arsitektur yang dikenal sebagai bawang. Layered architecture - suatu pendekatan untuk membangun arsitektur aplikasi yang menganut prinsip-prinsip SOLID . Itu dibuat di bawah pengaruh DDD dan beberapa prinsip pemrograman fungsional, dan juga secara aktif menerapkan prinsip injeksi ketergantungan.


Latar Belakang


Bagian ini menjelaskan beberapa pendekatan dan prinsip pengembangan perangkat lunak yang diperlukan untuk memahami arsitektur berlapis.


Prinsip pembagian tanggung jawab


Tanggung jawab mengacu pada berbagai aspek fungsi perangkat lunak. Misalnya, "logika bisnis" dan antarmuka yang digunakannya merupakan tanggung jawab yang berbeda.


Pemisahan tanggung jawab memungkinkan isolasi kode yang mengimplementasikan masing-masing tanggung jawab.Sebagai contoh, mengubah antarmuka seharusnya tidak memerlukan mengubah kode logika bisnis, dan sebagainya.


Prinsip SOLID


SOLID adalah akronim untuk lima prinsip berikut:
gambar


Prinsip tanggung jawab tunggal


Kelas seharusnya hanya memiliki satu tanggung jawab. (Catatan Penerjemah: kata-kata yang lebih tepat, menurut saya, adalah: "Kelas harus memiliki satu dan hanya satu alasan untuk perubahan")

Cara paling efektif untuk memecah suatu aplikasi adalah dengan menciptakan kelas dewa.


Kelas ilahi adalah kelas yang tahu dan melakukan terlalu banyak. Pendekatan ini adalah contoh yang baik dari pola-anti.

. , , , . , , .


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


: , .


gambar



. , . , Product , SQL Server.


, . , , , . , .



. . , , , .



, . MVC, ; . , , .


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


MVC , , . , , -. MVC — .


-


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


- . -, , - . - . - :


  • -.
  • -.
  • , .

. , . , , . .


gambar


. , , , , .


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


gambar


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


AircraftRepository , , AircraftController AircraftRepository, :


gambar


, () ( ). , , .


gambar


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

:


gambar


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


gambar


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

}

, , :


gambar


: (, ) (, ).


, — — .


gambar


Pendekatan arsitektur ini telah bekerja untuk saya dalam proyek perusahaan besar selama sepuluh tahun terakhir. Juga, saya akhirnya membagi monolit berlapis besar menjadi layanan microser yang mengikuti arsitektur yang sama. Saya suka mengatakan bahwa dalam kasus layanan microser pada arsitektur berlapis, kami memiliki "tas umbi."


Saya harap Anda menikmati artikel ini! Silakan bagikan pemikiran Anda dalam komentar atau langsung ke penulis @RemoHJansen .


Bonus bagi mereka yang telah membaca sampai akhir adalah repositori dengan contoh kode kerja.


All Articles