مرحبا يا هابر! أوجه انتباهكم إلى ترجمة مقال تنفيذ SOLID وبنية البصل في Node.js مع TypeScript و InversifyJS بواسطة Remo H. Jansen
في هذه المقالة ، سنلقي نظرة على العمارة المعروفة باسم البصل. بنية الطبقات - نهج لبناء بنية التطبيقات التي تلتزم بمبادئ SOLID . تم إنشاؤه تحت تأثير DDD وبعض مبادئ البرمجة الوظيفية ، كما يطبق بنشاط مبدأ حقن التبعية.
خلفية
يصف هذا القسم بعض مناهج ومبادئ تطوير البرمجيات اللازمة لفهم بنية الطبقات.
مبدأ تقسيم المسؤولية
تشير المسؤولية إلى جوانب مختلفة من وظائف البرنامج. على سبيل المثال ، "منطق الأعمال" والواجهة التي يتم استخدامها من خلالها هي مسؤوليات مختلفة.
يسمح فصل المسؤولية بعزل الشفرة التي تنفذ كل مسؤولية. على سبيل المثال ، يجب ألا يتطلب تغيير الواجهة تغيير رمز منطق الأعمال ، وما إلى ذلك.
مبادئ SOLID
SOLID هو اختصار للمبادئ الخمسة التالية:

مبدأ المسؤولية الوحيدة
يجب أن يكون للفصل مسؤولية واحدة فقط. (ملاحظة المترجم: صياغة أكثر دقة ، في رأيي ، هي: "يجب أن يكون للفصل سبب واحد وسبب واحد فقط للتغييرات")
الطريقة الأكثر فعالية لكسر تطبيق هو إنشاء فئة إلهية.
الطبقة الإلهية هي فئة تعرف وتفعل الكثير. هذا النهج هو مثال جيد على النمط المضاد.
. , , , . , , .
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())
}
}
, , :

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

لقد نجح هذا النهج المعماري بالنسبة لي في مشاريع المشاريع الكبيرة على مدى السنوات العشر الماضية. أيضا ، انتهيت من تقسيم الطبقة المتراصة الضخمة إلى خدمات صغيرة تتبع نفس العمارة. أود أن أقول إنه في حالة الخدمات الصغيرة في العمارة ذات الطبقات ، لدينا "حقيبة من المصابيح".
آمل أن يحظى هذا المقال بإعجابكم! يرجى مشاركة أفكارك في التعليقات أو مباشرة إلى المؤلف RemoHJansen .
مكافأة لأولئك الذين قرأوا حتى النهاية هي مستودع مع مثال رمز العمل.