TypeScript - چگونه الگوی طراحی استراتژی را به درستی پیادهسازی کنیم


من در تلاش هستم تا الگوی طراحی استراتژی را پیادهسازی کنم و میخواهم بدانم که آیا این کار را درست انجام میدهم یا نه.

فرض کنید، من کلاسی به نام FormBuilder دارم که از استراتژیهای موجود در لیست زیر برای ساخت فرم استفاده میکند:

  • SimpleFormStrategy
  • ExtendedFormStrategy
  • CustomFormStrategy

پس سوالها اینها هستند:

  1. آیا درست است که استراتژی را در داخل FormBuilder انتخاب کنیم و آن را از بیرون منتقل نکنیم؟
  2. آیا این اصل باز-بسته را نقض نمیکند؟ بنابراین، اگر بخواهم یک استراتژی فرم جدید اضافه کنم یا یکی از استراتژیهای موجود را حذف کنم، باید کلاس FormBuilder را ویرایش کنم.

نمونه کد پیشنویس

class Form {
    // Form data here
}

interface IFormStrategy {
    execute(params: object): Form;
}

class SimpleFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Here comes logics for building simple form
        return new Form();
    }
}

class ExtendedFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Here comes logics for building extended form
        return new Form();
    }
}

class CustomFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Here comes logics for building custom form
        return new Form();
    }
}

class FormBuilder {
    public build(params: object): Form {
        let strategy: IFormStrategy;

        // Here comes strategy selection logics based on params

        // If it should be simple form (based on params)
        strategy = new SimpleFormStrategy();
        // If it should be extended form (based on params)
        strategy = new ExtendedFormStrategy();
        // If it should be custom form (based on params)
        strategy = new CustomFormStrategy();

        return strategy.execute(params);
    }
}

پاسخ شماره ۱

شما دو سوال پرسیدید که به طور مستقیم با TypeScript مرتبط نیستند. کد را میتوان مستقیماً به C#/Java تبدیل کرد، زبانهای برنامهنویسی معمول و اصلی OOP. این حتی جالبتر است زیرا مربوط به هر دو الگوهای طراحی و اصول SOLID است که هر دو پایههای برنامهنویسی شیءگرا هستند.

بیایید قبل از عمومیتر شدن، به طور مشخص به آن پاسخ دهیم:

  1. آیا درست است که استراتژی را در داخل FormBuilder انتخاب کنیم و استراتژی را از بیرون منتقل نکنیم؟

بله. عکس این منجر به یک wrapper از FormFactory میشود که چندان جذاب نیست. چرا مستقیماً strategy.execute() را صدا نمیزنیم؟

  1. آیا این اصل باز-بسته را نقض نمیکند؟ بنابراین، اگر بخواهم یک استراتژی فرم جدید اضافه کنم یا یکی از استراتژیهای موجود را حذف کنم، باید کلاس FormBuilder را ویرایش کنم.

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

نظرات متفرقه

  • الگوهای طراحی

    • هر الگوی طراحی باید از زمینه (کد مشتری و حتی حوزه کسب و کار) پیدا شود و نه از پیش تعیین شده باشد. الگوهای طراحی به ندرت طبق کتاب استفاده میشوند، بلکه باید سازگار با زمینه شوند و میتوان آنها را با هم ترکیب کرد.
    • IFormStrategy در ابتدا یک Factory (انتزاعی) است: این یک Form ایجاد میکند. نام بهتر میتواند IFormFactory { create(...): Form; } باشد (یا فقط FormFactory، پیشوند «I» در C# رایجتر از TypeScript است). این یک Strategy برای FormBuilder است اما ذاتاً نیست. راستی، اصطلاح Strategy به ندرت هنگام نامگذاری کلاسها استفاده میشود، زیرا خیلی عمومی است. بهتر است از یک اصطلاح خاصتر/روشنتر استفاده کنید.
    • FormBuilder دقیقاً یک سازنده نیست که باید یک شیء را به صورت بخش به بخش بسازد، معمولاً با یک API روان مانند formBuilder.withPartA().withPartB().build();. این کلاس کارخانه/استراتژی مناسب را بر اساس پارامترهای ورودی انتخاب میکند. این یک انتخابکننده استراتژی است، یا یک کارخانهای از کارخانهها :D که همچنین کارخانه را فراخوانی میکند تا در نهایت Form را ایجاد کند. شاید کار زیادی انجام میدهد: فقط انتخاب کارخانه کافی باشد. شاید مناسب باشد، چون پیچیدگی را از کد مشتری پنهان میکند.
  • برنامهنویسی شیءگرا و الگوهای طراحی در مقابل برنامهنویسی تابعی در تایپاسکریپت

    • استراتژیها در یک زبان تابعی صرفاً توابع هستند. TypeScript اجازه میدهد توابع مرتبه بالاتر را با یک interface/type تعریف کنید، اما بدون یک شیء/کلاس بستهبندی شده که ممکن است ارزش بیشتری نداشته باشد. کد سمت مشتری فقط باید تابع دیگری را منتقل کند که میتواند یک «لامبدا ساده» (تابع فلش پهن) باشد.
  • متفرقه

    • آرگومان params توسط هر دو Builder و Factories استفاده میشود. احتمالاً بهتر است آن را جدا کنیم تا از ترکیب مسائل متمایز جلوگیری شود: انتخاب استراتژی و ایجاد فرم.
    • اگر نوع فرمی که قرار است ایجاد شود (ساده، توسعه یافته، سفارشی) پویا نباشد و توسط کد مشتری از قبل تعیین شده باشد، ممکن است بهتر باشد به صورت مستقیم ۳ متد با آرگومانهای مشخص ارائه شود: createSimpleForm(simpleFormArgs)، createExtendedForm(extendsFormArgs)... هر متد کارخانه مرتبط، شیء را نمونهسازی کرده و متد create(formArgs) خود را فراخوانی میکند. به این ترتیب، نیازی به الگوریتم پیچیده برای انتخاب یک استراتژی بر اساس if یا switch نخواهد بود که پیچیدگی روانسنجی را افزایش میدهد. فراخوانی هر متد createXxxForm نیز سادهتر خواهد بود، زیرا آرگومان شیء کوچکتر خواهد بود.

پاسخ شماره ۲

به زبان الگوهای طراحی برای استراتژی، FormBuilder شما نقش یک Context را ایفا میکند که مرجع به استراتژی فعلی مورد استفاده (IFormStragegy) را نگه میدارد. استراتژی از خارج منتقل میشود (با استفاده از setter) بنابراین برای گسترش باز است (OCP). بنابراین در مورد سوالات شما:

  1. آیا درست است که استراتژی را در داخل FormBuilder انتخاب کنیم و استراتژی را از بیرون منتقل نکنیم؟

این پیادهسازی درست استراتژی نیست. شما باید نمونههایی از استراتژی خود ایجاد کرده و آن را به کانتکست بدهید. بنابراین استراتژی میتواند در زمان اجرا تغییر کند.

  1. آیا این با اصل باز-بسته مغایرت ندارد؟ بنابراین، اگر بخواهم یک استراتژی فرم دیگر اضافه کنم یا یک استراتژی موجود را حذف کنم، باید کلاس FormBuilder را ویرایش کنم.

بله، اینطور است، شما نمیتوانید یک استراتژی جدید را به FormBuilder معرفی کنید مگر اینکه آن را تغییر دهید.

شما میتوانید اینجا برای یک مثال نگاه کنید.

FormBuilder context = new FormBuilder();
IFormStrategy simple = new SimpleFormStrategy();
IFormStrategy extended = new ExtendedFormStrategy();
IFormStrategy custom = new CustomFormStrategy();

context.setStrategy(simple);
context.build(/* parameters */)

context.setStrategy(custom);
context.build(/* parameters */)