برنامه نویسی

عملکرد دکوراتور مانند در تایپ اسکریپت در PHP

هنگام کار با PHP، من می‌خواستم رفتار متدهای کلاس را تغییر دهم، با هدف دستیابی به یک متد دکوراتور شبیه به دکوراتورهای متد Typescript. من به دنبال عملکردی شبیه به دکوراتور گشتم اما چیزی مناسب پیدا نکردم، بنابراین با ایجاد یک کارکرد آزمایش کردم. در این وبلاگ، یافته‌های خود را به اشتراک می‌گذارم و نشان می‌دهم که چگونه می‌توان به a روش دکوراتور مانند عملکردی در PHP، مشابه آنچه در TypeScript وجود دارد.

دکوراتور چیست

در TypeScript، decorator نوعی اعلان خاص است که می تواند به یک کلاس، متد، دسترسی، ویژگی یا پارامتر متصل شود. دکوراتورها راهی برای اضافه کردن حاشیه‌نویسی و ابرداده به هدفی که به آن متصل شده‌اند ارائه می‌کنند.

این نمونه ای از متد تایپ اسکریپت دکوراتور در عمل است:

export function simpleDecorator(): MethodDecorator {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      try {
        console.log("[Before trigger method] Hello from decorator");
        const result = await originalMethod.apply(this, args);
        console.log("[After trigger method] Hello from decorator");
        return result;
      } catch (err: any) {
        // error handler
        throw err;
      }
    };
  };
}

class ServiceA {
  @simpleDecorator()
  public methodA() {
    console.log("Hello From Method");
  }
}

const serviceA = new ServiceA();
serviceA.methodA();
// Output:
// [Before trigger method] Hello from decorator
// Hello From Method
// [After trigger method] Hello from decorator
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چرا دکوراتور؟

چرا_دکوراتور

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

  1. قابلیت استفاده مجدد کد و اصل DRY
    • قابلیت استفاده مجدد: دکوراتورها به شما این امکان را می دهند که تکه های کد قابل استفاده مجدد را تعریف کنید که می توانند در چندین کلاس یا روش اعمال شوند. این کار نیاز به نوشتن منطق تکراری در چندین مکان را از بین می برد.
    • اصل DRY: با انتزاع کارکردهای رایج در دکوراتورها، شما به اصل “تکرار نکنید” پایبند هستید و تکرار کد را کاهش می دهید و پایگاه کد خود را تمیزتر و نگهداری آسان تر می کند.
  2. جداسازی نگرانی ها دکوراتورها به تفکیک نگرانی های مقطعی (مانند ورود به سیستم، مجوز، اعتبارسنجی و غیره) از منطق تجاری کمک می کنند. این باعث می شود منطق اصلی شما متمرکز و سرراست باشد.
  3. خوانایی و نگهداری پیشرفته
    • خوانایی: استفاده از دکوراتورها مشخص می کند که چه رفتارها یا ابرداده های اضافی با یک کلاس یا روش مرتبط است. این می تواند کد را خواناتر و مستندتر کند.
    • قابلیت نگهداری: از آنجایی که دکوراتورها رفتارهای خاصی را در بر می گیرند، هر گونه تغییر در این رفتارها را می توان در یک مکان (خود دکوراتور) به جای پراکنده شدن در کل پایگاه کد ایجاد کرد.

یافته ها و آزمایش های من

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

راه حل یک

class SimpleDecorator
{
    private $target;
    public function __construct($target)
    {
        $this->target = $target;
    }

    public function __call($method, $args)
    {
        echo "[Decorator:Before] Hello\n";
        $result = call_user_func_array(array($this->target, $method), $args);
        echo "\n[Decorator:After] Hi\n";
        return $result;
    }
}

class ServiceA
{
    public function methodA()
    {
        echo "Method Triggered";
    }
}

$serviceA = new SimpleDecorator(new ServiceA());
$serviceA->methodA();
// Output:
// [Decorator:Before] Hello
// Method Triggered
// [Decorator:After] Hi
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای دستیابی به یک تابع دکوراتور مانند به یکی از متدهای جادویی کلاس php نیاز داریم که این است __call متد، این متد زمانی فعال می‌شود که متدی که فراخوانی می‌شود، تعریف نشده باشد و نام متد و آرگومان‌های ارسالی آن را بیاورد.

در مثال بالا می بینید که دکوراتور ما پارامتری به نام را می پذیرد $target، که انتظار می رود کلاسی باشد که دکوراتور برای آن اعمال می شود.

در اجرا، ما سعی می کنیم تماس بگیریم methodA، یکی از روش های کلاس ServiceA از طریق دکوراتور SimpleDecorator که ندارد methodA، این باعث می شود __call و به ما امکان می دهد از اطلاعاتی که برای تماس ارسال می کند استفاده کنیم methodA از کلاس هدف که هست ServiceA.

یکی از ویژگی های دکوراتور این است که می تواند روی هم چیده شود، بیایید ببینیم چگونه می توانیم دکوراتورها را روی هم قرار دهیم:

class AnotherSimpleDecorator
{
    private $target;
    public function __construct($target)
    {
        $this->target = $target;
    }

    public function __call($method, $args)
    {
        echo "\n[AnotherDecorator:Before] Hello\n";
        $result = call_user_func_array(array($this->target, $method), $args);
        echo "[AnotherDecorator:After] Hi\n";
        return $result;
    }
}
$serviceA = new AnotherSimpleDecorator(new SimpleDecorator(new ServiceA()));
$serviceA->methodA();
// [AnotherDecorator:Before] Hello
// [Decorator:Before] Hello
// Method Triggered
// [Decorator:After] Hi
// [AnotherDecorator:After] Hi
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مثال، ما پشته AnotherSimpleDecorator در بالای SimpleDecorator که به ServiceA.
هنگامی که چندین تزئین کننده اعمال می شوند، ارزیابی آنها شبیه ترکیب تابع در ریاضیات است.

به این ترتیب، مراحل زیر هنگام ارزیابی چند دکوراتور انجام می شود:

  1. عبارات هر دکوراتور از بالا به پایین ارزیابی می شود (در این مورد از چپ به راست است).
  2. سپس نتایج به عنوان توابع از پایین به بالا فراخوانی می شوند (در این مورد از چپ به راست است).

این راه حل دکوراتور را برای همه روش ها اعمال می کند، اما چگونه می توانیم مشخص کنیم که چه روش هایی باید توسط دکوراتور اعمال شود یا رفتار دکوراتور را سفارشی کنیم؟ خوب، ما می توانیم این کار را در این راه حل انجام دهیم، اما به این معنی است که شما باید همان سفارشی سازی را برای همه دکوراتورها اعمال کنید و این باعث می شود که نقاط ضعف در این راه حل شود. چرا دکوراتور؟ بخش، به همین دلیل راه حل دو به کمک می آید.

کد کامل Solution One: اینجا

راه حل دو

ما این ایده را از راه حل یک گرفتیم، اجازه دهید آن را پیشرفته تر کنیم.

class BaseDecorator
{
    private $target;
    private $include_methods = [];
    public function __construct($target, array $include_methods = [])
    {
        $this->target = $target;
        $this->include_methods = $include_methods;
        $this->registerMethods();
    }

    private function registerMethods()
    {
        if (!method_exists($this->target, "handler"))
            throw new Exception("Undefined Decorator Handler \n");

        $methods = get_class_methods($this->target);
        foreach ($methods as $method) {
            $include_method = in_array($method, $this->include_methods);
            if ($include_method)
                runkit7_method_rename(get_class($this->target), $method, "__$method");
        }
    }

    public function __call($method, $args)
    {
        if (!method_exists($this->target, "__$method"))
            throw new Exception("Call attempt on undefined method: $method\n");

        $original_method = function () use ($method, $args) {
            return call_user_func_array(array($this->target, "__$method"), $args);
        };

        return call_user_func_array(array($this->target, "handler"), [$original_method]);
    }
}

class SimpleDecorator extends BaseDecorator
{
    public function __construct($target, array $include_methods = [])
    {
        parent::__construct($target, $include_methods);
    }

    public function handler($original_method)
    {
        echo "[Decorator:Before] Hello\n";
        $result = $original_method();
        echo "\n[Decorator:After] Hi\n";
        return $result;
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون داریم BaseDecorator این باعث می شود که ما قدرت کنترل رفتار دکوراتور را داشته باشیم. روی پارامتر دوم که اضافه می کنیم $include_methods به عنوان یک پارامتر پیکربندی برای رسیدگی به روشی که دکوراتور باید اعمال شود.

همچنین اضافه کردیم registerMethods به BaseDecorator، این روش دو هدف را دنبال می کند:

  1. این روش تضمین می کند که هدف هر دکوراتور روشی به نام دارد handler، که به عنوان منطق دکوراتور عمل می کند، روش کنترل کننده دارای یک پارامتر است $original_method، نشان دهنده متد اصلی است که کلاس سعی در فراخوانی آن دارد.
  2. این روش نام همه متدها را از the تغییر می دهد $target کلاسی که مطابقت دارد $include_methods پیکربندی با پیشوند آنها با “__”، با کمک runkit7 افزونه. این تضمین می‌کند که وقتی متدی از یک کلاس با دکوراتور اعمال‌شده فراخوانی می‌شود، باعث راه‌اندازی آن می‌شود __call روش.

توجه داشته باشید:
من از php 7.4 و runkit7-4.0.0a6 استفاده می کنم

اجازه بدید ببینم SimpleDecorator در عمل:

class ServiceA extends SimpleDecorator
{
    public function __construct()
    {
        parent::__construct($this, ['methodA']);
    }

    public function methodA()
    {
        echo "MethodA Triggered";
    }

    public function methodB()
    {
        echo "\nMethodB Triggered\n";
    }
}

$serviceA = new ServiceA();
$serviceA->methodA();
// Output:
// [Decorator:Before] Hello
// MethodA Triggered
// [Decorator:After] Hi

$serviceA->methodB();
// Output:
// MethodB Triggered
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

وقتی سعی کنید استفاده کنید SimpleDecorator ما فقط به دکوراتور نیاز داریم که روی آن اعمال شود methodA، از این رو خروجی.

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

کد کامل راه حل دو: اینجا

راه حل سه

راه حل سه تقریباً مانند راه حل دو است اما از همان رویکرد در راه حل یک استفاده می کند.

trait BaseDecorator
{
    public $__base_target = [
        "class_name" => "",
        "methods" => [],
    ];
    private $target;
    private $handler;
    private array $method_options = [];
    private array $include_methods = [];

    public function construct($target, callable $handler, array $include_methods = [])
    {
        $this->target = $target;
        $this->handler = $handler;
        $this->include_methods = $include_methods;

        $this->setupBaseTarget();
    }

    private function setupBaseTarget()
    {
        $not_settled = empty(@$this->target->__base_target['class_name'] ?? "");
        if ($not_settled) {
            $this->__base_target = [
                "class_name" => get_class($this->target),
                "methods" => get_class_methods($this->target),
            ];
        } else
            $this->__base_target = $this->target->__base_target;
    }

    public function __call($method, $args)
    {
        $base_target_methods = $this->__base_target['methods'];
        if (!in_array($method, $base_target_methods))
            throw new Exception("Call attempt on undefined method: $method\n");

        $original_method = function () use ($method, $args) {
            return call_user_func_array(array($this->target, $method), $args);
        };

        $use_handler = in_array($method, $this->include_methods);
        return $use_handler ? call_user_func($this->handler, $original_method) : $original_method();
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا تفاوت کلیدی بین راه حل سه و دو وجود دارد:

  1. handler به عنوان پارامتر نه به عنوان روش.
  2. BaseDecorator به عنوان یک trait به جای الف class، از آنجایی که ما از همان رویکرد در راه حل یک استفاده می کنیم، نیازی به تغییر نام روش برای ایجاد __call ایجاد می شود، بنابراین می توانیم حذف کنیم registerMethods.
  3. ما داریم setupBaseTarget، این روش مسئول مقداردهی اولیه یا کپی است $targetاطلاعات

به عنوان مثال هنگام استفاده از دکوراتورهای متعدد:

     $serviceA = new DecoratorB(new DecoratorA(new ServiceA()));
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

DecoratorB‘s $target است DecoratorA، setupBaseTarget اطمینان حاصل شود که تمام دکوراتورهایی که درخواست داده اند به اطلاعات کلاس هدف اصلی دسترسی دارند ServiceA.

بیایید یک دکوراتور به نام اعلام کنیم SimpleDecorator استفاده كردن BaseDecorator:

class SimpleDecorator
{
    use BaseDecorator;
    public function __construct($target, array $include_methods = [])
    {
        $handler = function ($original_method) {
            echo "[Decorator:Before] Hello\n";
            $result = $original_method();
            echo "[Decorator:After] Hi\n";
            return $result;
        };

        $this->construct($target, $handler, $include_methods);
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید از decarotor استفاده کنیم:


class ServiceA
{
    public function methodA()
    {
        echo "MethodA Triggered\n";
    }

    public function methodB()
    {
        echo "\nMethodB Triggered\n";
    }
}
$serviceA = new SimpleDecorator(new ServiceA(), ['methodA']);
$serviceA->methodA();
// Output:
// [Decorator:Before] Hello
// MethodA Triggered
// [Decorator:After] Hi

$serviceA->methodB();
// Output:
// MethodB Triggered
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

دکوراتورهای روی هم در حال عمل:

class AnotherSimpleDecorator
{
    use BaseDecorator;
    public function __construct($target, array $include_methods = [])
    {
        $handler = function ($original_method) {
            echo "[AnotherDecorator:Before] Hello\n";
            $result = $original_method();
            echo "[AnotherDecorator:After] Hi\n";
            return $result;
        };

        $this->construct($target, $handler, $include_methods);
    }
}
$serviceA = new AnotherSimpleDecorator(
    new SimpleDecorator(new ServiceA(), ['methodA']),
    ['methodA']
);
$serviceA->methodA();
// Output:
// [AnotherDecorator:Before] Hello
// [Decorator:Before] Hello
// Method Triggered
// [Decorator:After] Hi
// [AnotherDecorator:After] Hi
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

راه حل سه کد کامل: اینجا

نتیجهراه حل سه باعث می شود دکوراتورها روی هم چیده شوند، اما وقتی نوبت به دکوراتور انباشته می شود، اعلان زشت می شود.

با این ما تقریباً تمام نکات در بخش را پوشش می دهیم چرا دکوراتور؟

این تمام چیزی است که می خواستم به اشتراک بگذارم. امیدوارم این اطلاعات به نوعی به شما کمک کند.

توجه داشته باشید:
اگر به دکوراتور تایپی علاقه دارید، پیشنهاد می کنم اسناد رسمی را در اینجا بررسی کنید یا می توانید وبلاگ دیگر من را بررسی کنید: Typescrypt: زندگی خود را با تزئینات آسان تر کنید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا