علیرضا ارومند
علیرضا ارومند
خواندن ۸ دقیقه·۵ سال پیش

فصل هفتم Clean Architecture - اصل مسئولیت یکتا

1. مقدمه

در بین اصول برنامه‌نویسی، اصلی مسئولیت یکتا یا Single Responsibility Principle که به اختصار SRP نامیده می‌شود، کمتر از سایر اصول شناخته شده است. شاید این اتفاق به خاطر نام نامناسب این اصل افتاده باشد. برنامه نویسان، با توجه به نامی که این اصل دارد خیلی ساده فرض می‌کنند این اصل می‌گوید هر ماژول یک کار بیشتر نباید انجام دهد.

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

هر ماژول باید یک و تنها یک دلیل برای تغییر داشته باشد.

سیستم‌های نرم‌افزاری برای اینکه خواسته‌های ذینفعان سیستم‌ها را به درستی انجام دهند در طول زمان تغییر می‌کنند. در اصل کاربران و ذینفعان نرم‌افزار‌ها دلایل تغییر آن‌ها هستند. می‌توانیم تعریف SRP را به این شکل تغییر دهیم:

هر ماژول نرم‌افزاری مسئول پاسخ‌گویی به یک و تنها یک کاربر یا ذینفع است.

متاسفانه کلمات "کاربر" و "ذینفع" برای استفاده در این تعریف‌ها مناسب نیستند. در اصل گروهی از کاربران و ذینفعان هستند که از یک جنبه نرم‌افزاری استفاده می‌کنند و همزمان نیازمند تغییر رفتاری در سیستم هستند. بهتر است به جای استفاده از واژه‌های "کاربر" یا "ذینفع" از واژه "بازیگر" استفاده کنیم. در نتیجه آخرین نسخه از تعریف ما برای SRP به صورت زیر خواهد بود:

هر ماژول نرم‌افزاری مسئول پاسخ‌گویی به یک و تنها یک بازیگر است.

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

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

شاید برای درک بهتر این اصل بهتر باشد به علائم نقض این اصل نگاهی بیاندازیم.

2. علامت اول: تکثیر تصادفی:

مثال مورد علاقه من برای این مورد، کلاس Employee در سیستم حقوق و دست‌مزد است. این کلاس سه متد به نام‌های calculatePay, reportHours و save دارد.

شمای کلاس Employee
شمای کلاس Employee

با یک نگاه سطحی به این کلاس در‌می‌یابیم که این کلاس اصل SRP را زیر سوال برده‌است چرا که این کلاس مسئول پاسخ‌گویی به سه بازیگر متفاوت است.

متد calculatePay توسط واحد حسابداری مورد استفاده قرار‌ می‌گیرد که مسئول پاسخ‌گویی به CFO است.

متد reportHours توسط واحد منابع انسانی استفاده می شود که به COO گزارش می‌دهد.

متد save نیز توسط مدیران پایگاه داده مورد استفاده قرار می گیرد که به CTO گزارش می‌دهند.

با قراردادن سورس کد این سه تابع در یک کلاس در حقیقت این بازیگران را به هم وابسته کرده‌ایم. این وابستگی ممکن است باعث شود تغییری که توسط تیم CFO داده می‌شود، روی عملکرد تیم COO تاثیر بگذارد.

فرض کنید که متد‌های calculatePay و reportHours برای محاسبه ساعات بدون اضافه‌کاری الگوریتم یکسانی را به کار می‌برند. در این شرایط توسعه دهنده بدون توجه به این موضوع که این دو متد بازیگران متفاوتی دارد، الگوریتم را در قالب یک تابع بین این دو متد به اشتراک می گذارد و نام این متد را reqularHours می‌گذارد.

اشتراک الگوریتم در متد regularHours
اشتراک الگوریتم در متد regularHours

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

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

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

همه ما مشکلاتی از این دست را هنگام توسعه سیستم‌های نرم‌افزاری مشاهده کرده ایم و باعث بانی تمامی این ایرادات عدم رعایت SRP است. SRP به ما می‌گوید:

کدها را با توجه به بازیگران آن‌ها از هم جدا کنید.

3. علامت دوم: ادغام کردن:

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

تصور کنید تیم پایگاه‌داده تصمیم می‌گیرد تغییری در ساختار جدول Employee ایجاد کند و در همین زمان هم تیم منابع انسانی نیز تصمیم می‌گیرد فرمت نمایش ساعات را در گزارش‌ها تغییر دهید.

دو برنامه نویس مختلف از دو تیم مختلف در یک فایل که همان Employee است، شروع به اعمال تغییرات می‌کنند. متاسفانه در نهایت کد‌های آن‌ها دچار تضاد شده و مجبور به ادغام کردن کد‌ها می شویم.

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

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

4. راه حل‌ها:

راهکار‌های متفاوتی برای حل این مشکلات وجود دارد که موجب قرار گرفتن سورس‌کدها در فایل‌های متفاوت و جدایی آن‌ها می‌شود.

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

جداسازی ساختار داده از عملکرد
جداسازی ساختار داده از عملکرد

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

استفاده از الگوی Facade برای رفع پیچیدگی کار با چندین کلاس
استفاده از الگوی Facade برای رفع پیچیدگی کار با چندین کلاس

کلاس Facade در مثال بالا، عملکرد بسیار کوچکی دارد. وظیفه این کلاس، صرفا نمونه سازی از سایر کلاس‌ها، و محول کردن مسئولیت انجام کار به آن‌ها است.

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

نگهداری قواعد کاری کنار داده ها
نگهداری قواعد کاری کنار داده ها

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

5. جمع‌بندی:

اصل مسئولیت یکتا درباره کلاس‌ها و توابع صحبت می‌کند. اما این اصل در دو سطح دیگر نیز تکرار می‌شود. در سطح Componentها به صورت Common Closure Principle ظاهر شده و در سطح معماری به صورت Axis of Change ظهور پیدا می‌کند. همه این موارد را در فصل‌های آینده بررسی خواهیم کرد.

clean architectureمعماری تمیزsolidSingle Responsibility Principleاصل مسئولیت یکتا
شاید از این پست‌ها خوشتان بیاید