کاربرد Interface ها در PHP و Laravel

واژه Interface به معنی رابط (کاربری) است. رابط کاربری در برنامه نویسی، رفتار (متد) های یک کلاس (ماژول) می‌باشد که دیگر ماژول ها از طریق آنها می‌توانند با آن کلاس (ماژول) ارتباط برقرار کنند.

برای مثال، کلاس زیر (Gmail) مسئولیت فرستادن ایمیل را بر عهده دارد.

class Gmail {
    public function send(string $to, string $content) {
         // Preparing Gmail API requirements
        $this->call([$to, $content, $username, $password, $server]);
    }
    private function call(array $parameters) {
        // Send email using the Gmail API
    }
}

در کلاس بالا، متد send بصورت public (عمومی) تعریف شده و دیگر ماژول ها برای ارسال ایمیل آن را فراخوانی می‌کنند. این متد، رابط کاربری کلاس محسوب می‌شود. در طرف دیگر، متد call یک متد private (خصوصی) است و تنها مصرف داخلی دارد (متد send از آن استفاده می‌کند) و جزو رابط کاربری کلاس محسوب نمی‌شود.

اکنون با استفاده از یک Interface، رابطه کاربری کلاس های ارسال ایمیل را تعریف کنیم.

interface Email {
    function send(string $to, string $content);
}

در کلاس Gmail نیز مشخص می‌کنیم که این کلاس از رابط کاربری Email پیروی می‌کند.

class Gmail implements Email { ... }

البته هنوز سوال اصلی که Interface چه کمکی به ما می‌کنند پابرجاست. این سوال را با معرفی اصول SOLID پاسخ می‌دهیم.

کاربرد Interface ها در PHP و Laravel
کاربرد Interface ها در PHP و Laravel

اصول SOLID

اصول SOLID، مجموعه پنج اصل مهم در برنامه نویسی شی گرا (Object-oriented programming) است. این اصول عبارت‌اند از:

  • اصل Single-responsibility (تک مسئولیتی)
  • اصل Open-closed (باز-بسته)
  • اصل Liskov substitution (جایگزینی لیسکوف)
  • اصل Interface segregation (تفکیک رابطه کاربری)
  • اصل Dependency inversion (وارونگی وابستگی)

اصل Dependency inversion در SOLID

اصل Dependency inversion (وارونگی وابستگی) در برنامه نویسی شی گرا و اصول SOLID، بر وابستگی به تعاریف انتزاعی (Abstraction) به جای وابستگی به پیاده سازی (Concrete یا Implementation) تاکید می‌کند.

به بیان ساده ساده تر، ماژول های پروژه باید به Interface ها (مانند Email) وابستگی داشته باشند، نه پیاده سازی ها (مانند Gmail). پیاده سازی ها ممکن است در طول زمان تغییر کنند و فقط Interface ها هستند که ثابت می‌مانند.

برای مثال، در زیر یک Controller به نام UsersContronller را می‌بینید. در متد create که Action مربوط به API ساخت کاربر جدید است، پس از ذخیره اطلاعات کاربر در دیتابیس، یک ایمیل خوشامد گویی برای او ارسال می‌کند.

class UsersContronller extends Controller {
    public function create(Request $request) {
        // Save user information into database
        $email = new Gmail();
        $email->send($user->email, 'Welcome dear user...');
    }
}

مشکل Controller بالا تا وقتی که ما تنها از Gmail برای ارسال ایمیل استفاده می‌کنیم ممکن است مشخص نشود. وقتی تصمیم بگیریم از سرویس ایمیل دیگری همچون Mailgun استفاده کنیم، باید این کلاس و دیگر کلاس هایی که از Gmail استفاده می‌کنند را پیدا کرده و کد های آنها را تغییر دهیم.

مشکل Controller بالا وابستگی به پیاده سازی (به جای وابستگی به تعاریف انتزاعی) است و اصل Dependency inversion از اصول SOLID را نقض کرده است. برای رفع این مشکل، این Controller را بصورت زیر بازنویسی می‌کنیم.

class UsersContronller extends Controller {
    public function create(Request $request, Email $email) {
        // Save user information into database
        $email->send($user->email, 'Welcome dear user...');
    }
}

دیگر بخش های پروژه را نیز در کد زیر می‌بینید.

interface Email {
    function send(string $to, string $content);
}
class Gmail implements Email { ... }
class Mailgun implements Email { ... }

از Interface ها نمی‌توان Object (شی) ساخت، اما از کلاس هایی که Interface ها پیاده سازی می‌کنند می‌شود Object ساخت. نوع پارامتر email در متد create کنترلر بالا، اینترفیس Email است. در نتیجه این پارامتر می‌تواند یک Object از کلاسی مانند Gmail یا Mailgun باشد که اینترفیس Email را پیاده سازی می‌کند. هر کلاسی که از رابط کاربری اینترفیس Email پیروی کند مجبور می‌شود متد send را پیاده سازی کند. در نتیجه هر Object که از نوع اینترفیس Email باشد حتما دارای متد پیاده سازی شده‌ی send است.

کلاس UsersContronller اکنون وابسته به اینترفیس Email (یک تعریف انتزاعی) است و وابستگی به پیاده سازی هایی همچون Gmail یا Mailgun ندارد. برای استفاده از این کلاس می‌توانید هر پیاده سازی که از رابط کاربری Email پیروی کند را به متد create آن تزریق کنیم. تزریق این وابستگی می‌تواند توسط ابزاری به نام IoCC انجام شود که در بخش بعد شرح داده می‌شود.

ابزار IoCC

ابزار IoCC یا Inversion of Control Container طراحی شده‌اند‌ تا با استفاده از آن اصل Dependency inversion در پروژه رعایت شود. این ابزار در ابتدای اجرای پروژه لیست Abstraction ها و پیاده سازی های آنها را دریافت می‌کنند و در ادامه هنگامی که یک ماژول به یک Abstraction نیاز داشت، پیاده سازی مناسب را در اختیار آن ماژول قرار می‌دهند.

در پروژه های PHP می‌توانید از miladrahimi/phpcontainer بعنوان IoCC پروژه استفاده کنید. در پروژه های Laravel می‌توانید از IoCC خود لاراول استفاده کنید.

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

  • متد های Constructor در Controller ها
  • متد های Controller که نقش Action برای یک Route ایفا می‌کنند (مانند create در مثال بالا)
  • متد های handle مربوط به Middleware ها
  • متد های handle مربوط به Job ها
  • متد های handle مربوط به Command ها

در متد register از کلاس App/Providers/AppServiceProvider می‌توانید تعیین کنید که به هنگام نیاز ماژول های پروژه به یک Abstraction، کدام پیاده سازی برای آنها فراهم شود. این فرایند، Bind کردن نام دارد و اصطلاحا Interface یا Abstraction ها را به پیاده سازی های آنها Bind می‌کنیم.

برای مثال در یک پروژه، کد زیر در متد register، اینترفیس Email را به پیاده سازی آن که Gmail می‌باشد، Bind می‌کند.

class AppServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->bind(Email::class, Gmail::class);
    }
    // ...
}

اکنون، UsersController را در این پروژه نظر بگیرید.

class UsersContronller extends Controller {
    public function create(Request $request, Email $email) {
        // Save user information into database
        $this->email->send($user->email, 'Welcome dear user...');
    }
}

می‌دانیم که متد create بعنوان یک Action توسط IoCC لاراول فراخوانی می‌شود تا وابستگی های مورد نیاز آن تزریق شود. با توجه به Binding تعریف شده، IoCC لاراول پیاده سازی Gmail بعنوان پارامتر این متد تزریق می‌کند.

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

$email = app(Email::class);
// $email will be an object of Gmail

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

class UsersContronller extends Controller {
    public function create(Request $request) {
        // Save user information into database
        app(Email::class)->send($user->email, 'Welcome dear user...');
    }
}

حال اگر تصمیم بگیریم که در پروژه بجای Gmail از سرویس ایمیل Mailgun استفاده کنیم تنها کافیست متد register در کلاس App/Providers/AppServiceProvider را بصورت زیر تغییر دهیم.

$this->app->bind(Email::class, Mailgun::class);

اصل Open-closed در SOLID

اصل Open-closed (باز-بسته) در در برنامه نویسی شی گرا و اصول SOLID، بر باز بودن ماژول ها برای گسترش یافتن و بسته بودن آنها برای اصلاح تاکید دارد. به بیان ساده تر، ماژول های برنامه نویسی باید طوری طراحی شوند که گسترش آنها راحت باشد تا گسترش به اصلاح ترجیح داده شود.

برای مثال، کلاس زیر مسئول ساخت کد ورود کاربران، ذخیره و ارسال آن به شماره همراه کاربر است.

class Otp {
    public function make(User $user) {
        $code = andom_int(1000, 9999);

        $userOtp = new UserOtp();
        $userOtp->user_id = $user->id;
        $userOtp->code = $code;
        $userOtp->save();

        app(Sms::class)->send($user->cellphone, $code);
    }
}

مشکل کلاس بالا وقتی مشخص می‌شود که مثلا به دلیلی مانند افزایش سرعت سایت، تصمیم بگیریم به جای ذخیره کد های OTP در دیتابیس (MySQL)، آنها را در Redis ذخیره کنیم. آنگاه به ناچار باید کلاس بالا را اصلاح کنیم و اصل Open-closed از اصول SOLID نقض می‌شود.

حال کلاس بالا را بازنویسی می‌کنیم و کلاس و Interface های جدیدی معرفی می‌کنیم تا ماژول نهایی از اصل Open-closed از اصول SOLID پیروی کرده باشد.

interface OtpStorage {
    function save(string $cellphone, int $code);
}
class MySQL implements OtpStorage {
    function save(string $cellphone, int $code) {
         $userOtp = new UserOtp();
         $userOtp->user_id = $user->id;
         $userOtp->code = $code;
         $userOtp->save();
    }
}
class Redis implements OtpStorage {
    function save(string $cellphone, int $code) {
        Redis::put($cellphone, $code);
    }
}
class Otp {
    public function make(User $user) {
        $code = random_int(1000, 9999);
        app(OtpStorage::class)->save($user->id, $code);
        app(Sms::class)->send($user->cellphone, $code);
    }
}

حال برای تغییر روش ذخیره سازی کد های ورود کاربران از MySQL به Redis، تنها کافیست که Binding آن در متد register از کلاس App/Providers/AppServiceProvider از

$this->app->bind(OtpStorage::class, MySQL::class);

به صورت زیر تغییر کند:

$this->app->bind(OtpStorage::class, Redis::class);

و دیگر هیچ تغییری در کلاس Otp نخواهیم داشت. اکنون کلاس Otp قابلیت گسترش یافتن (افزودن روش های ذخیره سازی جدید) را دارد اما نیازی به اصلاح شدن ندارد.

اصول ISP ،SRP و LSP در SOLID

اصل SRP یا Single-responsibility principle (تک مسئولیتی) در برنامه نویسی شی گرا و اصول SOLID، بر تک کاربرد (مسئولیت) بودن ماژول ها، کلاس ها و Interface ها تاکید دارد.

اصل ISP یا Interface segregation principle (تفکیک رابطه کاربری) در برنامه نویسی شی گرا و اصول SOLID، بر تعدد Interface ها و طراحی آنها بر اساس نیاز کاربر به جای طراحی یک Interface جامع تاکید می‌کند.

اصل LSP یا Liskov substitution principle (جایگزینی لیسکوف) در برنامه نویسی شی گرا و اصول SOLID، بر قابلیت جایگزین شدن Type با Subtype های آن Type تاکید دارد. بعنوان مثال مربع یک Subtype از مستطیل است، پس اگر این اصل در پروژه رعایت شده باشد، ماژولی که مستطیل را بعنوان ورودی قبول می‌کند باید بتواند مربع را هم به عنوان ورودی بپذیرد.

سه اصل ذکر شده را با یک مثال و در رابطه با استفاده صحیح از Interface ها شرح می‌دهیم.

برای مثال، Interface زیر را در نظر بگیرید:

interface Notification {
    function sendSms(string $to, string $content);
    fucntion sendEmail(string $to, string $contnet);
}

واضح ترین اصلی که در طراحی Interface بالا رعایت نشده، اصل Single-responsibility است. این Interface می‌تواند به دو Interface (یکی Email و دیگری Sms) تقسیم شود. برای رعایت این اصل باید Interface را تا جایی که امکان دارد خرد کنیم تا به Interface هایی برسیم که تنها یک مسئولیت دارند.

طبق اصل Interface segregation که مختص به طراحی Interface هاست نیز بر طراحی طبق نیاز کاربر تاکید دارد. برای مثال UsersController که در بخش های قبلی معرفی شد تنها به ارسال Email نیاز دارد اما این Interface قابلیت ارسال SMS هم در خود جای داده که بیشتر از نیاز کاربر (UsersController) است و در نتیجه این اصل هم از اصول SOLID نقض شده است.

مشکل بزرگتری که این Interface دارد به هنگام پیاده سازی آن مشخص می‌شود. واضح است که این Interface تنها پیاده سازی هایی را می‌تواند قبول کند که هم خدمات Email و هم خدمات SMS ارائه می‌دهد. برای مثال اگر بخواهیم پیاده ‌سازی مانند Gmail داشته باشیم که تنها متد sendEmail را می‌تواند پوشش دهد و متد sendSms را خالی رها کنیم آنگاه باید دقت کنیم که ماژولی که Gmail را به آن تزریق می‌کنیم تلاش نکند با استفاده از آن SMS ارسال کند. به بیانی، به همه ماژول هایی که به اینترفیس Notification نیاز دارند نمی‌توانیم Gmail را تزریق کنیم چرا که ممکن است به sendSms نیاز داشته باشد و اگر Notification را Type و Gmail را Subtype آن در نظر بگیریم، اصل Liskov substitution از اصول SOLID را نقض کرده ایم.

سخن پایانی

در این مقاله سعی کردیم کاربرد ها و نحوه استفاده صحیح از Interface ها را در PHP و Laravel شرح دهیم. برای شرح این موارد به اصول SOLID تکیه کردیم. هم Interface و هم اصول SOLID کاربرد های فراتری دارند که قطعا در یک مقاله کوتاه نمی‌توان جا داد. با این وجود امیدوارم این مقاله توانسته باشد شما را با این دو مهم در برنامه نویسی آشنا کرده باشد.