Abstract classها، abstract methodها و interfaceها

اگر مدتی باشه که با php کار می‌کنید، حتما راجع به classهای پایه، abstract classها، abstract methodها و interfaceها شنیده‌اید. بیشتر فریم‌ورک‌های مشهور، شما رو به کدنویسی با interfaceها تشویق می‌کنند، اما خب interface چه معنی‌ای می‌دهد و کی باید با abstract classها برنامه رو نوشت و یا کی باید یک abstract method را تعریف کرد؟

همه‌ی ما بارها تعاریف اینها رو خوانده‌ایم، اما نحوه استفاده‌شون رو شاید ندونیم، پس بهتره با یک مثال عملی پیش بریم. شما هیچ ماشین، حیوان یا هر چیز استعاری رو تو دنیای کد نمی‌بینید که بشه اونها رو یه مثال مناسب برای این تعاریف دونست، پس یه مثال کوچیک اما کاربردی و قابل پیاده سازی رو تعریف می‌کنیم تا درک کاملی از تعاریفمون پیدا کنیم؛ «اپلیکیشنی که نیاز دارد به کاربرانش ایمیل بفرستد.»

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


class BasicEmail 
{
    private $email;

    private $body;

    public function setEmail($email)
    {
        $this->email = $email;
    }

    public function setBody($body)
    {
        $this->body = $body;
    }

    protected function isValid()
    {
        return is_email($this->email) && strlen($this->body) > 0;
    }

    public function attempt()
    {
        if($this->isValid())
        {
            $this->send();
        }
    }

    public function send()
    {
        //use $email and $body to send email
    }
}

//elsewhere in your application

$email = new BasicEmail;
$email->setEmail('johnDoe@gmail.com');
$email->setBody('Thanks for joining CatLoverApp, the social media platform to show off your cat!');
$email->attempt();

خب، ما از برنامه‌نویسی شی‌گرا استفاده کردیم و حالا ادامه‌ش میدیم. کاربران شما می‌خوان که وقتی دوستانشون تصاویر گربه‌هاشون رو پست کردند، ایمیلی دریافت کنند. همه چیز به نظر خوب میاد، جز زمانی که برخی از کاربران تقاضای دریافت SMS هم علاوه بر ایمیل داشته باشند. پس یه کلاس جدید داریم:


class SMS 
{
    private $phone;

    private $text;

    public function setPhone($phone)
    {
        $this->phone = $phone;
    }

    public function setText($text)
    {
        $this->text = $text;
    }

    protected function isValid()
    {
        return is_phone($this->phone) && strlen($this->text) > 0;
    }

    public function attempt()
    {
        if($this->isValid())
        {
            $this->send();
        }
    }

    public function send()
    {
        //use Twilio to send SMS
    }
}

//elsewhere in your application

$sms = new SMS;
$sms->setPhone('123-456-7890');
$sms->setText('Bob, just posted a pic of his cat!');
$sms->attempt();

خب حالا می‌تونیم به کاربرامون ایمیل و SMS بفرستیم اما کلاس‌هامون خیلی ساده‌اند و در عین حال کاملا رضایت‌بخش هم نیستند. هر دو دارای destinationو text و ()isValidو()attempt و متود ()send هستند. این واقعا مثال خوبیه برای فهم بهتر یک abstract class:


abstract class Message 
{
    private $destination;

    private $content;

    public function setDestination($destination)
    {
        $this->destination = $destination;
    }

    public function setContent($content)
    {
        $this->content = $content;
    }

    public function attempt()
    {
        if($this->isValid()) {
            $this->send();
        }
    }

    abstract protected function isValid();

    abstract public function send();
}

class BasicEmail extends Message 
{
    protected function isValid()
    {
        return is_email($this->destination) && strlen($this->content) > 0;
    }

    public function send()
    {
        //use $this->destination and $this->content to send email
    }
}

class SMS extends Message 
{   
    protected function isValid()
    {
        return is_phone($this->destination) && strlen($this->content) > 0;
    }

    public function send()
    {
        //use $this->destination and $this->content to send SMS
    }
}

//

$message = new Message; //Invalid, won't work because it's abstract!

$message1 = new BasicEmail;
$message1->setDestination('johnDoe@gmail.com');
$message1->setContent('Sorry, Susan just de-friended you!');
$message1->attempt();

$message2 = new SMS;
$message2->setDestination('123-456-7890');
$message2->setContent('Sorry, Susan just de-friended you!');
$message2->attempt();

ما وقتی از abstract classها استفاده می‌کنیم که objectهای مشابه داشته باشیم که برخی قابلیت‌ها را تفاوت‌های خیلی جزئی به اشتراک بزارن یا انجامش بدن. در کلاس Message ما یه زوج از abstract methodها تعریف کردیم و یک اجبار برای همه کلاسهایی که از Message توسعه داده میشن، بوجود میاریم تا اجبارا متودهای ()send و ()isValid رو پیاده‌سازی کنند. کلاس Message به این که چطوری ارسال و اعتبار‌سنجی انجام میشه، کاری نداره، فقط براش مهمه که این قابلیت وجود داشته باشه. ذکر این نکته هم خوبه که abstract classها و interfaceها می‌تونن متودها رو با انواعی از visibility تعریف کنن، مثله public، protected و یا private. اینجا ()isValid شاید بتونه public هم باشه. فقط یه نکته رو بگم:

اخطار! شما نمی‌تونید یک abstract class را instantiate کنید. چون خیلی مبهم هستند و این یعنی باید اونها رو توسعه بدید.

نکته! اگر در یک کلاس، یک متود abstract تعریف شده باشه، خود کلاس حتما باید abstract باشه، با این حال میشه کلاسی رو abstract تعریف کرد، بدون اینکه متود abstract داشته باشه، که اگرچه غیرمعموله، اما غیرمجاز نیست.

صف پیام‌ها

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

حالا چند نوع از صف پیام‌ها را ایجاد می‌کنیم. اولین ارسال پیام می‌تونه اینطوری باشه:


$messages = array();

$messages[] = $message1;
$messages[] = $message2;

/** other application code **/

foreach ($messages as $message) {
    $message->attempt();    
}

درست کار می‌کنه، اما می‌تونه خیلی بهتر باشه. فرض کنید یکی از همکارانتون، messages$ رو ببینه و به اون $messages[] = "You've been awarded a badge!"? را اضافه کنه، چی میشه؟ مشکل پیش میاد. ممکنه اون نتونسته باشه تیکه کد ارسال پیام‌ها رو ببینه و ندونه تابع ()attempt چه چیزی رو به عنوان ورودی انتظار داره. برای همین، و رفع این مشکل بازسازی زیر رو انجام می‌دیم:


class MessageQueue 
{
    private $messages = array();

    public function add(Message $message)
    {
        $this->messages[] = $message;
    }

    public function sendAll()
    {
        foreach ($this->messages as $message) {
            $message->attempt();
        }
    }
}

به نظرم خیلی بهتر شد. ما تابع ()add رو type-hinting اضافه کردیم تا همکارانمون بدونن آبجکت‌های Message (چه BasicEmail و چه SMS) قابل پذیرش هستند.

Extending functionality

من ایده‌ی MessageQueue رو دوست دارم. اصالتا کاری نداره که چه نوع پیامی رو باید بفرسته، فقط نیاز می‌دونه که اونها رو ارسال کنه. خب حالا می‌تونیم واقعا از کاربرامون بخوایم که از سرویسمون استفاده کنن و برای هر تصویری که از هر گربه‌ای می‌زارن، ۵ واحد جایزه بگیرند.


class PhotoReward 
{
    private $amount;

    private $recipient;

    public function setAmount($amount)
    {
        $this->amount = $amount;
    }

    public function setRecipient($recipient)
    {
        $this->recipient = $recipient;
    }

    public function reward()
    {
        //use paypal to send reward to recipient
    }
}

به نظر می‌رسه شرایط Message رو نداره، اما چطور میشه اگر بتونیم تو Queue ازش استفاده کنیم؟ Interfaceها. یک interface نیازمندی‌های مشخصی رو به کلاس‌ها اعمال می‌کنه که فرم مشخصی از متودها پیاده شوند، مانند abstract methodها با این تفاوت که interface هیچ پیاده‌سازی از کد ندارد. یک مزیت استفاده از interfaceها این است که می‌تونیم اون رو به کلاس‌های موجودمون، متصل کنیم. یک کلاس فقط می‌تونه از یک کلاس دیگه پیاده‌سازی بشه یعنی فقط می‌تونه یک والد داشته باشه. در حالی که میتونن تعداد بیشماری از interfaceها رو پیاده سازی کنند.

حقیقت اینه که Interfaceها می‌توانند به وسیله دیگر interfaceها extend بشوند. من فکر نمیکنم یه همچین موردی رو تا حالا دیده باشم، اما به هر حال امکان پذیر هست. معمولا خیلی واضح‌تره اگر کلاس‌هاتون از چندین interface پیاده‌سازی شوند.

اکنون یک interface event تعریف می‌کنیم.


interface Event 
{
    public function fire();
}

همینطور که می‌بینید، هیچ تعریفی برای متود ()fire وجود ندارد، هر کلاسی که از این interface استفاده یا پیاده سازی کنه، مجبور خواهد بود که متود ()fire را تعریف کند. اکنون وقت بازسازی کلاس queue است. می‌خواهیم با استفاده از interface event آنرا کمی عمومی کنیم.


class EventQueue 
{
  private $events = array();

  public function add(Event $event)
  {
    $this->events[] = $event;
  }

  public function fireAll()
  {
    foreach ($this->events as $event) {
      $event->fire();
    }
  }
}

حالا classهای Message و PhotoReward را با استفاده از interface بازسازی می کنیم.


abstract class Message implements Event 
{
    private $destination;

    private $content;

    public function setDestination($destination)
    {
        $this->destination = $destination;
    }

    public function setContent($content)
    {
        $this->content = $content;
    }

    public function attempt()
    {
        if($this->isValid()) {
            $this->send();
        }
    }

    public function fire()
    {
        $this->attempt();
    }

    abstract protected function isValid();

    abstract public function send();
}

class PhotoReward implements Event 
{
    private $amount;

    private $recipient;

    public function setAmount($amount)
    {
        $this->amount = $amount;
    }

    public function setRecipient($recipient)
    {
        $this->recipient = $recipient;
    }

    public function fire()
    {
        $this->reward();
    }

    public function reward()
    {
        //use paypal to send reward to user
    }
}

چند نکته هست که اینجا باید بگیم:

متوجه خواهید شد که ما نیازی به کلاس BasicEmail و SMS نداریم. ما interface را در کلاس abstract والد Message ست می‌کنیم. متود جدید ()fire تنها متود ()attempt را شامل می‌شود. این ممکن است به نظر بی فایده باشد، اما اکنون ما قادر هستیم که این کلاس رویدادها را فراخوانی کنیم.

ما می توانیم interface را در Message نگه داشته و متود ()fire را برداریم، زیرا Message یک کلاس abstract است. در تمام مدت پیاده سازی، BasicEmail و SMS، متود ()fire خود را پیاده می‌کنند، به interface نیز پایبند خواهند بود. کلیه الزامات abstract در نهایت به پیاده سازی های پایه‌ای آنها بستگی دارد.

ما یک interface را در کلاس abstract و یک کلاس پایه پیاده سازی کردیم و هر دو نیز معتبر هستند.

بزارید ببینیم حالا چه می‌توانیم بکنیم؟


$queue = new EventQueue;

$event1 = new BasicEmail;
$event1->setDestination('johnDoe@gmail.com');
$event1->setContent('Sorry, Susan just de-friended you!');

$queue->add($event1);

$event2 = new SMS;
$event2->setDestination('123-456-7890');
$event2->setContent('Sorry, Susan just de-friended you!');

$queue->add($event2);

$event3 = new PhotoReward;
$event3->setRecipient($bob);
$event3->setAmount(5);

$queue->add($event3);

$queue->fireAll();

از آنجا که هر شیء به interface رویداد پایبند است، می‌توانیم آنها را به queue اضافه کنیم و queue می‌تواند به متود ()fire همه آن objectها متصل شود.

Inception

می‌خواهیم ابعاد جدیدی به EventQueue اضافه کنیم. توجه کنید که ما یک متود ()fireAll داریم. این کلاس نیز یک event محسوب می شود. بنابراین وقت آن است که واقعا از برخی از مزایای برنامه نویسی شی گرا استفاده کنیم.

EventQueue جدید و بهبود یافته ما می تواند چنین چیزی باشد.


class EventQueue implements Event 
{
    private $events = array();

    public function add(Event $event)
    {
        $this->events[] = $event;
    }

    public function fire()
    {
        foreach ($this->events as $event) {
            $event->fire();
        }
    }
}

حالا باید بدونیم که این مفاهیم برای چی به وجود آمدند؟یک سازمان و خواسته‌هایش شاید بهترین تشریح برای ایجاد این مفاهیم باشد.


$messageQueue = new EventQueue;
$rewardQueue  = new EventQueue;

$email = new BasicEmail;
$email->setDestination('johnDoe@gmail.com');
$email->setContent('Congrats, you\'re the cat lover of the month!');

$messageQueue->add($email);

$sms = new SMS;
$sms->setDestination('123-456-7890');
$sms->setContent('Congrats, you\'re the cat lover of the month!');

$messageQueue->add($sms);

$photoReward = new PhotoReward;
$photoReward->setRecipient($bob);
$photoReward->setAmount(5);

$rewardQueue->add($photoReward);

$badgeReward = new BadgeReward;
$badgeReward->setRecipient($spencer);

$rewardQueue->add($badgeReward);

$masterQueue = new EventQueue;
$masterQueue->add($messageQueue);
$masterQueue->add($rewardQueue);

//fire events in both queues
$masterQueue->fire();

//or only fire events in $messageQueue
$messageQueue->fire();

//or only fire events in $rewardQueue
$rewardQueue->fire();

اکنون ما می توانیم هر treat را به صورت جداگانه یا همه را در یک زمان اداره کنیم. در حال حاضر این امکان که صف ها را داخل یکدیگر قرار دهیم وجود دارد.

یک اثر جانبی استفاده از abstract classها و interfaceها، داشتن کد بسیار مدولار و قابل توسعه است. برای ایجاد یک Message جدید، ما فقط باید کلاس Message را extend کنیم و متود های ()send و ()isValid را پیاده کنیم.

کامنت ها (0)

ارسال نظر