آموزش اصل Single Responsibility از اصول SOLID

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

اصل Single Responsibility در فارسی با عنوان اصل مسئولیت واحد یا اصل تک وظیفگی و به‌طور خلاصه SRP (مخفف Single Responsibility Principle) نیز بیان می‌شود. این اصل تأکید دارد که کلاس‌ها و توابع طوری طراحی شوند که از تغییرات غیرضروری و پیچیدگی‌های نامناسب جلوگیری شود.

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

اصل Single Responsibility چیست؟

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

اگر بخواهم یک مثال خیلی ساده و کلی از دنیای واقعی بزنم، یک سازمان یا شرکت را فرض کنید. معمولاً در هر شرکتی، هر کدام از اعضا وظایف خاص و مشخصی دارند. هر کسی مسئول یکی از فرآیندها و کارهایی است که باید در شرکت انجام شود. مثلاً کسی که برنامه‌نویس back-end است، کارهای مربوط به دیجیتال مارکتینگ را انجام نمی‌دهد. همچنین کسی که مسئول امور مالی است، هیچ کاری در زمینه برنامه‌نویسی انجام نمی‌دهد.

اصل SRP در کد

فرض کنید می‌خواهیم کلاسی برای مدیریت اطلاعات کاربر بنویسیم. مثال‌هایی که می‌زنم به زبان پایتون است ولی مفاهیم در تمام زبان‌های برنامه‌نویسی یکسان است.

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

Class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def get_user_info(self):
        return {"name": self.name, "email": self.email}

    def save_to_database(self):
        # کدهای اتصال به دیتابیس و ذخیره اطلاعات در جدول
        pass

    def send_welcome_email(self):
        # کدهای ارسال ایمیل خوش‌آمدگویی
        pass

    def update_email(self, new_email):
        if self.is_valid_email(new_email):
            self.email = new_email

    def is_valid_email(self, email):
        # کدهای بررسی فرمت ایمیل
        pass

این نوع کدنویسی، اصل Single Responsibility را نقض کرده است. چرا؟

کلاس User برای مدیریت اطلاعات کاربر است. بنابراین می‌بایست صرفاً وظایف مربوط به اطلاعات کاربر را انجام دهید. در حالی که:

  • در متد save_to_database() کارهایی برای اتصال به دیتابیس و ذخیره‌سازی داده در آن انجام می‌شود.
  • در متد send_welcome_email() کدهایی برای ارسال ایمیل داریم.
  • متد is_valid_email() در راستای هدف کلاس (که مدیریت اطلاعات کاربر هست)  نیست و باید برایش فکری کنیم.

متدهای get_user_info() و update_email() در راستای هدف کلاس هستند و کار مشخصی را انجام می‌دهند. بنابراین این دو متد، مناسب‌اند.

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

برای اینکه کد بالا را تبدیل به کدی کنیم که از اصل Single Responsibility پیروی می‌کند، باید تغییراتی در کلاس ایجاد کرده و همچنین کلاس‌های جدیدی بنویسیم.

ابتدا کلاسی برای کارهای دیتابیس می‌نویسیم. این کلاس وظایف مربوط به کار با داده‌های جدول کاربر در دیتابیس پایتون را بر عهده دارد.

Class UserDB:
    def save(self, user):
        # کدهای ذخیره‌سازی اطلاعات کاربر در دیتابیس
        pass

توجه کنید که در حالت قبلی چون متد save() درون کلاس User بود، مستقیماً به کاربر و اطلاعاتش دسترسی داشتیم. اما در این جا، می‌بایست شیء کاربر را به‌عنوان پارامتر ورودی تعریف کنیم تا به اطلاعات موردنظر دسترسی داشته باشیم.

به‌طور مشابه، لازم است توابع مربوط به کار با ایمیل را جدا کنیم.

Class EmailService:
    def send_welcome_email(self, user):
        # کدهای ارسال ایمیل خوش‌آمدگویی
        pass

    def is_valid_email(self, email):
        # کدهای بررسی فرمت ایمیل
        pass

نکته: در پروژه‌های بزرگ‌تر، معمولاً کلاسی برای کار با سرویس ایمیل (صرفاً ارتباط با سرور ایمیل و ارسال ایمیل‌ها و …؛ مشابه آموزش ارسال ایمیل با پایتون یا ارسال ایمیل در PHP) داریم. در چنین حالتی، بهتر است متد is_valid_email() را به کلاس دیگری که مرتبط به اعتبارسنجی داده‌ها و فرمت‌هاست منتقل کنیم. اما در این آموزش برای کاهش پیچیدگی، از همین یک کلاس برای وظایف مرتبط با ایمیل و ارسال ایمیل استفاده می‌شود.

اکنون متدهای کلاس User به‌صورت زیر تغییر می‌کند:

Class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def get_user_info(self):
        return {"name": self.name, "email": self.email}

    def update_email(self, new_email):
        if EmailService.is_valid_email(new_email):
            self.email = new_email

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

اگر کلاسی داشته باشید که کارهایی را انجام دهد که از یک جنس نیستند یا از نظر منطقی قابل جداسازی باشند، احتمالاً در حال دور شدن از اصل Single Reposibility هستید.

مزایای اصل Single Responsibility

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

  1. افزایش خوانایی کد: وقتی هر کلاس تنها یک وظیفه خاص دارد، سایر توسعه‌دهندگان و هم‌تیمی‌ها می‌توانند به‌راحتی کد را بررسی و درک کنند.
  2. بهبود قابلیت نگهداری: تغییرات کد تنها به کلاس‌هایی محدود می‌شود که مسئول انجام آن وظیفهٔ خاص هستند. بنابراین هنگام اعمال تغییرات، می‌دانیم که باید سراغ کدام کلاس‌ها برویم و نیازی نیست ساختار کد و فراخوانی‌های توابع را یکی یکی بررسی کنیم.
  3. تسهیل تست و اشکال‌زدایی: وقتی وظایف منطقی در کلاس‌ها به‌درستی تقسیم شده باشند، تست و عیب‌یابی هر کلاس ساده‌تر خواهد بود. چون اولاً پیچیدگی کمتری داریم، ثانیاً می‌دانیم مسئولیت هر مشکل منطقی خاص، احتمالاً مربوط به کدام کلاس است.
مثال انتزاعی و تصویری از اصل SRP یا مسئولیت واحد
مثال انتزاعی و تصویری از اصل SRP یا مسئولیت واحد

روش پیاده‌سازی اصل SRP در پروژه‌ها

استفاده از اصل Single Reponsibility یا تک وظیفگی نیازمند این است که ما همیشه کدها را با دقت و توجه به نقش‌ها و وظایف طراحی کنیم. معمولاً دو گام زیر پیشنهاد می‌شود:

  • تفکیک وظایف مرتبط به کلاس‌های جداگانه: همان‌طور که در مثال کد بالا مشاهده کردید، بهتر است در هنگام طراحی و کدنویسی، هر وظیفه مشخص را به یک کلاس مستقل اختصاص دهید.
  • شناسایی و جداسازی متدهای چند وظیفه‌ای: اگر متدی دارید که چندین کار مختلف را در بدنهٔ خودش انجام می‌دهد، احتمالاً بتوانید آن را به چند متد کوچک‌تر (که هر کدام هدف و وظیفه مشخص‌تری دارند) تقسیم کنید.

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

  1. ابتدا بررسی می‌کند آیا ایمیلِ واردشده قبلاً ثبت شده یا خیر.
  2. سپس اطلاعات کاربر را در دیتابیس ذخیره می‌کند.
  3. در نهایت پیام موفقیت‌آمیز بودن یا نبودن را نمایش می‌دهد.

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

درون متد قبلی (همانی که کدهای ثبت‌نام کاربر درونش قرار داشت) صرفاً لازم است متدهای جدید را صدا زده و در صورت نیاز از ساختارهای شرطی (مثل شرط پایتون یا شرط PHP) استفاده کرد.

کدنویسی با اصل مسئولیت واحد

فرض کنید کد زیر را برای مدیریت فاکتور نوشته‌ایم. این کلاس را بررسی کنید و قبل از اینکه آموزش را ادامه دهید، با خودتان فکر کنید که احتمالاً لازم است چه کارهایی انجام دهید تا کدهای شما اصل Single Responsibility را رعایت کرده باشد. مجدداً یادآوری می‌کنم که: «هر کلاس و متد باید یک هدف و وظیفه واحد داشته و از نظر منطقی یک کار انجام دهد

class Order:
    def __init__(self, order_id, customer_id, products):
        self.order_id = order_id
        self.products = products
        self.customer_id = customer_id

    def process(self):
        # محاسبه مبلغ کل فاکتور
        # ثبت فاکتور و جزئیاتش در دیتابیس
        # ارسال ایمیل تأییدیه سفارش
        pass

    def get_products(self):
        return self.products

بازنویسی متدهای چند وظیفه‌ای

در این کلاس متدی به نام process() داریم که سه وظیفه مختلف را بر عهده دارد. این وظایف عبارت‌اند از:

  • محاسبه مبلغ فاکتور
  • ثبت اطلاعات در دیتابیس
  • ارسال ایمیل

برای اینکه این متد را طبق اصل SRP بازنویسی کنیم، باید این وظایف را در قالب متدهای کوچک‌تر نوشته و از آن‌ها درون این متد استفاده کنیم. چیزی شبیه به کد زیر:

class Order:
    def __init__(self, order_id, customer_id, products):
        self.order_id = order_id
        self.products = products
        self.customer_id = customer_id

    def calculate_total_price(self):
        # محاسبه مبلغ کل فاکتور
        pass

    def save_to_database(self):
        # اتصال به دیتابیس و ثبت اطلاعات
        pass

    def send_order_confirmation(self):
        # ارسال ایمیل تأییدیه
        pass

    def process(self):
        self.calculate_total_price()
        self.save_to_database()
        self.send_order_confirmation()

    def get_products(self):
        return self.products

بهینه‌سازی کلاس با رعایت SRP

تا اینجا، تمام متدهایی که داریم طبق اصل Single Responsibility هستند؛ اما کلاس Order دارای وظایف متعدد است. بهتر است وظایف ارسال ایمیل و کار با دیتابیس که ارتباط چندانی با موجودیت سفارش (Order) ندارد را در کلاس‌های دیگر تعریف کرده و صرفاً از آن‌ها استفاده کنیم.

class Order:
    def __init__(self, order_id, customer_id, products):
        self.order_id = order_id
        self.products = products
        self.customer_id = customer_id

    def calculate_total_price(self):
        # محاسبه مبلغ کل فاکتور
		self.total_price = 999999
        pass

    def process(self):
        self.calculate_total_price()
        OrderDB.save(self, self.total_price)
        EmailService.send_order_confirmation(self)

    def get_products(self):
        return self.products

دو کلاس جدید به‌صورت زیر می‌شود:

class OrderDB:
    def save(self, order, total_price):
        # اتصال به دیتابیس و ثبت اطلاعات سفارش با مبلغ کل
        pass

class EmailService:
    def send_order_confirmation(self, order):
        # ارسال ایمیل تأییدیه سفارش به مشتری
        pass

در پروژه‌هایی که از اصل مسئولیت واحد (تک وظیفگی / SRP) پیروی می‌کنند، می‌توانیم با تغییر یک کلاس خاص که مسئول یک تابع است، رفتار آن تابع را تغییر دهیم. همچنین، در صورت بروز مشکل در یک اجرای یک تابع، می‌دانیم که اشکال از کجاست. به عبارتی مطمئن هستیم که تنها همان یک کلاس تحت تأثیر قرار می‌گیرد. اصل Single Responsibility به بهبود خوانایی کد نیز کمک می‌کند، زیر برای درک عملکرد یک تابع، تنها نیاز به بررسی یک کلاس داریم.

خلاصه آموزش

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

اصل SRP کمک می‌کند که هنگام اعمال تغییرات در کدها، عملیات تغییر ساده‌تر و بدون تأثیر بر قسمت‌های دیگر پروژه باشد. این‌گونه احتمال خطا کمتر می‌شود و روند توسعه و درک کدها سریع‌تر. این اصل توسط Robert C. Martin مطرح شده است؛ که می‌توانید توضیحات ایشان را در کتاب Clean Coder یا یادداشت‌های او به زبان انگلیسی در اینجا بخوانید.

امیدوارم از این آموزش استفاده کرده باشید. اگر سؤال یا بحثی دارید از بخش دیدگاه‌ها مطرح کنید.