Hallo Habr! Ich mache Sie auf eine Ăśbersetzung des Artikels Implementieren von SOLID und der Zwiebelarchitektur in Node.js mit TypeScript und InversifyJS von Remo H. Jansen aufmerksam
In diesem Artikel werden wir uns die als Zwiebel bekannte Architektur ansehen. Layered Architecture - Ein Ansatz zum Erstellen von Anwendungsarchitekturen, der den Prinzipien von SOLID entspricht . Es wurde unter dem Einfluss von DDD und einigen Prinzipien der funktionalen Programmierung erstellt und wendet auch aktiv das Prinzip der Abhängigkeitsinjektion an.
Hintergrund
In diesem Abschnitt werden einige der Softwareentwicklungsansätze und -prinzipien beschrieben, die zum Verständnis einer Schichtarchitektur erforderlich sind.
Prinzip der Aufteilung der Verantwortung
Die Haftung bezieht sich auf verschiedene Aspekte der Funktionalität einer Software. Zum Beispiel sind „Geschäftslogik“ und die Schnittstelle, über die sie verwendet wird, unterschiedliche Verantwortlichkeiten.
Die Trennung der Verantwortlichkeiten ermöglicht das Isolieren des Codes, der jede Verantwortung implementiert. Zum Beispiel sollte das Ändern der Schnittstelle keine Änderung des Codes der Geschäftslogik usw. erfordern.
FESTE Prinzipien
SOLID ist eine AbkĂĽrzung fĂĽr die folgenden fĂĽnf Prinzipien:

Grundsatz der alleinigen Verantwortung
Eine Klasse sollte nur eine Verantwortung haben. (Anmerkung des Übersetzers: Eine genauere Formulierung lautet meiner Meinung nach: „Eine Klasse muss nur einen Grund für Änderungen haben“)
Der effektivste Weg, eine Anwendung zu brechen, besteht darin, eine göttliche Klasse zu erstellen.
Die göttliche Klasse ist eine Klasse, die zu viel weiß und tut. Dieser Ansatz ist ein gutes Beispiel für ein Anti-Muster.
. , , , . , , .
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
: , .

. , . , Product , SQL Server.
, . , , , . , .
. . , , , .
, . MVC, ; . , , .
MVC ( , ) . , . . — . - — . , . , , -.
MVC , , . , , -. MVC — .
-
MVC , - . , . -, . - , . , , -.
- . -, , - . - . - :
. , . , , . .

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

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

, () ( ). , , .

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> {
}
:

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

, , 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())
}
}
, , :

: (, ) (, ).
, — — .

Dieser architektonische Ansatz hat sich in den letzten zehn Jahren bei großen Unternehmensprojekten für mich bewährt. Außerdem habe ich den riesigen Schichtmonolithen in Mikrodienste unterteilt, die der gleichen Architektur folgen. Ich möchte sagen, dass wir im Fall von Microservices auf Schichtarchitektur eine "Tüte mit Glühbirnen" haben.
Ich hoffe dir hat dieser Artikel gefallen! Bitte teilen Sie Ihre Gedanken in den Kommentaren oder direkt an den Autor @RemoHJansen .
Ein Bonus fĂĽr diejenigen, die bis zum Ende gelesen haben, ist ein Repository mit einem funktionierenden Codebeispiel.