سرفصل‌های آموزشی
آموزش قوانین SOLID
درآمدی بر قانون Open-closed

درآمدی بر قانون Open-closed

Open-closed Principle

یک اپلیکیشن از هر نوعی که باشد از مجموعه‌ای از کلاس‌ها، فانکشن‌ها و ماژول‌های مختلف تشکیل شده است و اصلِ Open-closed حاکی از آن است که اپلیکیشن باید به گونه‌ای نوشته شود که دولوپر بتواند به طور مثال یک کلاس را توسعه‌ دهد بدون آنکه کدهای قبلی آن کلاس را دستخوش تغییر سازد که در نهایت این تضمین را ایجاد می‌کند که سورس‌کد بسته به نیازهای آتی اپلیکیشن توسعه یابد بدون آنکه بخش‌های قدیمی را تغییر دهیم چرا که خودِ این مسئله (تغییر کدهای قدیمی) منجر ایجاد باگ خواهد شد.

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

برای درک سازوکار قانون Open-closed،‌ همین سناریوی فرضی را تبدیل به کد می‌کنیم به طوری که ساختار پروژه به صورت زیر خواهد بود:

open-close-principle/
├── GmailUser.php
├── index.php
├── LoginInterface.php
├── LoginModule.php
└── NormalUser.php

لازم به یادآوری است که برای شلوغ نشدن سورس‌کدهای مورد استفاده در این آموزش، از نوشتن کلیهٔ تگ‌های آغازین php؟> خودداری کرده‌ایم. به منظور روشن شدن کاربرد OCP، ساختار پروژهٔ زیر را مد نظر قرار می‌دهیم:

ابتدا پوشه‌ای می‌سازیم با نامی دلخواه همچون open-closed-principle و داخل آن فایلی تحت عنوان LoginModule.php می‌سازیم که حاوی کدهای زیر است:

class LoginModule
{
    public function login($user)
    {
        if ($user instanceof NormalUser) {
            $this->authenticateNormalUser($user);
        } elseif($user instanceof GmailUser) {
            $this->authenticateGmailUser($user);
        }
    }
}

در حقیقت، فانکشن ()login این وظیفه را دارا است تا بسته به نوع کاربر که معمولی است یا حساب کاربری جیمیل دارد، به ترتیب فانکشن‌های فرضی ()authenticateNormalUser و ()authenticateGmailUser را فراخوانی کند چرا که با دستورات شرطی چک کرده‌ایم ببنیم آیا تایپ (نوع) کاربر به طور مثال NormalUser است یا GmailUser و بسته به همان تایپ، فانکشن مرتبط را کال کرده‌ایم و این در شرایطی است که اگر بخواهیم امکان لاگین با گیت‌هاب را هم به وب اپلیکیشن خود بیفزاییم، باید کلاس فوق را به صورت زیر ریفکتور کنیم:

class LoginModule
{
    public function login($user)
    {
        if ($user instanceof NormalUser) {
            $this->authenticateNormalUser($user);
        } elseif($user instanceof GmailUser) {
            $this->authenticateGmailUser($user);
        } elseif($user instanceof GithubUser) {
            $this->authenticateGithubUser($user);
        }
    }
}

می‌بینیم که یک بلوک elseif دیگر اضافه کرده و کاربران گیت‌هاب را با استفاده از آن هندل کرده‌ایم که این کار ناقض Open-Closed Principle یا به اختصار OCP است زیرا ما برای آنکه فیچرهای جدیدی به ماژول خود اضافه کنیم، داخل کدهای قبلی دست برده‌ایم و این در حالی است که این اصل ما را موظف می‌کند که به گونه‌ای کد بزنیم تا بدون آنکه کدهای قبلی دستخوش تغییر شوند، قابلیت‌های جدید به آن افزوده گردد. برای رفع این مشکل، ابتدا یک اینترفیس تحت عنوان LoginInterface داخل فایلی به نام LoginInterface.php ایجاد می‌کنیم:

interface LoginInterface
{
    public function authenticateUser();
}

کاملاً مشخص است هر کلاسی که از این اینترفیس ایمپلیمنت شود، باید فانکشنی با نام ()authenticateUser داشته باشد که یک پارامتر ورودی تحت عنوان user$ می‌گیرد. حال می‌خواهیم کلاسی بنویسیم که مسئول هندل کردن کاربران عادی سایت است؛ به عبارتی، آن دسته از کاربرانی که با نام‌کاربری و پسورد لاگین می‌کنند به طوری که داریم:

require_once 'LoginInterface.php';
class NormalUser implements LoginInterface 
{
    public function authenticateUser()
    {
        return "normal user logged in via username and password";
    }
}

ابتدا فایلی ساخته‌ایم تحت عنوان NormalUser.php سپس در خط اول فایل LoginInterface.php را ایمپورت کرده‌ایم چرا که این کلاس قرار است تا از اینترفیس LoginInterface استفاده کند. همان‌طور که این اینترفیس ما را موظف کرده، داخل این کلاس باید فانکشنی به نام ()authenticateUser بنوسیم که مسئول تأیید هویت کاربران عادی است. در ادامه، کلاس LoginModule که پیش از این ساختیم را به صورت زیر تغییر می‌دهیم:

require_once 'LoginInterface.php';
class LoginModule
{
    public function login(LoginInterface $user)
    {
        return $user->authenticateUser();
    }
}

به عبارتی، دستورات شرطی که دائم در شرف تغییر بودند را حذف کرده و صرفاً فانکشن ()authenticateUser که منتسب به آبجکت user$ است را کال کرده و ریترن می‌کنیم. حال به منظور تست این اپلیکیشن، فایلی تحت عنوان index.php ساخته و کدهای زیر را داخل آن درج می‌کنیم:

require_once 'LoginModule.php';
require_once 'NormalUser.php';
$loginObject = new LoginModule();
$user = new NormalUser();
echo $loginObject->login($user);

ابتدا به ساکن فایل‌های مورد نیاز را ایمپورت کرده سپس شیئی به نام loginObject$ از روی کلاس LoginModule ساخته‌ایم که در نهایت قرار است فانکشن ()login این کلاس را فراخوانی کنیم اما از آنجا که نیاز به یک پارامتر ورودی از جنس LoginInterface دارد، یک آبجکت از روی کلاس NormalUser تحت عنوان user$ ساخته‌ایم و به آن پاس داده‌ایم:

normal user logged in via username and password

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

require_once 'LoginModule.php';
require_once 'NormalUser.php';
echo (new LoginModule())->login(new NormalUser());

به عبارتی،‌ هر زمانی که بخواهیم یک کلاس را بدون ساخت یک متغیر مورد استفاده قرار داد، ابتدا یک جفت () درج کرده، همان‌طور که می‌بینیم، ()new LoginModule را داخل پرانتزها نوشته و با قرار دادن علائم <- پس از پرانتزهای بیرونی، می‌توانیم به فانکشن‌های داخل این کلاس دست یابیم.

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

require_once 'LoginInterface.php';
class GmailUser implements LoginInterface 
{
    public function authenticateUser()
    {
        return "user logged in via gmail account";
    }
}

فایلی ساخته‌ایم تحت عنوان GmailUser.php که تا حد زیادی شبیه به کلاس NormalUser است. جهت تست، کدهای داخل فایل index.php را به صورت زیر آپدیت می‌کنیم:

require_once 'LoginModule.php';
require_once 'GmailUser.php';
echo (new LoginModule())->login(new GmailUser());

به عنوان خروجی هم خواهیم داشت:

user logged in via gmail account

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

online-support-icon