آشنایی با اصول SOLID در برنامه نویسی
اصول SOLID در برنامه نویسی مجموعهای از پنج اصلی است که طراحی کلاس شی گرا (Object Oriented) را هدایت و معماری خوب نرمافزار و الگوهای طراحی را ترویج میکند. آنها قوانین و بهترین شیوهها را برای ایجاد یک ساختار کلاس قوی و قابل نگهداری ارائه میکنند.
در این مطلب از مکتوب، تاریخچه اصول سالید (solid principles) را بررسی کرده و در کنار آن جزئیات هر اصل را بررسی میکنیم و یاد میگیریم که چگونه آنها را در پروژههای خود اعمال کنیم. با پیروی از این اصول، توسعهدهندگان میتوانند کدی ایجاد کنند که درک، خواندن و آزمایش آن آسانتر باشد و کار مشترک را در میان اعضای تیم تسهیل کند.
اصول SOLID در برنامه نویسی چیست؟
اصطلاح SOLID اولین بار توسط مایکل فیرز معرفی شد، در حالی که خود اصول در ابتدا توسط رابرت جی. مارتین، همچنین به نام عمو باب، در مقاله خود در سال 2000 ارائه شد. عمو باب دانشمند کامپیوتر مشهور، نویسنده کتابهای پرفروشی مانند «Clean Code» و «Clean Architecture» و یکی از شرکتکنندگان فعال در Agile Alliance است.
اصول SOLID در برنامه نویسی با مفاهیم کدنویسی تمیز، معماری شی گرا و الگوهای طراحی همسو هستند، زیرا همگی هدف مشترک ایجاد نرمافزار با کیفیت بالا را دارند. در اصل SOLID از ٥ اصل اساسی تشكیل شده است که به صورت موارد زیر هستند:
- اصل مسئولیت واحد (Single Responsibility Principle)
- اصل باز – بسته (Open-Closed Principle)
- اصل جایگزینی لیسکوف (Liskov Substitution Principle)
- اصل جداسازی رابط (Interface Segregation Principle)
- اصل وارونگی وابستگی (Dependency Inversion Principle)
اصول سالید را در همه زبانهای برنامه نویسی شی گرا مانند پایتون، جاوا اسکریپت، سی شارپ، net core و غیره میتوان به کار گرفت. در بخشهای بعدی، هر یک از این اصول SOLID در برنامه نویسی را با جزئیات بیشتری بررسی میکنیم و اهمیت آنها را نشان میدهیم و در کنار آن مثالهای عملی از نحوه به کارگیری مؤثر آنها را ارائه خواهیم کرد.
پیشنهاد مطالعه: آشنایی با مفهوم TDD در برنامه نویسی
اصل مسئولیت واحد در اصول SOLID در برنامه نویسی
اصل مسئولیت واحد (SRP) تأکید میکند که یک کلاس باید تنها یک دلیل برای تغییر داشته باشد. اصل مذکور این ایده را ترویج میکند که هر کلاس باید یک مسئولیت واحد داشته باشد و تنها یک مشکل را حل کند. با رعایت این اصل، تست، نگهداری و درک کد آسانتر میشود. استفاده از SRP در کلاسها، اجزای نرمافزار و میکروسرویسها به طراحی بهتر نرمافزار منجر شده و به جلوگیری از عوارض جانبی ناخواسته هنگام ایجاد تغییرات کمک میکند.
برای اطمینان از انطباق با SRP، استفاده از بررسیهای خودکار در طول فرآیند ساخت برای محدود کردن دامنه کلاسها میتواند مفید باشد. اگرچه این بررسی بیخطا نیست، اما میتواند به شناسایی نقضهای احتمالی این اصل کمک کرده و توسعهدهندگان را به سمت طراحی بهتر راهنمایی کند.
مثالی برای اصل مسئولیت واحد
در اینجا یک مثال در پایتون آمده است که اصل مسئولیت واحد را نشان میدهد:
class Invoice:
def __init__(self, number, customer, items):
self.number = number
self.customer = customer
self.items = items
def calculate_total(self):
total = 0
for item in self.items:
total += item.price * item.quantity
return total
def print_invoice(self):
print("Invoice Number:", self.number)
print("Customer:", self.customer)
print("Items:")
for item in self.items:
print(item.name, "-", item.price, "x", item.quantity)
print("Total:", self.calculate_total())
class Item:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
در این مثال دو کلاس داریم: Invoice و Item. کلاس Invoice وظیفه مدیریت اطلاعات فاکتور، محاسبه کل مبلغ و چاپ فاکتور را بر عهده دارد. کلاس Item نشاندهنده اقلام جداگانه در فاکتور است. کلاس Invoice یک مسئولیت دارد و آن انجام وظایف مربوط به فاکتور خواهد بود. منطق محاسبه کل مبلغ و چاپ فاکتور را در برمیگیرد. این تفکیک نگرانیها درک، آزمایش و نگهداری کد را آسانتر میکند.
اگر تغییرات یا بهروزرسانیهایی در محاسبه یا چاپ فاکتورها لازم باشد، فقط باید کلاس Invoice را تغییر دهیم و آن را بر مسئولیت خاص خود متمرکز کنیم. این کار به اصل مسئولیت واحد در اصول SOLID در برنامه نویسی پایبند است، زیرا هر کلاس یک هدف مشخص دارد و مسئولیتهای اضافی را بر عهده نمیگیرد.
با پیروی از این اصل، کد ماژولارتر، انعطافپذیرتر و توسعه آن آسانتر میشود. همچنین خطر ایجاد اشکالات، باگ یا عوارض جانبی ناخواسته هنگام ایجاد تغییرات را به حداقل میرساند، زیرا هر کلاس بر روی وظیفه خاص خود متمرکز است.
پیشنهاد مطالعه: درک مفهوم instance در برنامه نویسی و کاربردهای آن
اصل باز بسته در SOLID
اصل باز بسته (OCP) پیشنهاد میکند که کلاسها باید برای توسعه باز باشند اما برای اصلاح بسته شوند. این بدان معنی است که کلاسهای موجود نباید در صورت نیاز به افزودن عملکرد جدید، اصلاح شوند. در عوض، هدف گسترش رفتار یک کلاس بدون تغییر کد منبع آن است. این اصل از قابلیت نگهداری و ثبات کد پشتیبانی میکند.
برای پایبندی به OCP، میتوانید از انتزاعهایی مانند وراثت یا رابطهایی که جایگزینهای چندشکلی را فعال میکنند، استفاده کنید. با معرفی لایههای انتزاعی، میتوانید رفتار کلاسها را بدون تغییر مستقیم آنها گسترش دهید. پیروی از OCP تضمین میکند که کد بهراحتی قابل نگهداری و تغییر در طول زمان باقی میماند.
پیشنهاد مطالعه: بهترین کتاب های برنامه نویسی در دنیا
مثالی برای اصل باز و بسته
در اینجا یک مثال در پایتون برای اصل باز – بسته در اصول SOLID در برنامه نویسی آورده شده است:
from abc import ABC, abstractmethod
class InvoicePersistence(ABC):
@abstractmethod
def save(self, invoice):
pass
class FilePersistence(InvoicePersistence):
def save(self, invoice):
# Save the invoice to a file
print("Saving invoice to file:", invoice)
class DatabasePersistence(InvoicePersistence):
def save(self, invoice):
# Save the invoice to a database
print("Saving invoice to database:", invoice)
class PersistenceManager:
def __init__(self, persistence):
self.persistence = persistence
def save_invoice(self, invoice):
self.persistence.save(invoice)
# Usage example
invoice = "Sample Invoice"
file_persistence = FilePersistence()
db_persistence = DatabasePersistence()
pm = PersistenceManager(file_persistence)
pm.save_invoice(invoice) # Saving invoice to file: Sample Invoice
pm = PersistenceManager(db_persistence)
pm.save_invoice(invoice) # Saving invoice to database: Sample Invoice
در مثال فوق، ما یک کلاس انتزاعی InvoicePersistence داریم که قرارداد ذخیره یک فاکتور را تعریف میکند. این کلاس از اصل Open-Closed پیروی خواهد کرد و برای توسعه باز (پیادهسازیهای ماندگاری جدید را میتوان اضافه کرد) اما برای اصلاح بسته است (هیچ تغییری در کد موجود لازم نیست).
ما دو پیادهسازی مشخص از InvoicePersistence داریم: FilePersistence و DatabasePersistence. هر پیادهسازی به ترتیب منطق خاص ذخیره فاکتور در یک فایل یا پایگاه داده را مدیریت میکند. کلاس PersistenceManager بهعنوان یک ماژول سطح بالا عمل میکند که به انتزاع (InvoicePersistence) بستگی دارد تا پیادهسازیهای مشخص. این یک نمونه از InvoicePersistence را از طریق سازنده خود میپذیرد، و اجازه میدهد استراتژیهای پایداری مختلف تزریق شود.
با رعایت اصل باز-بسته در اصول SOLID در برنامه نویسی شی گرا میتوانیم بهراحتی انواع جدیدی از پیادهسازیهای ماندگار (مانند ذخیره در API، ذخیرهسازی ابری و غیره) را بدون تغییر کد موجود معرفی کنیم. ما به سادگی میتوانیم یک کلاس جدید ایجاد کرده که InvoicePersistence را پیادهسازی کند و آن را به PersistenceManager ارسال کنیم. این امر قابلیت استفاده مجدد، قابلیت نگهداری و گسترش کد را ارتقا میدهد.
پیشنهاد مطالعه: API در برنامه نویسی چیست و چه کاربردی دارد؟
اصل جایگزینی در اصول SOLID در برنامه نویسی
اصل جایگزینی لیسکوف (LSP) بیان میکند که اشیای یک کلاس مشتق شده باید بدون تغییر در صحت برنامه، جایگزین اشیای کلاس پایه خود شوند. به عبارت سادهتر، این اصل به این معنی است که کلاسهای مشتق شده باید بتوانند بدون تغییر رفتار مورد انتظار سیستم، کلاسهای پایه خود را جایگزین کنند.
LSP بر اساس ایدههای OCP با تمرکز بر زیرگروه سازی رفتاری است. این کار تضمین میکند که کلاسهای مشتق شده، کلاس پایه را بدون معرفی رفتار غیرمنتظره یا شکستن کد موجود گسترش میدهند. با پیروی از LSP، میتوانید از بروز مشکلات در حین بهروزرسانیها جلوگیری کرده و یکپارچگی نرمافزار را حفظ کنید.
در حالی که درک LSP در ابتدا میتواند چالشبرانگیز باشد، اصول توسعهپذیری و قابلیت نگهداری موجود در سایر اصول SOLID در برنامه نویسی را تقویت میکند. استفاده از LSP در طول توسعه میتواند به پیشبینی و جلوگیری از مسائلی که ممکن است در هنگام جایگزینی اشیاء کلاسهای مشتق شده بهجای همتایان کلاس پایه آنها به وجود آید، کمک کند.
مثالی برای اصل جایگزینی
در اینجا یک مثال در پایتون آورده شده است که اصل جایگزینی در اصول SOLID در برنامه نویسی را نشان میدهد:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side_length):
self.side_length = side_length
def area(self):
return self.side_length ** 2
def calculate_total_area(shapes):
total_area = 0
for shape in shapes:
total_area += shape.area()
return total_area
# Usage
shapes = [Rectangle(4, 5), Square(3)]
total_area = calculate_total_area(shapes)
print(total_area) # Output: 37
در مثال فوق یک کلاس پایه به نام Shape داریم که رفتار مشترک همه اشکال را تعریف میکند. این کلاس دارای یک متد انتزاعی area() است که مساحت یک شکل را محاسبه میکند.سپس دو کلاس مشتق شده داریم، Rectangle و Square، که از Shape به ارث میبرند و متد area() را بر اساس محاسبات شکل خاص خود پیادهسازی میکنند.
نکته کلیدی در اینجا این است که تابعcalault_total_area() قادر است با هر شکلی، خواه مستطیل یا مربع، بدون دانستن نوع خاصی از شکل کار کند. این اصل جایگزینی Liskov را نشان میدهد، زیرا هر کلاس مشتق شده (مستطیل یا مربع) را میتوان بدون تغییر رفتار برنامه جایگزین کلاس پایه آن ایجاد کرد. با پایبندی به اصل جایگزینی لیسکوف به عنوان یکی از مهمترین اصول SOLID در برنامه نویسی، اطمینان حاصل میکنیم که کد قابل توسعه و قابل نگهداری است و از رفتار غیرمنتظره هنگام استفاده از چندشکلی جلوگیری میکند.
اصل جداسازی رابط در اصول SOLID در برنامه نویسی
اصل جداسازی رابط (ISP) نشان میدهد که بهتر است چندین رابط کوچکتر از چند رابط بزرگتر داشته باشیم. این اصل بیان میکند که رابطها باید خاص کلاینت باشند و کلاینتها نباید مجبور شوند متدهایی را که استفاده نمیکنند پیادهسازی کنند. بهجای شروع با یک رابط موجود و اضافه کردن متدهای جدید، توصیه میشود رابطهای جدیدی را متناسب با نیازهای مشتری خاص ایجاد کنید. مهندسان باید با ترکیب و تأکید بر ارثبری و جداسازی بر جفت، طراحی بسیاری از رابطهای خاص دستگاه مشتری را بهجای یک رابط کاربری عمومی واحد طراحی کنند. این رویکرد ماژولار بودن و انعطافپذیری را در پایگاه کد ارتقا میدهد.
مثالی برای اصل جداسازی رابط
در اینجا مثالی برای اصل جداسازی رابط از اصول SOLID در برنامه نویسی در پایتون آورده شده است:
from abc import ABC, abstractmethod
# Bad Design: Large Interface
class Printer(ABC):
@abstractmethod
def print(self, document):
pass
@abstractmethod
def scan(self, document):
pass
@abstractmethod
def fax(self, document):
pass
class SimplePrinter(Printer):
def print(self, document):
print(f"Printing: {document}")
def scan(self, document):
print(f"Scanning: {document}")
def fax(self, document):
# Not supported by SimplePrinter
raise NotImplementedError("Faxing is not supported by this printer.")
# Good Design: Segregated Interfaces
class Printer(ABC):
@abstractmethod
def print(self, document):
pass
class Scanner(ABC):
@abstractmethod
def scan(self, document):
pass
class FaxMachine(ABC):
@abstractmethod
def fax(self, document):
pass
class SimplePrinter(Printer, Scanner):
def print(self, document):
print(f"Printing: {document}")
def scan(self, document):
print(f"Scanning: {document}")
# Usage
document = "Sample Document"
printer = SimplePrinter()
printer.print(document)
printer.scan(document)
در مثال فوق، ما یک رابط چاپگر داریم که در ابتدا با داشتن یک رابط بزرگ که شامل متدهای چاپ (Print)، اسکن (Scan)و فکس(Fax) است، اصل جداسازی رابط را نقض میکند. این میتواند مشکلساز باشد زیرا ممکن است همه چاپگرها از فکس پشتیبانی نکنند و مجبور کردن تمام کلاسهای چاپگر برای اجرای روش فکس غیرضروری است.
برای پایبندی به اصل جداسازی رابط، کد را تغییر میدهیم تا اینترفیسها یا همان رابطها را به رابطهای کوچکتر و ویژه کلاینت تفکیک کنیم. ما رابطهای جداگانهای برای چاپ، اسکن و فکس ایجاد میکنیم. سپس یک کلاس چاپگر SimplePrinter تعریف خواهیم کرد که رابطهای چاپگر و اسکنر را پیادهسازی میکند. فقط نیاز به پیادهسازی متدهای مربوط به چاپ و اسکن دارد و نیازی به اجرای متد فکس پشتیبانی نشده نیست. با جداسازی اینترفیسها، اطمینان حاصل میکنیم که کلاینتها (کلاسها یا ماژولها) فقط باید به رابطهای خاصی که استفاده میکنند وابسته باشند و از اجبار آنها به اجرای متدهای غیرضروری جلوگیری میکنیم. این امر باعث جداسازی، انعطافپذیری و نگهداری بهتر پایگاه کد میشود.
اصل وارونگی وابستگی در اصول SOLID
اصل وارونگی وابستگی (DIP) بر جداسازی ماژولهای نرمافزار تمرکز دارد. این اصل در اصول SOLID در برنامه نویسی بر این مسئله تأکید دارد که ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند. در عوض، هر دو باید به انتزاعات وابستگی ایجاد کنند. در اصل انتزاعها نباید به جزئیات پیادهسازی وابسته باشند و جزئیات باید به انتزاعها وابستگی ایجاد کنند.
یکی از راههای متداول برای پیروی از این اصل، استفاده از الگوهای وارونگی وابستگی است، اگرچه میتوان از روشهای دیگری نیز استفاده کرد. با وابستگی به انتزاعات بهجای اجرای واقعی، کد انعطافپذیرتر، سازگارتر و قابلاستفادهتر میشود.
مثالی برای اصل وارونگی وابستگی
در این بخش مثالی برای اصل وارونگی وابستگی از اصول SOLID آورده شده است:
from abc import ABC, abstractmethod
# High-level Module
class PaymentProcessor:
def __init__(self, payment_gateway):
self.payment_gateway = payment_gateway
def process_payment(self, amount):
self.payment_gateway.pay(amount)
# Low-level Modules
class PayPalGateway:
def pay(self, amount):
print(f"Processing payment of ${amount} via PayPal.")
class StripeGateway:
def pay(self, amount):
print(f"Processing payment of ${amount} via Stripe.")
# Abstraction/Interface
class PaymentGateway(ABC):
@abstractmethod
def pay(self, amount):
pass
# Dependency Injection
paypal_gateway = PayPalGateway()
payment_processor = PaymentProcessor(paypal_gateway)
payment_processor.process_payment(100)
stripe_gateway = StripeGateway()
payment_processor = PaymentProcessor(stripe_gateway)
payment_processor.process_payment(200)
در مثال فوق ما یک ماژول سطح بالا به نام PaymentProcessor داریم که وظیفه پردازش پرداختها را بر عهده دارد. این وابستگی به paypal_gateway دارد، که یک ماژول سطح پایین است که مسئول رسیدگی به پردازش پرداخت واقعی است.
برای پایبندی به اصل وارونگی وابستگی، یک انتزاع یا رابط به نام PaymentGateway معرفی میکنیم. این انتزاع قرارداد درگاههای پرداخت را با مشخص کردن روش پرداخت تعریف میکند. ماژولهای سطح پایین، PayPalGateway و StripeGateway، رابط PaymentGateway را پیادهسازی میکنند و پیادهسازیهای مشخصی را برای روش پرداخت خاص برای هر دروازه پرداخت ارائه میدهند.
کلاس PaymentProcessor بهجای پیادهسازی مشخص به PaymentGateway انتزاعی بستگی دارد. یک نمونه از PaymentGateway را از طریق تزریق وابستگی در سازنده خود میپذیرد. به این ترتیب، ماژول سطح بالا به انتزاعات (رابط PaymentGateway) بهجای پیادهسازیهای خاص (به عنوان مثال، PayPalGateway یا StripeGateway) بستگی دارد.
با پیروی از اصل وارونگی وابستگی در اصول SOLID در برنامه نویسی، ما به اتصال شلی بین ماژولهای سطح بالا و سطح پایین دست مییابیم. ماژول سطح بالا مستقیماً به ماژولهای سطح پایین بستگی ندارد، بلکه به انتزاع بستگی دارد و امکان انعطافپذیری، ماژولار بودن و آزمایش یا تعویض آسانتر پیادهسازیهای مختلف را فراهم میکند.
در مثال فوق، ما نشان دادیم که چگونه PaymentProcessor میتواند با درگاههای پرداخت مختلف (PayPal یا Stripe) با تزریق پیادهسازی دروازه مناسب در طول زمان اجرا کار کند. این وارونگی وابستگیها را نشان میدهد، جایی که ماژولهای سطح بالا به انتزاعها و ماژولهای سطح پایین به همان انتزاعها بستگی دارند.
سخن پایانی
اجرای اصول طراحی SOLID در طول توسعه منجر به سیستمهایی میشود که قابل نگهداری، مقیاسپذیرتر، آزمایشپذیرتر و قابلاستفاده مجدد هستند. این اصول به طور گسترده توسط مهندسان در سراسر جهان برای ایجاد کد با کیفیت بالا که مطابق با استانداردهای صنعت است استفاده میشود. با پایبندی به اصول SOLID در برنامه نویسی، توسعهدهندگان میتوانند اطمینان حاصل کنند که کد آنها به خوبی ساختار یافته، سازگار و قادر به مقاومت در برابر تغییرات و پیشرفتها در طول زمان است.
آموزش برنامه نویسی با مکتب خونه
اگر در برنامه نویسی مبتدی هستید و یا اگر هیچ پایهای از برنامه نویسی ندارید نگران نباشید با مکتب خونه همه چیز آسانتر از آن چیزی است که فکرش را میکنید. در مکتب خونه انواع دوره آموزش برنامه نویسی برای زبان های مختلف برنامه نویسی و همچنین برای ترندهای مختلف برنامه نویسی، انواع دوره آموزش از پایه تا پیشرفته وجود دارد که مفاهیم را از صفر تا صد به کاربران آموزش داده و در عین حال گواهینامههای معتبری را نیز به آنها ارائه میکند. اگر به فکر ورود به دنیای برنامه نویسی هستید و یا میخواهید مهارتهای خود را در زبانهای مختلف برنامه نویسی ارتقا دهید هماکنون میتوانید با آموزش برنامه نویسی مکتب خونه همراه باشید و از نتایج آن شگفتزده شوید.
منبع مقاله:FreeCodeCamp