این اصل حاکی از آن است که هر کلاس باید مسئول یک کار خاص باشد و این در حالی است که اگر کلاس بیش از یک تَسک را هندل کند، وابستگی به آن کلاس در جایجای اپلیکیشن بیشتر خواهد شد به طوری که تغییر در یکی از فانکشنهای کلاس مذکور ممکن است منجر به تغییر دیگر فانکشنها و بالتبع ایجاد مشکل گردد (لازم به یادآوری است که این اصل در مورد معماری میکروسرویس هم صادق است.) در یک کلام، فقط و فقط باید یک دلیل برای ریفکتور کردن یک کلاس خاص وجود داشته باشد که در غیر این صورت، Single Responsibility Principle یا به اختصار SRP نقض شده است.
لازم به یادآوری است که برای شلوغ نشدن سورسکدهای مورد استفاده در این آموزش، از نوشتن کلیهٔ تگهای آغازین php؟>
خودداری کردهایم. به منظور روشن شدن کاربرد SRP، ساختار پروژهٔ زیر را مد نظر قرار میدهیم:
single-responsibility-principle/
├── HTMLReportFormatter.php
├── index.php
├── JsonReportFormatter.php
├── ReportFormattable.php
└── Report.php
برای شروع، فایلی تحت عنوان Report.php
ساخته و کلاس زیر را داخلش مینویسیم:
class Report
{
public function getTitle()
{
return 'Report Title';
}
public function getDate()
{
return '2000-04-21';
}
public function getContents()
{
return [
'title' => $this->getTitle(),
'date' => $this->getDate(),
];
}
public function formatJson()
{
return json_encode($this->getContents());
}
}
همانطور که مشاهده میشود، کلاسی داریم به نام Report
که داخل آن چهار فانکشن مختلف تعریف کردهایم که عبارتند از:
()getTitle
که این وظیفه را دارا است تا عنوان گزارش را ریترن کند (بازگرداند).()getDate
که این وظیفه را دارا است تا تاریخ گزارش را ریترن کند.()getContents
که این وظیفه را دارا است تا عنوان و تاریخ را با هم ریترن کند.()formatJson
که این وظیفه را دارا است تا خروجی فانکشن()getContents
در قالب جیسون بازگرداند.
این کلاس ناقض قانون SRP است زیرا تَسکهای مختلفی بر عهدهٔ آن گذاشته شده است. به عبارتی، کاربرد فانکشن ()formatJson
به کلی با سایر فانکشنها متفاوت است و هرگز نباید داخل این کلاس قرار داده باشد. فرض کنیم روزی بخواهیم علاوه بر JSON، فرمت HTML را در اختیار کاربر قرار دهیم که در چنین شرایطی یا باید این فانکشن را ریفکتور کنیم و یا فانکشن دیگری مثلاً تحت عنوان ()formatHtml
اضافه نماییم. برای رفع این مشکل و تبعیت از اصلِ SRP، کدهای فوق را به صورت زیر بازنویسی میکنیم:
class Report
{
public function getTitle()
{
return 'Report Title';
}
public function getDate()
{
return '2000-04-21';
}
public function getContents()
{
return [
'title' => $this->getTitle(),
'date' => $this->getDate(),
];
}
}
سپس در فایلی تحت عنوان ReportFormattable.php
یک اینترفیس به صورت زیر تعریف میکنیم:
interface ReportFormattable
{
public function format(Report $report);
}
حال فایل دیگری تحت عنوان JsonReportFormatter.php
ساخته و کدهای زیر را داخل آن مینویسیم:
require_once 'ReportFormattable.php';
class JsonReportFormatter implements ReportFormattable
{
public function format(Report $report)
{
return json_encode($report->getContents());
}
}
در تفسیر کدهای فوق باید گفت که تنها تغییر صورتگرفته داخل کلاس Report
این است که فانکشن ()formatJson
که ارتباطی با سایر فانکشنهای داخل این کلاس نداشت را حذف کرده سپس دست به ساخت یک اینترفیس تحت عنوان ReportFormattable
زدیم که این وظیفه را دارا است تا فقط اصول ساخت کلاسهایی را مشخص سازد که وظیفهٔ فرم دادن به خروجی را دارند بدین صورت که داخل آن فانکشنی به نام ()format
نوشتیم که یک پارامتر ورودی تحت عنوان report$
میگیرد که حتماً باید از جنس کلاس Report
باشد (در همین راستا توصیه میکنیم به مقالهٔ آشنایی با مفهوم Type Hinting در زبان PHP مراجعه نمایید.)
در ادامه، کلاس JsonReportFormatter
را از اینترفیس ReportFormattable
اصطلاحاً ایمپلیمنت کرده، فانکشنی تحت عنوان ()format
داخل آن نوشتهایم که موقع کال کردن (فراخوانی) این فانکشن، حتماً باید آبجکتی از روی کلاس Report
به آن پاس داده شود سپس داخل این فانکشن با استفاده از فانکشنی از پیش تعریف شده در زبان پیاچپی به نام ()json_encode
خروجی فانکشن ()getContents
را ریترن کردهایم که این خروجی در قالب فرمت JSON در اختیار دولوپر قرار خواهد گرفت که جهت تست کردن این اپلیکشن، فایلی تحت عنوان index.php
میسازیم و آن را به صورت زیر تکمیل میکنیم:
require_once 'Report.php';
$report = new Report();
var_dump($report->getContents());
در خط اول فایل Report.php
را ایمپورت کردهایم چرا که در ادامه قصد داریم از کلاس Report
استفاده نماییم بدین صورت که آبجکتی تحت عنوان report$
از روی این کلاس ساختهایم. حال قصد داریم به فانکشن ()getContents
دست یابیم اما از آنجا که خروجی این فانکشن یک آرایه است، هرگز نمیتوانیم با دستوراتی همچون echo
یا print
خروجی آن را نمایش دهیم و از همین روی از فانکشن ()var_dump
استفاده نمودهایم. به عنوان خروجی اسکریپت فوق داریم:
array(2) { ["title"]=> string(12) "Report Title" ["date"]=> string(10) "2000-04-21" }
در حقیقت، کلاس Report
فقط و فقط این وظیفه را دارا است تا گزارش ارائه کند و این در صورتی است که اگر بخواهیم این گزارش را در قالب جیسون عرضه کنیم (تا مثلاً در یک اپ موبایل بعداً آن را مورد استفاده قرار دهیم)، یک آبجکت از روی کلاس JsonReportFormatter
میسازیم و آن را به صورت زیر مورد استفاده قرار میدهیم:
require_once 'Report.php';
require_once 'JsonReportFormatter.php';
$report = new Report();
$reportAsJson = new JsonReportFormatter();
echo $reportAsJson->format($report);
در خط دوم کلاس JsonReportFormatter
را ایمپورت کردهایم سپس آبجکتی تحت عنوان reportAsJson$
ساختهایم و در خط بعد هم فانکشن ()format
را به این آبجکت منتسب کرده و به عنوان پارامتر ورودی هم آبجکت report$
که قبلاً ساخته بودیم را پاس دادهایم به طوری که خروجی اسکریپت فوق عبارت است از:
{"title":"Report Title","date":"2000-04-21"}
میبینیم که خروجی به صورت JSON درآمد است. حال فرض کنیم بسته به نیازهای آتی، قصد داریم تا خروجی را در قالب HTML نیز در معرض دید کاربر قرار دهیم که با توجه به اینکه کدی استاندارد نوشتهایم، به سادگی قادر خواهیم خواهیم بود تا فایل دیگری مثلاً با نام HTMLReportFormatter.php
نوشته و آن را به صورت زیر تکمیل میکنیم:
require_once 'ReportFormattable.php';
class HTMLReportFormatter implements ReportFormattable
{
public function format(Report $report)
{
$resultset = '';
foreach($report->getContents() as $item) {
$resultset .= "<h2>$item</h2>";
}
return $resultset;
}
}
ابتدا فایل index.php
را به صورت زیر آپدیت کرده سپس خروجی را مشاهده میکنیم و در نهایت به تفسیر کدها خواهیم پرداخت:
require_once 'Report.php';
require_once 'HTMLReportFormatter.php';
$report = new Report();
$reportAsHtml = new HTMLReportFormatter();
echo $reportAsHtml->format($report);
و این در حالی است که در مرورگر خروجیهای زیر داخل تگ <h2></h2>
نمایش داده خواهند شد:
Report Title
2000-04-21
آنچه داخل کلاس HTMLReportFormatter
رخ میدهد بدین صورت است که داخل فانکشن ()format
متغیری تعریف کردهایم با نام resultset$
که مقدار اولیهٔ آن خالی است. سپس با استفاده از حلقهٔ foreach
تکتک مقادیر ذخیرهشده داخل ()report->getContents$
را داخل تگهای <h2></h2>
گذاشته و داخل متغیر resultset$
ذخیره کرده و در نهایت آن را ریترین کردهایم (کاربرد علائم =.
این است که مقادیر جدید را به مقادیر قبلی این متغیر ضمیمه میکند.) در ادامه، داخل فایل index.php
هم صرفاً کلاس JsonReportFormatter
را با HTMLReportFormatter
جایگزین کردهایم.
به طور خلاصه، اصلِ Single Responsibility این تضمین را ایجاد میکند که ما مجموعهای از کلاسهای تخصصی داشته باشیم که هر کدام تَسک (کار) خاصی یا مجموعهای از تَسکهای مرتبط با هم را هندل کرده و در صورتی که در آینده نیاز به ریفکتور کردن یا توسعهٔ اپلیکیشن داشته باشیم، فقط و فقط همان کلاس مربوطه را آپدیت خواهیم کرد.