آموزش اصل Open Closed از اصول SOLID

یکی از مهم‌ترین اصول طراحی نرم‌افزار اصل Open-Closed است. این اصل به بهبود نگهداری و مقیاس‌پذیری کد کمک می‌کند. به‌طور خلاصه، اصل باز-بسته می‌گوید که یک سیستم (و یک کلاس) باید برای توسعه باز و برای تغییر بسته باشد. در این آموزش به زبان ساده و مثال‌ها کد کاربردی، این اصل که اصل دوم SOLID است را بررسی می‌کنیم.

رعایت استانداردها و اصولی که توسط اکثر برنامه‌نویس‌ها پذیرفته شده است، به ما کمک می‌کند تا کدهای بهینه، منظم و قابل درک‌تری بنویسیم. اینکه فکر کنیم فقط کافی است کدی بنویسیم که کار کند، تفکر اشتباهی است! کدهای نامرتب و بی‌کیفیت موجب هزینه‌برتر و زمان‌برتر شدن فرآیند توسعه نرم‌افزار می‌شود.

اصول پنج‌گانه SOLID یکی از اصولی است که بسیار پیشنهاد می‌شود در برنامه‌نویسی به آن توجه کنیم. در این آموزش درباره اصل Open-Closed (که ممکن است گاهی در فارسی اصل باز-بسته هم بشنوید) بحث می‌کنیم. این اصل قانون دوم از اصول SOLID (حرف O) است.

اصل Open Closed چیست؟

اصل Open Closed می‌گوید که «یک کلاس باید برای توسعه باز و برای تغییر بسته باشد». به این معنی که باید بتوانیم ویژگی‌های جدیدی را به سیستم اضافه کنیم بدون آن‌که در کدهای موجود تغییر ایجاد شود.

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

این اصل به کاهش Coupling (وابستگی) و افزایش Cohesion (همبستگی/انسجام) در سیستم کمک می‌کند. اگر با این دو مفهوم آشنا نیستید، خیلی کوتاه توضیح می‌دهم:

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

به‌طور خلاصه، اصل Open Closed در برنامه‌نویسی می‌گوید که موجودیت‌های نرم‌افزاری (نظیر کلاس‌ها، ماژول‌ها و توابع) باید برای توسعه باز باشند اما برای اصلاح بسته. یعنی بتوان آن‌ها را توسعه داد بدون آنکه تغییری در کدهای قدیمی (کدهای فعلی که در حال کار هستند) بدهیم.

مثال استفاده از اصل Open Closed

برای درک بهتر این موضوع، مثالی از یک سیستم پرداخت می‌زنم. در ابتدا کد را طوری می‌نویسم که این اصل را رعایت نکند و سپس آن را اصلاح می‌کنم.

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

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

<?php
class PaymentProcessor {
    public function process_payment($payment_gateway) {
        if ($payment_gateway == 'saman') {
            echo "Processing payment through Saman.";

        } elseif ($payment_gateway == 'mellat') {
            echo "Processing payment through Mellat.";

        } elseif ($payment_gateway == 'parsian') {
            echo "Processing payment through Parsian.";

        } else {
            throw new InvalidArgumentException("Unsupported payment gateway");
        }
    }
}

در این مثال، هر بار که بخواهیم روش پرداخت جدیدی به سیستم اضافه کنیم، باید متد process_payment() را تغییر دهیم. در حقیقت باید یک شرط به ساختار شرطی آن اضافه کرده و کدهای مربوط به درگاه جدید را بنویسیم. این روش ناقض اصل Open Closed است.

در این آموزش از کدهای PHP استفاده می‌کنم. مفهوم اصل Open Closed در تمام زبان‌های برنامه‌نویسی شیءگرا قابل پیاده‌سازی است. مفهوم آن یکی است. بنابراین می‌توانید آن را در برنامه‌نویسی با زبان پایتون یا جاوا یا هر زبان دیگری پیاده‌سازی کنید.

اصلاح کد با رعایت اصل باز-بسته

برای پیاده‌سازی اصولی این سیستم، از پلی‌مورفیسم (Polymorphism) و وراثت (Inheritance) استفاده می‌کنیم.

ابتدا یک اینترفیس به‌نام PaymentGateway ایجاد می‌کنیم. البته ممکن است به‌جای interface از یک کلاس انتزاعی (Abstract) نیز استفاده شود.

<?php
interface PaymentGateway {
    public function process();
}

سپس برای هر درگاه (یا نوع روش پرداخت) صرفاً لازم است یک کلاس ایجاد کنیم تا از این اینترفیس پیروی کرده یا از کلاس Abstract ارث‌بری کرده باشد.

همان‌طور که می‌دانید، interface در برنامه‌نویسی، شبیه یک قرارداد است. هر کلاسی که از آن پیروی کرده یا یک کلاس abstract را پیاده‌سازی کند، مجبور است متدهای آن را نیز دقیقاً با همان نام پیاده‌سازی کند. بنابراین در این مثال، تمام کلاس‌های درگاه‌های ما حتماً متد process() را خواهند داشت.

برای سه درگاه مثال بالا، باید کلاس‌های زیر را ایجاد کنیم:

<?php
class SamanGateway implements PaymentGateway {
    public function process() {
        echo "Processing payment through Saman bank.";
    }
}

class MellatGateway implements PaymentGateway {
    public function process() {
        echo "Processing payment through Mellat bank.";
    }
}

class ParsianGateway implements PaymentGateway {
    public function process() {
        echo "Processing payment through Parsian bank.";
    }
}

حالا کلاس PaymentProcessor را به‌گونه‌ای تغییر می‌دهیم تا با استفاده از مفهوم پلی‌مورفیسم، هر روش پرداخت را به‌طور مستقل پردازش کند:

<?php
class PaymentProcessor {
    public function process_payment(PaymentGateway $payment_gateway) {
        $payment_gateway->process();
    }
}

اکنون اگر بخواهیم یک روش پرداخت جدید (مثلاً پرداخت با ارز دیجیتال یا پرداخت با چک) را به سیستم اضافه کنیم، فقط کافی است یک کلاس جدید برای این روش بنویسیم که از PaymentGateway ارث‌بری کرده و متد process() را پیاده‌سازی کرده باشد.

مثلاً در قطعه کد زیر، یک روش پرداخت جدید (پرداخت با بیت کوین) اضافه کرده‌ام:

<?php
class BitcoinGateway implements PaymentGateway {
    public function process() {
        echo "Processing payment through Bitcoin.";
    }
}

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

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

اصل Single Responsibility در برنامه‌نویسی به زبان ساده

اصل Single Responsibility در برنامه‌نویسی به زبان ساده

چرا اصل Open Closed اهمیت دارد؟

به‌طور کلی سه مورد زیر را می‌توان جزء مزایای رعایت اصل Open Closed در برنامه‌نویسی در نظر گرفت:

  • توسعه آسان‌تر: زمانی که نیاز به افزودن ویژگی‌ها و امکانات جدیدی داریم، کدهای قدیمی یا تغییری نمی‌کنند یا به میزان بسیار کمی نیازمند ویرایش خواهند بود.
  • کاهش خطا: تغییرات کدهای موجود ممکن است باعث مشکلات ناخواسته‌ای شود. مثلاً قطعه کدی که در جای دیگر از این کلاس ما استفاده کرده هم باید حتماً تغییر کند و اگر این موضوع را فراموش کنیم، آن بخش از برنامه با خطا مواجه خواهد شد. این اصل از ایجاد مشکلات احتمالی ناشی از تغییرات کد جلوگیری می‌کند.
  • قابلیت نگهداری بهتر: با رعایت قانون Open Closed از اصول SOLID در برنامه‌نویسی، کدهای ما قابلیت اصلاح و توسعه‌ی بهتری خواهند داشت. عملاً می‌توانیم سیستم را با کمتر بازنویسی کدهای قدیمی، اصلاح کرده یا توسعه دهیم.
مثال انتزاعی و تصویری از اصل OCP یا باز-بسته در برنامه‌نویسی
مثال انتزاعی و تصویری از اصل OCP یا باز-بسته در برنامه‌نویسی

مثال دیگر برای این اصل

فرض کنید می‌خواهیم سیستمی برای ارسال اعلان‌ها (Notifications) ایجاد کنیم. یکی از اولین راه حل‌هایی که به ذهنمان می‌رسد، استفاده از یک ساختار شرطی است. در این شرط روش موردنظر برای ارسال اعلان را بررسی کرده و متناسب با آن کدهای موردنظر را اجرا می‌کنیم.

در قطعه کد زیر، در بدنه‌ی هر شرط، صرفاً یک echo ساده قرار داده‌ام. اما ممکن است چندین خط کد برای ارسال ایمیل با PHP داشته باشیم یا صرفاً یک تابع یا متد را برای آن فراخوانی کنیم.

<?php
class NotificationSender {
    public function send_notification($method, $message, $receiver) {
        if ($method == 'sms') {
            echo "Sending SMS: {$message} to {$receiver}";

        } elseif ($method == 'email') {
            echo "Sending Email: {$message} to {$receiver}";

        } else {
            throw new InvalidArgumentException("Unsupported notification method");
        }
    }
}

در این روش، مشابه مثال قبل، اگر بخواهیم روش ارسال اعلان جدیدی اضافه کنیم، باید در کدهای همین کلاس و متد send_notification() تغییراتی ایجاد کنیم.

پیشنهاد می‌کنم قبل از این‌که کدهای اصلاح‌شده را در ادامه ببینید، کمی فکر کنید ببینید کدی که منطبق بر اصل Open Closed است باید چگونه باشد.

یک حالت مناسب برای پیاده‌سازی این بخش از سیستم، تعریف یک اینترفیس برای روش ارسال است. این اینترفیس دارای یک متد است که حاوی کدهای ارسال اعلان خواهد بود.

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

<?php
interface NotificationMethod {
    public function send($message, $receiver);
}

class SMSNotification implements NotificationMethod {
    public function send($message, $receiver) {
        echo "Sending SMS: $message";
    }
}

class EmailNotification implements NotificationMethod {
    public function send($message, $receiver) {
        echo "Sending Email: $message";
    }
}

در نهایت، کلاس NotificationSender را به‌صورت بازنویسی می‌کنیم که بدون نیاز به تغییر کدهایش در آینده، بتواند از تمام روش‌های اعلانی که از اینترفیس NotificationMethod پیروی کرده‌اند استفاده کند:

<?php
class NotificationSender {
    public function send_notification(NotificationMethod $method, $message, $receiver) {
        $method->send($message, $receiver);
    }
}

جمع‌بندی آموزش

در این آموزش با اصل باز-بسته در برنامه‌نویسی و کاربردهای آن در طراحی کدهای قابل توسعه و نگهداری آشنا شدیم. اصل OCP (مخفف Open-Closed Principle)  می‌گوید که کد باید برای گسترش (توسعه) باز و برای تغییر بسته باشد. استفاده از مفاهیم پلی‌مورفیسم و وراثت راهی مناسب برای دستیابی به این هدف است.

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

مدتی بعد، Robert C. Martin (+) که به عمو باب هم معرف است، از اصل Open Closed در برنامه نویسی با عنوان مهم‌ترین اصل طراحی شیءگرا نام برده و پیشنهاد داده است که به‌جای سوپرکلاس (کلاس والد) از interface استفاده کنیم. دقیقاً کاری که در این آموزش انجام دادیم، راه‌حل نهایی و پذیرفته‌شده‌ی عمو باب بود.

اگر با این مفاهیم در برنامه‌نویسی شیءگرا آشنا نیستید، پیشنهاد می‌کنم سعی کنید از همین امروز بیشتر با آن‌ها آشنا شوید. همچنین دوره مفاهیم شیءگرایی را نیز به شما پیشنهاد می‌کنم. امیدوارم این آموزش به شما کمک کرده باشد تا اصل Open Closed را بهتر درک کرده و بتوانید از این پس آن را در پروژه‌های خود به کار بگیرید.