اصول Solid در برنامه نویسی چیست؟ – توضیح به زبان ساده

۲۶۴۴ بازدید
آخرین به‌روزرسانی: ۲۴ دی ۱۴۰۲
زمان مطالعه: ۱۵ دقیقه
اصول Solid در برنامه نویسی چیست؟ – توضیح به زبان ساده

اصول SOLID در برنامه نویسی توسط آقای «رابرت سی. مارتین» (Robert C. Martin) در مقاله‌ای به نام «اصول طراحی و الگوهای طراحی» (Design Principles and Design Patterns) در سال ۱۳۸۲ شمسی (۲۰۰۰ میلادی) معرفی شدند. این مفاهیم بعدا توسط «مایکل فدرز» (Michael Feathers) گسترش یافتند. آقای فدرز کسی بود که کلمه مخفف «سالید» (SOLID) را ابدع کرد و برای اولین بار استفاده کرد و در طول ۲۰ سال گذشته، این اصول پنج‌گانه از طریق اصلاح راه و روش نوشتن نرم‌افزارها، دنیای برنامه‌نویسی شی‌گرایانه را دچار تغییر و تحول بزرگی کردند. در این مطلب از مجله فرادرس به اصول سالید در برنامه نویسی خواهیم پرداخت و برای هریک با نوشتن نمونه کد مثال‌هایی ارائه می‌دهیم.

اصول Solid در برنامه نویسی چه کاربردی دارند؟

در بیانی ساده، اصول طراحی مارتین و فدرز ما را تشویق می‌کنند تا نرم‌افزارهای انعطاف‌پذیرتر، قابل درک‌تر و با قابلیت نگهداری بیشتری ایجاد کنیم. درنتیجه، همین‌طور که برنامه‌های ما اندازه بزرگتری به خود می‌گیرند، می‌توانیم از پیچیدگی آن‌ها بکاهیم و خودمان را از مشکلات و سردرگمی‌های آینده نجات دهیم.

کلمه مخفف SOLID

زمانی که نوبت به نوشتن کلاس‌ها و طراحی تعاملات بین آن‌ها می‌شود، می‌توانید از مجموعه اصولی پیروی کنید که به ایجاد بهتر کدهای شی‌گرایانه، کمک می‌کنند. یکی از مجموعه استاندارهای بسیار پرطرفدار و مقبول برنامه‌نویسان برای طراحی برنامه‌های شی‌گرایانه، با عنوان اصول سالید در برنامه نویسی شناخته می‌شود. کلمه SOLID، که اشاره به اصول ۵ گانه‌ای در برنامه نویسی شی‌گرایانه دارد، از ترکیب اولین حروف این اصول ساخته شده‌است. در ادامه این اصول را نام برده‌ایم.

  1. اصل تک‌مسئولیتی (Single Responsibility Principle | SRP)
  2. اصل باز - بسته (Open/Closed Principle | OCP)
  3. اصل جایگزینی لیسکوف (Liskov Substitution Principle | LSP)
  4. اصل جداسازی اینترفیس‌ها (Interface Segregation Principle | ISP)
  5. اصل وارونگی وابستگی (Dependency Inversion Principle | DIP)

درحالی که این قواعد در نگاه اول می‌توانند پیچیده و دشوار به نظر برسند، با کمک چند مثال کدنویسی ساده، قابل درک می‌شوند. برای نوشتن کدهای نمونه از زبان پایتون استفاده خواهیم کرد اما این اصول برای انواع زبان‌های برنامه‌نویسی که از شی‌گرایی پشتیبانی می‌کنند، صدق می‌کنند.

سه معکب با حروف کلمه SOILD روی وجه ها

کاربرد اصول سالید

وقتی که در حال اجرای پروژه‌ای با روش «برنامه‌نویسی شی‌گرایانه» (Object-Oriented Programming | OOP) هستید، طراحی اینکه چگونه اشیا و کلاس‌ها باید باهم تعامل داشته باشند تا مسئله مورد نظر را بتوانند حل کنند، قسمت مهمی از کار برنامه‌نویس است. این عمل طراحی به نام «طراحی شی‌گرایانه» (Object-Oriented Design | OOD) شناخته می‌شود. انجام صحیح این طراحی می‌تواند به چالشی برای توسعه‌دهندگان نرم‌افزار تبدیل شود. اگر در زمان طراحی کلاس‌های خود دچار مشکل شدید، اصول SOLID در برنامه نویسی می‌توانند در حل مشکل به شما کمک کنند.

اگر قبلا با ++C یا Java کدنویسی کرده باشید و با تکنیک شی‌گرایی در این نوع از زبان‌ها کار کرده باشید، احتمالا از قبل با این اصول آشنا شده‌اید و احتمالا تعجب خواهید کرد اگر بدانید که اصول SOLID را روی کدهای پایتون نیز می‌توان اعمال کرد ولی در واقع نه تنها این اصول را می‌توان در زبان پایتون نیز استفاده کرد بلکه اگر درحال نوشتن برنامه‌نویسی شی‌گرایانه هستید باید اعمال این اصول را روی طراحی کلاس‌ها و اینترفیس‌ها در نظر داشته باشید.

در این مطلب از کدهای پایتون استفاده خوهیم کرد که با روش اعمال این اصول در زبان پایتون نیز آشنا شوید. برای اینکه بتوانید از این مقاله بیشترین بهره را ببرید، باید درک خوبی از مفاهیم «برنامه‌نویسی شی‌گرایی» (Object-Oriented Programming) پایتون مانند کلاس‌ها، «رابط» (Interface) یا اینترفیس و «وراثت» (Inheritance) داشته باشید.

توجه کنید که این اصول، مفاهیم و استاندارهای کلی هستند و فارغ از نوع زبانی که کار می‌کنید، مدل چیدمان و آرایش کدهای شی‌گرایانهی شما را به نحو بهینه و موثری طراحی می‌کنند. پس فارغ از زبان برنامه نویسی خود با خیال راحت بر روی درک مطلب اصول Solid در برنامه نویسی، تمرکز کنید. در ادامه به اولین اصل از اصول سالید در برنامه نویسی خواهیم پرداخت.

اصل تک‌مسئولیتی یا SRP

اولین مورد از اصول Solid در برنامه نویسی «اصل تک‌مسئولیتی» (Single Responsibility Principle) است. اصل تک‌مسئولیتی بیان می‌کند که هر کلاس باید فقط یک دلیل برای تغییر داشته باشد. این اصل به این معنی است که هر کلاس باید فقط یک مسئولیت داشته باشد که توسط متدهای آن بیان شده باشد. اگر کلاسی به بیش از یک وظیفه بپردازد، باید آن وظایف را توسط کلاس‌های جداگانه‌ای از هم جدا کنید.

دختر برنامه نویس با توجه به اصول solid در برنامه نویسی کار میکند و خوشحال است.

نکته: شاید عبارت‌های مربوط به اصول SOLID را خارج از این مقاله به اشکال گوناگونی پیدا کنید. در این مطلب عبارت‌های مربوطه به شکلی بیان شده است که مارتین در کتاب خودش به نام - «توسعه نرم‌افزار چابک» (Agile Software Development) بیان کرده‌ است.

نمونه کد برای اصل تک‌مسئولیتی

این اصل ارتباط نزدیکی با مفهوم «تفکیک مسئولیت‌ها» (Separation of Concerns) دارد که اشاره به این نکته دارد، باید برنامه را به تکه‌های مختلفی تقسیم کرد. هر تکه باید به مسئولیت جداگانه‌ای اشاره کند. برای اینکه اصل تک‌مسئولیتی را نمایش دهیم و نشان دهیم چگونه می‌تواند طراحی شی‌گرایانه شما را ارتقا دهد، فرض کنید قصد ایجاد کلاس FileManager  دارید که در کد زیر نشان داده‌ایم.

1# file_manager_srp.py
2
3from pathlib import Path
4from zipfile import ZipFile
5
6class FileManager:
7    def __init__(self, filename):
8        self.path = Path(filename)
9
10    def read(self, encoding="utf-8"):
11        return self.path.read_text(encoding)
12
13    def write(self, data, encoding="utf-8"):
14        self.path.write_text(data, encoding)
15
16    def compress(self):
17        with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:
18            archive.write(self.path)
19
20    def decompress(self):
21        with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:
22            archive.extractall()

در این مثال، کلاس FileManager  دو مسئولیت مختلف را دارد. از متدهای .read()  و .write()  برای مدیریت فایل‌ها استفاده می‌کند. همچنین به وسیله فراهم کردن متدهای .compress()  و .decompress()  با فایل‌های بایگانی‌ ZIP نیز سروکار دارد.

این کلاس اصل تک‌مسئولیتی را نقض می‌کند زیرا دو نوع مسئولیت کاملا متفاوت را یک کلاس بر عهده گرفته است، اولی خواندن و نوشتن فایل‌ها و دومی فشرده‌سازی و غیرفشرده‌سازی فایل‌ها است. برای اینکه این مشکلات حل شوند و طراحی‌ کلاس‌ها پایدارتر شود، می‌توانید کلاس را به دو کلاس کوچکتر و متمرکزتر تقسیم کنید که هرکدام مسئولیت‌های خاص خود را داشته باشند.

1# file_manager_srp.py
2
3from pathlib import Path
4from zipfile import ZipFile
5
6class FileManager:
7    def __init__(self, filename):
8        self.path = Path(filename)
9
10    def read(self, encoding="utf-8"):
11        return self.path.read_text(encoding)
12
13    def write(self, data, encoding="utf-8"):
14        self.path.write_text(data, encoding)
15
16class ZipFileManager:
17    def __init__(self, filename):
18        self.path = Path(filename)
19
20    def compress(self):
21        with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:
22            archive.write(self.path)
23
24    def decompress(self):
25        with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:
26            archive.extractall()

حالا ۲ کلاس کوچکتر دارید که هرکدام یک نوع مسئولیت را برعهده دارند. کلاس FileManager  به مدیریت فایل‌ها توجه می‌کند درحالی که کلاس ZipFileManager  به فشرده‌سازی و استخراج فایل‌های فشرده شده در فرمت ZIP می‌پردازد. این دو کلاس کوچک‌تر هستند و بنابراین، بیشتر قابل مدیریت‌اند. همچنین برای فهمیدن، بررسی کردن و اشکال زدایی در صورت بروز خطا، کار آسانتری در پیش دارید.

یک برنامه نویس تنها با رعایت اصل تک مسئولیتی درحال کار کردن است.

توجه به مسئولیت‌پذیری در این مفهوم کاملا ذهنی و انتزاعی است در واقع هر کلاسی دقیقا کاری را که فقط برعهده اوست می‌کند و فقط روی مسئولیت خود تمرکز دارد. داشتن مسئولیت‌پذیری یگانه و جدا از دیگران، الزاما به معنی داشتن متدی یگانه نیست. مسئولیت‌پذیری مستقیما به تعداد متدها گره نخورده است بلکه به ظیفه اصلی که کلاس شما برعهده گرفته، مربوط می‌شود. با توجه به اینکه توقع دارید کلاس نماینده چه رفتاری در کد باشد، این وظیفه تعریف می‌شود. این موجودیت مستقل و این اندازه تفکیک مسئولیت نباید مانع از این شود که از اصل تک‌مسئولیتی (SRP) پیروی کنید.

اصل باز-بسته یا OCP

دومین مورد از اصول Solid در برنامه نویسی «اصل باز-بسته» (Open-Closed Principle) بودن است که مربوط به طراحی شی‌گرایانه است و در ابتدا توسط آقای پروفسور «برتراند مایر» (Bertrand Meyer) در سال ۱۳۶۷ شمسی (۱۹۸۸ میلادی) معرفی شد.

اصل باز-بسته بیان می‌کند که نهادهای نرم‌افزاری مانند کلاس‌ها، ماژول‌ها، توابع و غیره، باید برای گسترش باز و برای تغییر بسته باشند. یعنی نرم‌افزارها باید مانند جعبه‌هایی باشند که می‌توان درون آن‌ها وسیله گذاشت اما امکان خارج کردن وسیله از آن‌ها وجود ندارد. هر قدر که نیاز باشد می‌توان به نهادهای نرم افزاری متد و تابع اضافه کرد اما امکان حذف متدها یا تغییر دادنشان وجود ندارد.

نمونه کد برای اصل باز-بسته

برای اینکه بیشتر و بهتر درک کنیم که اصل باز-بسته از اصول Solid در برنامه نویسی چه می‌گوید، به کد زیر و کلاس Shape  توجه کنید.

1# shapes_ocp.py
2
3from math import pi
4
5class Shape:
6    def __init__(self, shape_type, **kwargs):
7        self.shape_type = shape_type
8        if self.shape_type == "rectangle":
9            self.width = kwargs["width"]
10            self.height = kwargs["height"]
11        elif self.shape_type == "circle":
12            self.radius = kwargs["radius"]
13
14    def calculate_area(self):
15        if self.shape_type == "rectangle":
16            return self.width * self.height
17        elif self.shape_type == "circle":
18            return pi * self.radius**2

متد سازنده __init__()  در کلاس Shape آرگومانی به اسم shape_type  را می‌گیرد که می‌تواند مستطیل یا دایره باشد. همچنین مجموعه‌ای از «آرگومان‌های کلمه‌کلیدی» (Keyword Arguments) خاص را با استفاده از عبارت **kwargs  می‌گیرد. اگر نوع شکل را روی مستطیل تنظیم کنید، باید آرگومان‌های کلمه کلیدی height  و width   (همان طول و عرض) را هم به شکل بدهید که درنتیجه می‌توانید یک مستطیل تمام و کمال ایجاد کنید.

در عوض، اگر نوع شکل را روی دایره تنظیم کنید، باید آرگومان‌ کلمه کلیدی radius   (شعاع) را هم به شکل بدهید که درنتیجه می‌توانید دایره‌ای ایجاد کنید. توجه کنید که این مثال ممکن است کمی پیچیده باشد اما هدف اصلی این است که به وضوح برای شما معنی و منظور اصل باز-بسته شرح داده شود تا به‌خوبی مطلب را درک کنید. کلاس Shape  متدی به نام .calculate_area()  نیز دارد که مساحت شکل موجود را با توجه به .shape_type  محاسبه می‌کند.

1>>> from shapes_ocp import Shape
2
3>>> rectangle = Shape("rectangle", width=10, height=5)
4>>> rectangle.calculate_area()
550
6>>> circle = Shape("circle", radius=5)
7>>> circle.calculate_area()
878.53981633974483

می‌بینید که کلاس کار می‌کند. می‌توانید دایره‌ها و مستطیل‌هایی را ایجاد کنید، مساحت آن‌ها را محاسبه کنید و این روند را ادامه دهید. اگرچه کلاس کاملا بد به‌نظر می‌رسد. همان اول کار به‌نظر می‌رسد که جایی مشکل داریم. تصور کنید که نیاز به اضافه کردن شکل جدیدی داریم، مثلا مربع. چگونه این کار را انجام خواهید داد. خوب یکی از گزینه‌های موجود اینجا این است که بند دیگری به‌صورت elif  به .__init__()  و .calculate_area()  اضافه کنیم، درنتیجه می‌توانید نیازمندی‌های شکل مربع را نشان دهید و برآورده کنید.

اینکه مجبور باشید برای ایجاد اشکال جدید، چنین تغییراتی را اعمال کنید، به این معناست که کلاس برای اعمال تغییرات باز است. باز بودن کلاس برای اعمال تغییرات اصل باز-بسته را نقض می‌کند. چگونه می‌توانید کلاس را طوری اصلاح کنید که برای گسترش باز باشد اما برای تغییرات بسته بماند. راه حل ممکن را در ادامه آورده‌ایم.

1# shapes_ocp.py
2
3from abc import ABC, abstractmethod
4from math import pi
5
6class Shape(ABC):
7    def __init__(self, shape_type):
8        self.shape_type = shape_type
9
10    @abstractmethod
11    def calculate_area(self):
12        pass
13
14class Circle(Shape):
15    def __init__(self, radius):
16        super().__init__("circle")
17        self.radius = radius
18
19    def calculate_area(self):
20        return pi * self.radius**2
21
22class Rectangle(Shape):
23    def __init__(self, width, height):
24        super().__init__("rectangle")
25        self.width = width
26        self.height = height
27
28    def calculate_area(self):
29        return self.width * self.height
30
31class Square(Shape):
32    def __init__(self, side):
33        super().__init__("square")
34        self.side = side
35
36    def calculate_area(self):
37        return self.side**2

در کد بالا، به‌طور کامل کلاس Shape  بازنویسی شده و به «کلاس انتزاعی» (Abstract Base Class | ABC) تبدیل شده‌است. این کلاس برای هر شکلی که بخواهید تعریف کنید، اینترفیس API مورد نیاز را فراهم می‌کند. این اینترفیس شامل ویژگی .shape_type  و متد .calculate_area()  می‌شود که باید در همه زیرکلاس‌ها «بازنویسی» (Override) کنید.

یک لپ تاپ که چندین مکعب را نمایش می دهد

تعریف وراثت رابطه ای

مثال بالا و بعضی از مثال‌هایی که در ادامه مطلب خواهند آمد بر پایه و اساس کلاس‌های انتزاعی پایتون، ABC خواهند بود تا وضعیتی را به نام «وراثت رابطه‌ای» (Interface Inheritance) بوجود بیاورند. در این نوع از وراثت، زیرکلاس‌ها روابط را بجای عملکردها به ارث می‌برند، در واقع در وراثت انتزاعی در کلاس انتزاعی فقط از متدهایی که باید وجود داشته باشند نام‌برده می‌شود و هیچ رفتاری برای آن متدها تعریف نمی‌شود. کلاس‌های فرزند موظف به داشتن آن متدها هستند و هرکدام فراخور نیازشان برای متد به ارث رسیده، رفتار تعریف می‌کنند. در مقابل وقتی کلاس‌ها قابلیت‌ها را به ارث ببرند، شما با وراثت اجرایی روبه‌رو می‌شوید. یعنی تمام متدها با رفتار تعیین شده در کلاس والد به ارث برده می‌شوند.

این به‌روزرسانی راه را برای لزوم اعمال تغییرات در کلاس می‌بندد. اکنون به سادگی می‌توانید اشکال جدیدی به طراحی کلاس اضافه کنید بدون اینکه نیاز باشد کلاس Shape  را تغییر بدهید. در موارد زیادی شما مجبور خواهید بود که اینترفیس مورد نیاز را پیاده‌سازی کنید، که باعث می‌شود کلاس‌های شما «چندریختی» (Polymorphic) شوند. درک این مطلب برای درک هرچه بهتر اصول Solid در برنامه نویسی لازم است.

اصل جایگزینی لیسکوف یا LSP

سومین مورد از اصول Solid در برنامه نویسی «اصل جایگزینی لیسکوف» (Liskov Substitution Principle) است که توسط خانم دکتر «باربارا لیسکوف» (Barbara Liskov) در کنفرانس OOPSLA به سال ۱۳۶۶ شمسی(۱۹۸۷ میلادی) مطرح شد. از آن زمان به بعد، این اصل قسمتی اساسی از برنامه‌نویسی شی‌گرایانه بوده است. اصل جایگزینی لیسکوف بیان می‌کند که زیرگونه‌ها باید بتوانند قابل جایگزین شدن با نوع اصلی خود باشند.

به عنوان مثال، اگر قطعه کدی دارید که با کلاس Shape  کار می‌کند، مانند دایره Circle  یا مستطیل Rectangle  ، باید بتوانید آن کلاس را بدون خراب کردن کد، با هر کدام از زیر کلاس‌هایش تعویض کنید. در عمل، این اصل درباره این است که کلاس‌های فرزند را طوری ایجاد کنید، زمانی که متدهای یکسان بین کلاس والد و فرزند فراخوانی می‌شوند، همانند کلاس‌های والد خود رفتار کنند. بدون اینکه رفتارهایی که از کلاس والد سرزده می‌شود را دستکاری کرده باشند.

نمونه کد برای اصل جایگزینی لیسکوف

مثال‌های خود را با نمونه‌های مربوط به شکل ادامه می‌دهیم. فرض کنید کلاس مستطیلی Rectangle   دارید، مانند نمونه کدی که در ادامه آورده‌ایم.

1# shapes_lsp.py
2
3class Rectangle:
4    def __init__(self, width, height):
5        self.width = width
6        self.height = height
7
8    def calculate_area(self):
9        return self.width * self.height

متد .calculate_area()  را در کلاس Rectangle  فراهم کرده‌اید که با ویژگی‌های تعریف شده .height  و .width  عمل می‌کند.

به این دلیل که مربع نمونه‌ای خاص از مستطیل با اضلاع برابر است و برای این که دوباره از کد استفاده کنید به این فکر می‌کنید که برای تعریف کلاس مربع Square  از کلاس مستطیل Rectangle  استفاده کنید. متد «مقدار دهنده» (Setter) را برای ویژگی‌های .width  و .height  بازنویسی می‌کنید. به این صورت که وقتی ضلعی تغییر می‌کند، ضلع مجاور هم تغییر کند.

1# shapes_lsp.py
2
3# ...
4
5class Square(Rectangle):
6    def __init__(self, side):
7        super().__init__(side, side)
8
9    def __setattr__(self, key, value):
10        super().__setattr__(key, value)
11        if key in ("width", "height"):
12            self.__dict__["width"] = value
13            self.__dict__["height"] = value

در این تکه کد، کلاس Square  را به عنوان زیرکلاسی از Rectangle  تعریف کرده‌ایم. همان‌طور که کاربر انتظار دارد، سازنده کلاس، فقط ضلع مربع را به عنوان آرگومان می‌پذیرد. به‌طور ناخودآگاه، متد .__init__()  ویژگی‌های .width  و .height  والد را با آرگومان side  مقدار دهی اولیه می‌کند.

به‌علاوه متد ویژه‌ای به نام .__setattr__()  تعریف کرده‌ایم تا مکانیزم مقداردهی پایتون را دستکاری کنیم و از اتصال مقدار جدید به .height  و .width  بصورت جداگانه جلوگیری کنیم. بخصوص زمانی که یکی از آن ویژگی‌ها را تنظیم می‌کنیم، مقدار ویژگی دیگر هم به همان مقدار تنظیم می‌شود.

1>>> from shapes_lsp import Square
2
3>>> square = Square(5)
4>>> vars(square)
5{'width': 5, 'height': 5}
6
7>>> square.width = 7
8>>> vars(square)
9{'width': 7, 'height': 7}
10
11>>> square.height = 9
12>>> vars(square)
13{'width': 9, 'height': 9}

اکنون که مطمئن شده‌اید که شی Square  همیشه به‌صورت مربعی با طول و عرض استاندارد، خواهد بود، با یک ذره مصرف حافظه بیشتر کارها را برای خود آسانتر کرده‌اید اما متاسفانه این روش اصل جایگزینی لیسکوف را نقض می‌کند که یکی از مهمترین اصول SOLID در برنامه نویسی است. زیرا نمی‌توانید نمونه‌های Rectangle  را با نمونه‌های قرینه Square  جفت آن‌ها جایگزین کنید.

وقتی کسی در کد خود انتظار مستطیلی را دارد، ممکن است تصور کند، کد مانند شیی عمل خواهد کرد که دو ویژگی عرض .width  و ارتفاع .height  را بصورت مستقل از هم ارائه خواهد کرد. در این حین، کلاس Square  ، با تغییر دادن رفتاری که توسط اینترفیس شی تعریف شده‌است، آن تصور را برهم می‌ریزد و فقط با گرفتن یک ضلع مساحت را محاسبه می‌کند و شکل را ایجاد می‌کند. این اتفاق باعث می‌شود نتایج ناخواسته و غیر منتظره‌ای به وجود آیند که احتمالا خطایابی مشکلی هم خواهند داشت.

از آنجا که مربع در ریاضیات به عنوان نوع خاصی از مستطیل تعریف می‌شود، درصورتی که بخواهید کدهای شما از اصل جایگزینی لیسکوف پیروی کنند، کلاس‌هایی که آن اشکال را نمایش می‌دهند نباید در رابطه والد و فرزندی قرار بگیرند. یکی از راه‌های حل این مشکل ایجاد یک کلاس پایه برای هر دو کلاس Square  و Rectangle  است که قابل گسترش با‌شد.

1# shapes_lsp.py
2
3from abc import ABC, abstractmethod
4
5class Shape(ABC):
6    @abstractmethod
7    def calculate_area(self):
8        pass
9
10class Rectangle(Shape):
11    def __init__(self, width, height):
12        self.width = width
13        self.height = height
14
15    def calculate_area(self):
16        return self.width * self.height
17
18class Square(Shape):
19    def __init__(self, side):
20        self.side = side
21
22    def calculate_area(self):
23        return self.side ** 2

کلاس Shape  به کلاسی تبدیل می‌شود که به روش «چندریختی» (Polymorphism) می‌توان آن را با Square  و Rectangle  جاگزین کرد که الان بجای اینکه باهم رابطه والد و فرزندی داشته باشند، رابطه‌ای از نوع خواهربرادری دارند. توجه کنید که این دو نوع شکل عینی مجموعه ویژگی‌های مشخص و متدهای سازنده متفاوت دارند، حتی ممکن است رفتارهای جداگانه مختلفی نیز داشته باشند. تنها وجه اشتراک آن‌ها توانایی محاسبه مساحت است.

با این پیاده سازی صحیح، زمانی که می‌خواهید رفتارهای مشترکشان را به‌کار ببرید، می‌توانید از کلاس نوع Shape  بجای زیر نوع‌های خودش، Rectangle  و Square  نیز استفاده کنید.

1from shapes_lsp import Rectangle, Square
2
3def get_total_area(shapes):
4    return sum(shape.calculate_area() for shape in shapes)
5
6get_total_area([Rectangle(10, 5), Square(5)])

تا اینجای کار، جفتی شامل مربع و مستطیل برای محاسبه مساحت به صورت .calculate_area()   پیاده‌سازی کرده‌اید. چون تابع فقط به محاسبه مساحت توجه می‌کند برای آن اهمیتی ندارد که شکل مستطیل است یا مربع. مفهوم این مثال، درون مایه اصل جایگزینی لیسکوف است.

اصل جداسازی اینترفیس ها یا ISP

«اصل جداسازی اینترفیس‌ها» (Interface Segregation Principle) چهارمین اصل از اصول SOLID در برنامه نویسی است و از همان ایده اصل تک‌مسئولیتی گرفته شده‌است.

اصل جداسازی اینترفیس‌ها بیان می‌کند که کاربرها نباید مجبور به وابسته بودن به متدهایی باشند که از آن‌‌ها استفاده نمی‌کنند. اینترفیس‌ها متعلق به کاربران است نه به سلسله مراتب. در مورد تعریف بالا، کاربران، کلاس‌ها و زیر کلاس‌ها هستند و روابط شامل ویژگی‌ها و صفت‌ها می‌شوند. به عبارت دیگر، اگر کلاسی از متدها و ویژگی‌های به‌خصوصی استفاده نمی‌کند، پس آن متدها و ویژگی‌ها را باید در کلاس‌های اختصاصی‌تری جداسازی کرد.

صحنه‌ای که نشان‌دهنده ادغام آرام بین انسان‌ها و کد کامپیوتری است، نمادی از هماهنگی مابین برنامه‌نویسان و ماشین‌ها.

نمونه کد برای اصل جداسازی اینترفیس ها

در ادامه این مطلب از مجله فرادرس برای درک بهتر و بیشتر اصل چهارم سالید، سلسله مراتب کلاسی را برای مدل‌سازی چاپگرها آورده‌ایم.

1# printers_isp.py
2
3from abc import ABC, abstractmethod
4
5class Printer(ABC):
6    @abstractmethod
7    def print(self, document):
8        pass
9
10    @abstractmethod
11    def fax(self, document):
12        pass
13
14    @abstractmethod
15    def scan(self, document):
16        pass
17
18class OldPrinter(Printer):
19    def print(self, document):
20        print(f"Printing {document} in black and white...")
21
22    def fax(self, document):
23        raise NotImplementedError("Fax functionality not supported")
24
25    def scan(self, document):
26        raise NotImplementedError("Scan functionality not supported")
27
28class ModernPrinter(Printer):
29    def print(self, document):
30        print(f"Printing {document} in color...")
31
32    def fax(self, document):
33        print(f"Faxing {document}...")
34
35    def scan(self, document):
36        print(f"Scanning {document}...")

در این نمونه، کلاس پایه Printer  اینترفیسی را که زیرکلاس‌هایش باید پیاده‌سازی کنند فراهم کرده‌است. کلاس OldPrinter  از Printer  ارث می‌برد و باید همان اینترفیس را پیاده‌سازی کند. اگرچه، چاپگر قدیمی از متدهای .scan()  و .fax()  استفاده نمی‌کند، زیرا این نوع از مدل‌های قدیمی چاپگر اصلا این قابلیت‌ها را پشتیبانی نمی‌کنند.

این پیاده‌سازی اصل جداسازی اینترفیس‌ها را نقض می‌کند زیرا کلاس OldPrinter  را مجبور به در معرض نمایش گذاشتن اینترفیسی می‌کند که کلاس اجرا نمی‌کند یا به آن احتیاج نخواهد داشت. برای اینکه این مشکل حل شود، باید اینترفیس‌ها را به کلاس‌های کوچکتر و اختصاصی‌تری تقسیم کنید. در نتیجه می‌توانید کلاس‌های ملموس‌تری را درصورت نیاز حتی با ارث‌بری از چندین کلاس اینترفیس، ایجاد کنید.

1# printers_isp.py
2
3from abc import ABC, abstractmethod
4
5class Printer(ABC):
6    @abstractmethod
7    def print(self, document):
8        pass
9
10class Fax(ABC):
11    @abstractmethod
12    def fax(self, document):
13        pass
14
15class Scanner(ABC):
16    @abstractmethod
17    def scan(self, document):
18        pass
19
20class OldPrinter(Printer):
21    def print(self, document):
22        print(f"Printing {document} in black and white...")
23
24class NewPrinter(Printer, Fax, Scanner):
25    def print(self, document):
26        print(f"Printing {document} in color...")
27
28    def fax(self, document):
29        print(f"Faxing {document}...")
30
31    def scan(self, document):
32        print(f"Scanning {document}...")

حالا، Printer  ، Fax  و Scanner  کلاس‌های پایه‌ای هستند که هرکدام اینترفیس‌های خاصی را با یک مسئولیت خاص  ارائه می‌دهند. برای ایجاد کلاس OldPrinter  فقط از اینترفیس Printer  ارث‌بری خواهید کرد. با این روش، کلاس‌ها متدهای بی‌استفاده نخواهند داشت. برای ایجاد کلاس ModernPrinter  ، نیاز دارید که از همه اینترفیس‌ها ارث ببرید. بطور خلاصه، اینترفیس Printer  را به اینترفیس‌های تک مسئولیت و کوچک‌تری تجزیه کرده‌اید. با کمک این روش طراحی کلاس، این امکان را خواهید داشت که دستگاه‌های مختلفی با مجموعه قابلیت‌های مختلفی ایجاد کنید و طراحی کلاس‌های خود را بسیار انعطاف‌پذیر و توسعه‌پذیرتر کنید.

اصل وارونگی وابستگی یا DIP

پنجمین و آخرین اصل از اصول SOLID در برنامه نویسی، «اصل وارونگی وابستگی» (Dependency Inversion Principle) است که به جداسازی ماژول‌های نرم‌افزاری اشاره دارد. اصل وارونگی وابستگی بیان می‌کند که موارد انتزاعی نباید به جزییات وابسته باشند بلکه جزییات باید به موارد انتزاعی وابسته باشند.

‎نمونه کد برای اصل وارونگی وابستگی

فرض کنید که درحال ساخت برنامه‌ای کاربردی هستید و کلاسی به نام FrontEnd  دارید که داده‌ها را به صورت کاملا کاربر پسندی به کاربرها نمایش می‌دهد. اپلیکیشن به صورت کاملا صحیح داده‌ها را از دیتابیس می‌گیرد در نتیجه به آسانی با کدی که در ادامه می‌آید به جواب خواهید رسید.

1# app_dip.py
2
3class FrontEnd:
4    def __init__(self, back_end):
5        self.back_end = back_end
6
7    def display_data(self):
8        data = self.back_end.get_data_from_database()
9        print("Display data:", data)
10
11class BackEnd:
12    def get_data_from_database(self):
13        return "Data from the database"

در این مثال، کلاس FrontEnd  به کلاس BackEnd  و پیاده‌سازی ملموس آن وابسته است. منظور از پیاده‌سازی ملموس این است، پیاده‌سازی کلاس به گونه‌ای است که کارکرد متدهای داخل کلاس بصورت روشن و واضح مشخص است و می‌دانیم که چه ورودی دارند و چه خروجی خواهند داشت. می‌توان گفت که این دو کلاس بهه شدت به‌هم مرتبط هستند. این به‌هم‌پیوستگی می‌تواند کل پروژه را به سمت مشکلات مربوط به مقیاس‌پذیری سوق دهد. برای نمونه، فرض کنید که اپلیکیشن به سرعت درحال رشد است، و می‌خواهید که اپلیکیشن شما بتواند داده‌ها را از REST API نیز بخواند. چطور این کار را خواهید کرد.

شاید بخواهید برای گرفتن داده‌ها از REST API متد جدیدی به BackEnd   اضافه کنید. اگرچه، این کار هم نیازمند این است که FrontEnd  را تغییر دهید، درحالی که بنا بر اصل باز-بسته باید کلاس‌ها نسبت به تغییر بسته بمانند. برای حل این مشکل، می‌توانید اصل وارونگی وابستگی را به‌کار ببرید و کلاس‌های خود را بجای اینکه به پیاده‌سازی‌های ملموسی مانند BackEnd  وابسته‌ باشند، به کلاس‌های انتزاعی وابسته کنید. در این مثال خاص، می‌توانید کلاس DataSource  را معرفی کنید که اینترفیسی برای استفاده در کلاس‌های کاربری‌تان فراهم می‌کند.

1# app_dip.py
2
3from abc import ABC, abstractmethod
4
5class FrontEnd:
6    def __init__(self, data_source):
7        self.data_source = data_source
8
9    def display_data(self):
10        data = self.data_source.get_data()
11        print("Display data:", data)
12
13class DataSource(ABC):
14    @abstractmethod
15    def get_data(self):
16        pass
17
18class Database(DataSource):
19    def get_data(self):
20        return "Data from the database"
21
22class API(DataSource):
23    def get_data(self):
24        return "Data from the API"

در این بازطراحی از پروژه، کلاس DataSource  به‌عنوان کلاس انتزاعی که اینترفیس‌های مورد نیاز یا متد .get_data()  را تعریف می‌کند به کدها افزوده شده‌ است. توجه داشته باشید که اکنون چگونه کلاس FrontEnd  به اینترفیسی وابسته شده که توسط کلاس DataSource  ارائه شده است و خود این کلاس DataSource  یک کلاس انتزاعی است.

یک مرد جوان نشسته روی مکعب ها در حال کار با بپ تاپ

سپس کلاس Database را تعریف می‌کنید، که پیاده‌سازی ملموسی برای مواقعی است که می‌خواهید داده‌ها را از پایگاه‌داده بدست بیاورید. این کلاس به کلاس انتزاعی DataSource  از طریق وراثت وابسته است. درنهایت، کلاس API را برای پشتیبانی سیستم دریافت اطلاعات از طریق REST API تعریف می‌کنید. البته که این کلاس نیز به کلاس انتزاعی DataSource  وابسته است.

در کد زیر روش استفاده از کلاس FrontEnd را در کدهایتان خواهید دید.

1from app_dip import API, Database, FrontEnd
2
3db_front_end = FrontEnd(Database())
4db_front_end.display_data()
5
6
7api_front_end = FrontEnd(API())
8api_front_end.display_data()

اینجا، در ابتدای کار کلاس FrontEnd  را با استفاده از شی از کلاس Database  مقداردهی می‌کنید و بعدا دوبار همین عملیات را با وسیله شی از کلاس API  تکرار می‌کنید. هر زمان که تابع .display_data()  فراخوانی شود، نتیجه وابسته به منبع داده عینی خواهد بود که شما استفاده می‌کنید. توجه کنید که همچنین می‌توانید منبع داده را به‌صورت پویا (دینامیک) تغییر دهید. برای این کار لازم است ویژگی .data_source  را در نمونه FrontEnd  کد خود بازنویسی کنید.

سوالات متداول

در ادامه به بررسی برخی از سوالاتی پرداخته‌ایم که برای کاربران در ابتدای آشنایی با اصول سالید در برنامه نویسی مطرح می‌شود.

سالید مخفف چه کلمه ای است؟

سالید نوشتار فارسی از کلمه SOLID است. این کلمه، سرنام حروف اول از اصول پنج‌گانه‌ای است که برای افزایش راندمان برنامه نویسی شی گرایانه معرفی شده‌اند. این اصول به ترتیب اصل تک‌مسئولیتی (Single Responsibility Principle)، اصل باز - بسته (Open/Closed Principle)، اصل جایگزینی لیسکوف (Liskov Substitution Principle)، اصل جداسازی اینترفیس‌ها (Interface Segregation Principle) و اصل وارونگی وابستگی (Dependency Inversion Principle) است.

اصول SOLID در برنامه نویسی چیست؟

اصول SOLID در برنامه نویسی، به اصول ۵ گانه‌ای می‌گویند که برای افزایش راندمان، مقیاس‌پذیری، قدرت آزمایش و خطایابی و افزایش قابلیت نگهداری کدها تعریف شده‌اند. این اصول مخصوص برنامه‌نویسی شی‌گرایانه تعریف و تدوین شده و در تمام زبان‌هایی که از شی‌گرایی پشتیبانی می‌کنند قابل پیاده‌سازی هستند.

آیا اصول سالید فقط در پایتون رعایت می شوند؟

خیر، اصول سالید برای پایتون نیز استفاده می‌شود، اما این اصول برای تکنیک برنامه‌نویسی شی‌گرایانه تدوین شده‌اند و در هر زبانی که از شی‌گرایی پشتیبانی کند قابل پیاده‌سازی هستند و کیفیت نرم‌افزارها را افزایش می‌دهند.

جمع بندی

در این مطلب از مجله فرادرس، سعی کردیم درباره اصول SOLID در برنامه نویسی، نکات مهمی را به صورت قابل درک بیان کنیم. مثال‌های مختلفی در رابطه با هر اصل بررسی کردیم و متوجه شدید که به‌کار بردن اصول SOLID در برنامه نویسی برای افزایش کیفیت طراحی شی گرایانه مفید است.

در این مقاله تلاش کردیم تا موارد زیر را آموزش بدهیم.

  • معنی و منظور هرکدام از اصول SOLID در برنامه نویسی
  • شناسایی طراحی‌های کلاسی که بعضی از اصول SOLID را در پایتون نقض می‌کند.
  • استفاده از اصول SOLID در برنامه‌نویسی برای کمک به بازسازی کدهای پایتون و بهبود «طراحی شی‌گرایانه» ( Object-Oriented Design | OOD)

به کمک اطلاعات کافی از اصول SOLID در برنامه نویسی، شناخت بسیار خوبی از بهترین روش‌های شناخته شده‌ای بدست می‌آید که باید درهنگام طراحی شی‌گرایانه به‌کار ببرید. با استفاده از این اصول می‌توانید کدهایی بنویسید که قابلیت نگهداری بالایی دارند، گسترش‌پذیر مقیاس‌پذیر هستند و امکان تست و آزمایش آن‌ها به‌سادگی فراهم می‌شود.

بر اساس رای ۳ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
BaeldungRealPython
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *