استفاده از اصول SOLID در NestJS

معرفی
در دنیای توسعه نرم افزار، نوشتن کدهای تمیز، قابل نگهداری و مقیاس پذیر از اهمیت بالایی برخوردار است. یکی از راه های رسیدن به این هدف، پیروی از اصول SOLID است، مجموعه ای از پنج اصل طراحی که به توسعه دهندگان کمک می کند تا سیستم های نرم افزاری قوی و انعطاف پذیر ایجاد کنند.
در این مقاله، نحوه اعمال اصول SOLID را در زمینه Nest، یک چارچوب محبوب برای ساخت برنامههای مقیاسپذیر و ماژولار با TypeScript، بررسی خواهیم کرد. ما به هر اصل می پردازیم و نمونه های کد مختصری را برای نشان دادن اجرای آنها در پروژه NestJS ارائه می دهیم. در پایان این راهنما، شما درک درستی از نحوه استفاده از این اصول برای نوشتن کدهای تمیزتر و قابل نگهداری تر در برنامه های NestJS خود خواهید داشت. بنابراین، بیایید شروع کنیم و توسعه NestJS خود را با اصول SOLID ارتقا دهیم!
اصول SOLID که در این مقاله به آنها خواهیم پرداخت عبارتند از:
- اصل مسئولیت واحد (SRP)
- اصل باز-بسته (OCP)
- اصل جایگزینی لیسکوف (LSP)
- اصل جداسازی رابط (ISP)
- اصل وارونگی وابستگی (DIP)
1. اصل مسئولیت واحد (SRP):
همانطور که از نامش پیداست، اصل مسئولیت واحد پیشنهاد می کند که هر ماژول یا کلاس نرم افزار باید یک نقش یا مسئولیت خاص داشته باشد. این اصل در مورد انسجام در کلاس ها است و هدف آن این است که طراحی نرم افزار را قابل درک تر، انعطاف پذیرتر و قابل نگهداری تر کند.
بیایید مثالی را در زمینه یک برنامه NestJS در نظر بگیریم:
// Before applying SRP
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
createUser() {
// code to create a user
}
deleteUser() {
// code to delete a user
}
sendEmail() {
// code to send an email
}
}
در مثال بالا، کلاس UserService مدیریت کاربر و ارسال ایمیل را مدیریت می کند که اصل مسئولیت واحد را نقض می کند.
برای پایبندی به SRP، میتوانیم کد را به صورت زیر تغییر دهیم:
// After applying SRP
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
createUser() {
// code to create a user
}
deleteUser() {
// code to delete a user
}
}
@Injectable()
export class EmailService {
sendEmail() {
// code to send an email
}
}
در حال حاضر، ما دو کلاس جداگانه داریم که هر کدام یک مسئولیت واحد را بر عهده دارند. UserService مسئولیت مدیریت کاربر را بر عهده دارد و EmailService مسئولیت ارسال ایمیل را بر عهده دارد. این باعث می شود کد ما قابل نگهداری تر و درک آن آسان تر باشد.
2. اصل باز-بسته (OCP):
اصل باز-بسته بیان میکند که نهادهای نرمافزار (کلاسها، ماژولها، توابع و غیره) باید برای توسعه باز باشند، اما برای اصلاح بسته باشند. این به این معنی است که یک کلاس باید به راحتی بدون تغییر خود کلاس قابل گسترش باشد.
بیایید این اصل را با یک مثال NestJS توضیح دهیم:
قبل از اعمال OCP:
// greeter.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class GreeterService {
greeting(type: string) {
if (type === 'formal') {
return 'Good day to you.';
} else if (type === 'casual') {
return 'Hey!';
}
}
}
در مثال بالا، اگر بخواهیم نوع جدیدی از احوالپرسی را اضافه کنیم، باید روش احوالپرسی را در کلاس GreeterService تغییر دهیم که اصل Open-Closed را نقض می کند.
برای پایبندی به OCP، میتوانیم کد را به صورت زیر تغییر دهیم:
// greeting.interface.ts
export interface Greeting {
greet(): string;
}
// formalGreeting.ts
import { Greeting } from './greeting.interface';
export class FormalGreeting implements Greeting {
greet() {
return 'Good day to you.';
}
}
// casualGreeting.ts
import { Greeting } from './greeting.interface';
export class CasualGreeting implements Greeting {
greet() {
return 'Hey!';
}
}
// greeter.service.ts
import { Injectable } from '@nestjs/common';
import { Greeting } from './greeting.interface';
@Injectable()
export class GreeterService {
greeting(greeting: Greeting) {
return greeting.greet();
}
}
در حال حاضر، هر کلاس و رابط در فایل خود تعریف شده است، و کد را سازماندهی کرده و مدیریت آن را آسان تر می کند. اگر بخواهیم نوع جدیدی از احوالپرسی اضافه کنیم، میتوانیم به سادگی یک واسط Greeting پیادهسازی کنیم، بدون اینکه کلاسهای موجود را تغییر دهیم. این به اصل باز-بسته پایبند است.
3. اصل جایگزینی لیسکوف (LSP):
اصل جایگزینی لیسکوف (LSP) مفهومی در برنامه نویسی شی گرا است که بیان می کند که در یک برنامه، اشیاء یک سوپرکلاس باید بدون تأثیر بر صحت برنامه، با اشیاء یک زیر کلاس جایگزین شوند. این در مورد اطمینان از این است که یک کلاس فرعی می تواند برای کلاس والد خود بدون شکستن عملکرد برنامه شما قرار گیرد.
بیایید این اصل را با یک مثال NestJS توضیح دهیم:
class Bird {
fly(speed: number): string {
return `Flying at ${speed} km/h`;
}
}
class Eagle extends Bird {
dive(): void {
// ...
}
fly(speed: number): string {
return `Soaring through the sky at ${speed} km/h`;
}
}
// LSP Violation:
class Penguin extends Bird {
fly(): never {
throw new Error("Sorry, I can't fly");
}
}
در مثال بالا، کلاس Eagle که از کلاس Bird به ارث میرسد، با رعایت اصل جایگزینی Liskov، روش fly را با همان تعداد آرگومان لغو میکند. با این حال، کلاس پنگوئن اصل جایگزینی Liskov را نقض می کند زیرا نمی تواند پرواز کند، و بنابراین، روش fly خطا می دهد. این بدان معنی است که یک شی پنگوئن را نمی توان بدون تغییر در صحت برنامه جایگزین شی Bird کرد.
برای پایبندی به اصل جایگزینی Liskov، باید اطمینان حاصل کنیم که کلاسهای فرعی رفتار کلاس والد را طوری تغییر نمیدهند که عملکرد برنامه ما را خراب کند. در مورد مثال پرنده، عقاب و پنگوئن ما، میتوانیم کد را اصلاح کنیم تا متد fly را از کلاس Bird حذف کنیم و در عوض از رابطها برای تعریف قابلیتهای انواع مختلف پرندگان استفاده کنیم.
interface FlyingBird {
fly(speed: number): string;
}
interface NonFlyingBird {
waddle(speed: number): string;
}
در مرحله بعد، کلاس های Eagle و Penguin خود را با پیاده سازی این رابط ها تعریف می کنیم:
class Eagle implements FlyingBird {
fly(speed: number): string {
return `Soaring through the sky at ${speed} km/h`;
}
}
class Penguin implements NonFlyingBird {
waddle(speed: number): string {
return `Waddling at ${speed} km/h`;
}
}
در حال حاضر، ما دو کلاس جداگانه برای Eagle و Penguin داریم که رابط های مختلفی را بر اساس توانایی های خود پیاده سازی می کنند. به این ترتیب، ما اصل جایگزینی لیسکوف را نقض نمی کنیم زیرا تظاهر نمی کنیم که یک پنگوئن می تواند پرواز کند. در عوض، ما به وضوح تعریف میکنیم که هر نوع پرنده چه کاری میتواند انجام دهد و بر اساس تواناییهایشان با آنها رفتار متفاوتی میکنیم.
این رویکرد همچنین کد ما را انعطافپذیرتر و نگهداری آسانتر میکند. اگر در آینده نیاز به افزودن نوع جدیدی از پرنده داشته باشیم، می توانیم به سادگی یک کلاس جدید برای آن ایجاد کنیم و رابط مناسب را بر اساس توانایی های آن پیاده سازی کنیم.
4. اصل جداسازی رابط (ISP):
اصل جداسازی رابط (ISP) بیان میکند که هیچ مشتری نباید مجبور شود به روشهایی که استفاده نمیکند وابسته باشد. به عبارت دیگر، کلاینت ها نباید مجبور به پیاده سازی رابط هایی شوند که از آنها استفاده نمی کنند. این اصل ایجاد رابط های ریز دانه و ویژه مشتری را ترویج می کند.
هدف پشت این اصل حذف کدهای غیرضروری از کلاس ها برای کاهش باگ های غیرمنتظره زمانی است که کلاس توانایی انجام یک عمل را ندارد. ISP رابط های کوچکتر و هدفمندتر را تشویق می کند. بر اساس این مفهوم، چندین رابط کاربری خاص به یک رابط کاربری عمومی ترجیح داده می شود.
بیایید این اصل را با یک مثال TypeScript توضیح دهیم:
interface FullFeatureUser {
viewAd(): void;
skipAd(): void;
startParty(): void;
}
class User {
viewAd(): void {
// ...
}
}
class FreeUser extends User implements FullFeatureUser {
skipAd(): void {
throw new Error("Sorry, I can't skip ads");
}
startParty(): void {
throw new Error("Sorry, I can't start parties");
}
}
class PremiumUser extends User implements FullFeatureUser {
skipAd(): void {
// ...
}
startParty(): void {
// ...
}
}
در مثال بالا، کلاس FreeUser مجبور به پیاده سازی متدهای skipAd و startParty است، حتی اگر از آنها استفاده نمی کند. این نقض اصل جداسازی رابط است. برای پایبندی به ISP، میتوانیم رابطهای خاصتری ایجاد کنیم:
interface User {
viewAd(): void;
}
interface PremiumFeatureUser {
skipAd(): void;
startParty(): void;
}
class FreeUser implements User {
viewAd(): void {
// ...
}
}
class PremiumUser implements User, PremiumFeatureUser {
viewAd(): void {
// ...
}
skipAd(): void {
// ...
}
startParty(): void {
// ...
}
}
با این تغییرات، هر کلاس فقط روش هایی را که استفاده می کند، با رعایت اصل جداسازی رابط، پیاده سازی می کند. این رویکرد خطر اشکالات را کاهش میدهد و کد ما را انعطافپذیرتر میکند و نگهداری آن را آسانتر میکند.
5. اصل وارونگی وابستگی (DIP):
اصل وابستگی وارونگی (DIP) اصل نهایی در متدولوژی طراحی SOLID است. بیان می کند که ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند. در عوض، هر دو باید به انتزاعات بستگی داشته باشند. انتزاع ها نباید بر جزئیات تکیه کنند. جزئیات باید به انتزاعات بستگی داشته باشد.
Dependency Inversion Principle یک اصل طراحی است که به جداسازی ماژول های نرم افزار کمک می کند. این اصل نقش حیاتی در کنترل جفت بین ماژول های مختلف یک برنامه دارد.
بیایید این اصل را با یک مثال TypeScript توضیح دهیم:
class MySQLDatabase {
save(data: string): void {
// Save data to MySQL database
}
}
class UserService {
private database: MySQLDatabase;
constructor(database: MySQLDatabase) {
this.database = database;
}
saveUser(user: string): void {
this.database.save(user);
}
}
در مثال بالا، کلاس UserService به شدت با کلاس MySQLDatabase همراه است. این بدان معنی است که اگر می خواهید سیستم پایگاه داده را تغییر دهید (مانند تغییر از MySQL به MongoDB)، باید کلاس UserService را نیز تغییر دهید. این نقض اصل وارونگی وابستگی است.
برای پایبندی به DIP، میتوانیم یک انتزاع (رابط) بین کلاسهای UserService و MySQLDatabase معرفی کنیم:
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string): void {
// Save data to MySQL database
}
}
class UserService {
private database: Database;
constructor(database: Database) {
this.database = database;
}
saveUser(user: string): void {
this.database.save(user);
}
}
اکنون، کلاس UserService به رابط پایگاه داده بستگی دارد، نه به کلاس MySQLDatabase عینی. این بدان معناست که شما به راحتی می توانید با ایجاد یک کلاس جدید که رابط پایگاه داده را پیاده سازی می کند، به یک سیستم پایگاه داده متفاوت سوئیچ کنید. این رویکرد کد شما را انعطاف پذیرتر و نگهداری آسان تر می کند.
نتیجه:
اصول SOLID مجموعه ای از اصول طراحی هستند که به توسعه دهندگان کمک می کند کدهای تمیز، قابل نگهداری و مقیاس پذیر بنویسند. با پیروی از این اصول، می توانید سیستم های نرم افزاری ایجاد کنید که به راحتی قابل درک، انعطاف پذیر و قوی هستند.
با درک و به کارگیری این اصول، می توانید مهارت های توسعه NestJS خود را داشته باشید و کدهای تمیزتر و قابل نگهداری تر را در برنامه های خود بنویسید. کد نویسی مبارک!