الگوی طراحی حالت (State) در زبان C#

توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.

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

الگوی طراحی State یکی از مفیدترین الگوهایی است که توسط Gang of Four (گروه چهارنفره) توصیف شده است. بازی‌ها معمولاً به شدت به این الگو وابسته‌اند، زیرا اشیاء در بازی‌ها می‌توانند به‌طور مکرر تغییر کنند. بسیاری از شبیه‌سازی‌های دیگر، چه بازی باشند و چه نباشند، نیز به الگوی State وابسته‌اند.

می‌توانید کد مثال مربوط به این پست را در GitHub پیدا کنید.

مفهوم‌سازی مسئله

الگوی طراحی State به شدت با مفهوم ماشین حالت محدود (Finite-State Machine) مرتبط است.

ماشین حالت محدود

ایده این است که در هر لحظه مشخص، یک برنامه می‌تواند در تعداد محدودی از حالات (states) قرار داشته باشد. در هر حالت منحصربه‌فرد، برنامه رفتار متفاوتی از خود نشان می‌دهد و می‌تواند به‌صورت آنی از یک حالت به حالت دیگر تغییر کند. با این حال، بسته به حالت فعلی، برنامه تنها می‌تواند به حالات خاصی تغییر کند. این قوانین تغییر، که انتقال‌ها (Transitions) نامیده می‌شوند، از پیش تعیین شده‌اند.

این رویکرد در محیط وب بسیار رایج است. به عنوان مثال، فرض کنید در یک وب‌سایت وبلاگی یک کلاس به نام Post داریم. یک پست می‌تواند در یکی از سه حالت زیر باشد:

  1. پیش‌نویس (Draft)
  2. انتشار خصوصی (Private Publish)
  3. منتشر شده (Published)

روش Publish برای یک سند در هر یک از این حالات متفاوت عمل می‌کند:

  • در حالت Draft، سند با یک URL مخفی منتشر می‌شود.
  • در حالت Private Publish، سند عمومی می‌شود.
  • در حالت Published، هیچ کاری انجام نمی‌دهد.

ماشین حالت محدود انتشار پست

ماشین‌های حالت معمولاً با زنجیره‌های طولانی از شرط‌ها (Conditional Statements) پیاده‌سازی می‌شوند که رفتار مناسب را بسته به حالت فعلی شیء انتخاب می‌کنند. معمولاً این حالت تنها مجموعه‌ای از متغیرها است. حتی اگر تاکنون چیزی درباره ماشین‌های حالت محدود نشنیده باشید، احتمالاً حداقل یک‌بار آن را در پروژه‌های خود پیاده‌سازی کرده‌اید.

				
					public class Post
{
    private string state;

    //...

    public void Publish()
    {
        switch(state)
        {
            "draft":
                state = "private publish"
                break;
            "private publish":
                state = "published"
                break;
            "public publish":
                // do nothing
                break;
        }
    }
}

				
			

بزرگ‌ترین ضعف ماشین حالت مبتنی بر شرط‌ها زمانی آشکار می‌شود که شروع به اضافه کردن حالات و رفتارهای وابسته به حالت می‌کنیم. اکثر متدها شامل شرط‌های بسیار بزرگی می‌شوند که رفتار مناسب را بر اساس حالت فعلی انتخاب می‌کنند. این کد بسیار دشوار برای نگهداری است زیرا هر تغییری در منطق انتقال ممکن است نیاز به تغییر شرط‌ها در هر متد داشته باشد.

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

نمودار کلاس مثلا انتشار پست

الگوی State پیشنهاد می‌کند که برای تمام حالات ممکن یک شیء، کلاس‌های جدیدی ایجاد کنیم و تمام رفتارهای خاص هر حالت را به این کلاس‌ها استخراج کنیم.

به‌جای پیاده‌سازی همه رفتارها، شیء اصلی که به آن Context گفته می‌شود، مرجعی به یکی از این اشیاء حالت نگهداری می‌کند که نشان‌دهنده حالت فعلی است و همه کارهای مرتبط با حالت را به آن شیء واگذار می‌کند.

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

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

ساختاردهی الگوی State

در پیاده‌سازی پایه، الگوی State دارای چهار عنصر اصلی است:

نمودار کلاس الگوی طراحی حالت

  1. Context
    Context مرجعی به یکی از اشیاء حالت مشخص نگهداری می‌کند و تمام کارهای وابسته به حالت را به آن واگذار می‌کند. Context از طریق رابط حالت با شیء حالت ارتباط برقرار می‌کند و یک متد Setter برای انتقال به یک شیء حالت جدید ارائه می‌دهد.

  2. State
    رابط State متدهای خاص به حالت را تعریف می‌کند. این متدها باید برای تمام حالات مشخص منطقی باشند، زیرا نمی‌خواهیم برخی از حالات دارای متدهایی باشند که هرگز فراخوانی نشوند.

  3. Concrete State
    Concrete States پیاده‌سازی متدهای خاص به حالت را ارائه می‌دهند. برای جلوگیری از تکرار کد مشابه در چندین حالت، ممکن است کلاس‌های انتزاعی میانی ارائه کنیم که برخی رفتارهای مشترک را در خود جای دهند. اشیاء حالت ممکن است یک مرجع بازگشتی به شیء Context ذخیره کنند. از طریق این مرجع، حالت می‌تواند هر اطلاعات موردنیاز را از Context دریافت کند و انتقال‌های حالت را آغاز کند. هم Context و هم Concrete States می‌توانند مرحله بعدی را تنظیم کرده و انتقال حالت واقعی را با جایگزینی شیء حالت مرتبط با Context انجام دهند.

  4. Client
    Client می‌تواند تغییرات حالت را برای شیء Context ایجاد کند.

برای نشان دادن نحوه عملکرد الگوی State، یک کامپوننت را پیاده‌سازی می‌کنیم که دمای داخلی یک استیک را ردیابی می‌کند و سطح “پختگی” آن را ارزیابی می‌کند.

ابتدا، بیایید عنصر State خود را تعریف کنیم که نشان‌دهنده سطح “پختگی” استیک است.

				
					using State.Context;

namespace State.State
{
    /// <summary>
    /// The State abstract class
    /// </summary>
    public abstract class Doneness
    {
        protected Steak steak;
        protected double currentTemperature;
        protected double lowerTemperature;
        protected double upperTemperature;
        protected bool isSafe;

        public Steak Steak
        {
            get { return steak; }
            set { steak = value; }
        }

        public double CurrentTemperature
        {
            get { return currentTemperature; }
            set { currentTemperature = value; }
        }

        public abstract void IncreaseTemperature(double degrees);
        public abstract void DecreaseTemperature(double degrees);
        public abstract void DonenessCheck();
    }
}
				
			

ما متدهای انتزاعی IncreaseTemperature()، DecreaseTemperature() و DonenessCheck() را تعریف کرده‌ایم. این متدها توسط هر کدام از حالت‌های ممکن برای استیک پیاده‌سازی خواهند شد.

تعریف حالت‌های مشخص (ConcreteState)

اکنون که عنصر State را تعریف کرده‌ایم، برخی از اشیاء حالت مشخص را تعریف می‌کنیم. ابتدا، یک حالت برای زمانی که استیک خام است و به همین دلیل برای خوردن ایمن نیست، تعریف خواهیم کرد. در این حالت، می‌توانیم دمای پخت را افزایش یا کاهش دهیم، اما استیک تا زمانی که دمای مرکزی آن از 48.9 درجه سلسیوس بالاتر نرود، برای مصرف ایمن نخواهد بود.

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

				
					namespace State.State
{
    /// <summary>
    /// A Concrete State class.
    /// </summary>
    public class Uncooked : Doneness
    {
        public Uncooked(Doneness state)
        {
            currentTemperature = state.CurrentTemperature;
            steak = state.Steak;

            lowerTemperature = 0;
            upperTemperature = 48.9;
            isSafe = false;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature > upperTemperature)
                steak.State = new Rare(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
				
			

به صورت مشابه می‌توانیم بقیه حالت‌های پخت استیک را هم تعریف کنیم:

				
					using State.Context;

namespace State.State
{
    /// <summary>
    /// A Concrete State class.
    /// </summary>
    public class Rare : Doneness
    {
        public Rare(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Rare(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 49;
            upperTemperature = 54.4;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Uncooked(this);

            if (currentTemperature > upperTemperature)
                steak.State = new MediumRare(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
				
			
				
					using State.Context;

namespace State.State
{
    public class MediumRare : Doneness
    {
        public MediumRare(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public MediumRare(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 54.5;
            upperTemperature = 57.2;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Rare(this);

            if (currentTemperature > upperTemperature)
                steak.State = new Medium(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
				
			
				
					using State.Context;

namespace State.State
{
    public class Medium : Doneness
    {
        public Medium(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Medium(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 57.3;
            upperTemperature = 62.8;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new MediumRare(this);

            if (currentTemperature > upperTemperature)
                steak.State = new Well(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
				
			
				
					using State.Context;

namespace State.State
{
    public class Well : Doneness
    {
        public Well(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Well(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 62.9;
            upperTemperature = 68.3;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Medium(this);

            if (currentTemperature > upperTemperature)
                steak.State = new WellDone(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
				
			
				
					using State.Context;

namespace State.State
{
    public class WellDone : Doneness
    {
        public WellDone(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public WellDone(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 68.4;
            upperTemperature = 73.9;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Well(this);

            if (currentTemperature > upperTemperature)
                steak.State = new Burnt(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
				
			
				
					using State.Context;

namespace State.State
{
    public class Burnt : Doneness
    {
        public Burnt(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Burnt(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 74.0;
            upperTemperature = double.MaxValue;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new WellDone(this);

        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
				
			

اکنون که تمامی حالت‌های خود را تعریف کرده‌ایم، می‌توانیم در نهایت شرکت‌کننده Context را پیاده‌سازی کنیم. در این مورد، Context کلاسی به نام Steak است که یک ارجاع به حالت Doneness (میزان پختگی) که در حال حاضر در آن قرار دارد، نگه می‌دارد. علاوه بر این، هر زمان که دمای استیک را افزایش یا کاهش دهیم، باید متد مربوط به حالت Doneness فعلی را فراخوانی کند.

				
					using State.State;

namespace State.Context
{
    /// <summary>
    /// The Context class
    /// </summary>
    public class Steak
    {
        private Doneness state;
        private string cut;

        public Steak(string cut)
        {
            this.cut = cut;
            state = new Rare(0.0, this);
        }

        public double CurrentTemperature
        {
            get { return state.CurrentTemperature; }
        }

        public Doneness State
        {
            get { return state; }
            set { state = value; }
        }

        public void IncreaseTemperature(double degrees)
        {
            state.IncreaseTemperature(degrees);
            Console.WriteLine($"Increased degrees by {degrees} degrees");
            Console.WriteLine($"    Current degrees is {CurrentTemperature}");
            Console.WriteLine($"    Doneness is {State.GetType().Name}");
            Console.WriteLine("");
        }

        public void DecreaseTemperature(double degrees)
        {
            state.DecreaseTemperature(degrees);
            Console.WriteLine($"Decreased degrees by {degrees} degrees");
            Console.WriteLine($"    Current degrees is {CurrentTemperature}");
            Console.WriteLine($"    Doneness is {State.GetType().Name}");
            Console.WriteLine("");
        }
    }
}
				
			

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

				
					using State.Context;

Steak steak = new Steak("T-Bone");

steak.IncreaseTemperature(48.9);
steak.IncreaseTemperature(10);
steak.IncreaseTemperature(5);
steak.DecreaseTemperature(15);
steak.DecreaseTemperature(5);
steak.IncreaseTemperature(10);
steak.IncreaseTemperature(20);
				
			

با تغییر دما، وضعیت شیء Steak نیز تغییر می‌کند. خروجی این متد به صورت زیر است:

خروجی نتیجه الگوی طراحی state

همزمان با تغییر دمای داخلی نمونه‌ای از Steak، وضعیت Doneness که در آن قرار دارد نیز تغییر می‌کند. به این ترتیب رفتار ظاهری آن شیء به رفتار تعریف‌شده توسط وضعیت فعلی تغییر می‌یابد.

مزایا و معایب الگوی State

مزایا

  • می‌توان کد مربوط به حالت‌های مختلف را در کلاس‌های جداگانه سازماندهی کرد و به این ترتیب اصل مسئولیت واحد (Single Responsibility Principle) را رعایت کرد.
  • می‌توان حالت‌های جدید را بدون تغییر کلاس‌های حالت موجود یا کلاس Context اضافه کرد و اصل باز/بسته (Open/Closed Principle) را رعایت کرد.
  • می‌توان کد کلاس Context را با حذف شرط‌های پیچیده مربوط به ماشین حالت ساده کرد.

معایب

  • اعمال این الگو می‌تواند بیش از حد پیچیده باشد، اگر ماشین حالت تنها چند حالت محدود داشته باشد یا به ندرت تغییر کند.

ارتباط الگوی State با سایر الگوها

  • الگوهای Bridge، State و Strategy ساختارهای بسیار مشابهی دارند. در واقع، همه این الگوها بر پایه ترکیب (Composition) بنا شده‌اند، یعنی واگذاری کار به اشیاء دیگر. با این حال، هر یک از این الگوها مشکلات متفاوتی را حل می‌کنند. یک الگو فقط یک دستورالعمل برای ساختاردهی کد به شیوه‌ای خاص نیست؛ بلکه می‌تواند به سایر توسعه‌دهندگان نشان دهد که آن الگو چه مشکلی را حل می‌کند.
  • State را می‌توان به‌عنوان یک گسترش برای الگوی Strategy در نظر گرفت. هر دو الگو بر ترکیب استوار هستند: آنها رفتار کلاس Context را با واگذاری برخی وظایف به اشیاء کمکی تغییر می‌دهند.
    • در Strategy، این اشیاء کاملاً مستقل و از یکدیگر بی‌اطلاع هستند.
    • اما در State محدودیتی برای وابستگی بین حالات مختلف وجود ندارد، به طوری که می‌توانند وضعیت کلاس Context را آزادانه تغییر دهند.

جمع‌بندی

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

لازم به ذکر است که الگوی State، همانند سایر الگوهای طراحی که توسط گروه Gang of Four معرفی شده‌اند، راه‌حل جادویی یا یک راه‌حل نهایی برای طراحی برنامه‌ها نیست. باز هم این وظیفه مهندسان است که تصمیم بگیرند چه زمانی از یک الگوی خاص استفاده کنند. در نهایت، این الگوها زمانی مفید هستند که مانند یک ابزار دقیق به‌کار روند، نه به‌عنوان یک چکش سنگین.

©دوات با هدف دسترس‌پذیر کردن دانش انگلیسی در حوزه صنعت نرم‌افزار وجود آمده است. در این راستا از هوش مصنوعی برای ترجمه گلچینی از مقالات مطرح و معتبر استفاده می‌شود. با ما در تماس باشید و انتقادات و پیشنهادات خود را از طریق صفحه «تماس با ما» در میان بگذارید.