سرفصل‌های آموزشی
آموزش قوانین SOLID
درآمدی بر قانون Single Responsibility

درآمدی بر قانون Single Responsibility

این اصل حاکی از آن است که هر کلاس باید مسئول یک کار خاص باشد و این در حالی است که اگر کلاس بیش از یک تَسک را هندل کند، وابستگی به آن کلاس در جای‌جای اپلیکیشن بیشتر خواهد شد به طوری که تغییر در یکی از فانکشن‌های کلاس مذکور ممکن است منجر به تغییر دیگر فانکشن‌ها و بالتبع ایجاد مشکل گردد (لازم به یادآوری است که این اصل در مورد معماری میکروسرویس‌ هم صادق است.) در یک کلام، فقط و فقط باید یک دلیل برای ریفکتور کردن یک کلاس خاص وجود داشته باشد که در غیر این صورت، 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 این تضمین را ایجاد می‌کند که ما مجموعه‌ای از کلاس‌های تخصصی داشته باشیم که هر کدام تَسک (کار) خاصی یا مجموعه‌ای از تَسک‌های مرتبط با هم را هندل کرده و در صورتی که در آینده نیاز به ریفکتور کردن یا توسعهٔ اپلیکیشن داشته باشیم، فقط و فقط همان کلاس مربوطه را آپدیت خواهیم کرد.

online-support-icon