دیزاین پترن State در عمل

wikimedia
wikimedia


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

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

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

فرض کنید ما دوتا نسخه داریم با اسم های primary و secondary که اولی پیام ها رو به سیستم ارسال میکنه ولی دومی فقط پیام ها رو در دیتابیس ذخیره میکنه و هرموقع سیستم اول به مشکل خورد، بجاش ادامه میده
برای اینکه secondary بتونه بجای سیستم اول ادامه بده، سه تا حالت مختلف ممکنه پیش میاد:

  • درست تا جایی که سیستم اول پیام ها رو پردازش کرده بود، دریافت کرده باشه
  • از سیستم اول عقب تر باشیم
  • از سیستم دوم جلوتر باشیم

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

حالا برای این کار لازم بود که بدونیم دقیقا در چه حالتی هستیم که تو سناریوهای مختلف که بررسی کردیم، سه متغیر زیر به برنامه اضافه شدن

public static bool IsPrimary { get; private set; }
public static bool IsBefotrPrimary { get; private set; }
public static bool IsFirstTime { get; private set; }

تو قسمت نتیجه گیری هم با توجه به این متغیرها بصورت کد زیر تصمیم میگرفتیم که چیکار کنیم:

private static int GetLastDispatchedMessageIdTypeCode()
        {
            int result;

            switch (FailOverService.IsBeforePrimary)
            {
                case true when FailOverService.IsPrimary:
                    result = PrimaryTypeCode;
                    break;
                
                case true when !FailOverService.IsPrimary:
                    result = SecondaryTypeCode;
                    break;
                
                case false when !FailOverService.IsPrimary:
                    result = SecondaryTypeCode;
                    break;
                
                case false when FailOverService.IsPrimary && 
                            FailOverService.IsFirstTime:
                    result = PrimaryTypeCode;
                    break;
                
                case false when FailOverService.IsPrimary && 
                            !FailOverService.IsFirstTime:
                    result = SecondaryTypeCode;
                    break;
                
                default:
                    throw new Exception(&quotnot valid input&quot);
            }

            return result;
}

همونطور که میبینید کد بالا خیلی خوب نیست و خوانایی کمی هم داره.
روشی که برای درست کردن کد بالا انجام دادم، استفاده از دیزاین پترن State بود

https://sourcemaking.com/design_patterns/state

خلاصه کار به این صورت بود:
یه کلاس با اسم Context به این صورت:

public class Context
    {
        private State _state;

        public Context()
        {
            _state = new InitialState();
        }

        public void TransitionTo(State state)
        {
            Logger.WriteInformationLog($&quotContext: Transition to {state.GetType().Name}&quot);

            _state = state;
            _state.SetContext(this);
        }

        public FailoverReadingTypeEnum GetState()
        {
            return _state.Handle();
        }
}

کلاس State به صورت زیر:

public abstract class State
    {
        protected Context Context;

        public void SetContext(Context context)
        {
            Context = context;
        }

        public abstract FailoverReadingTypeEnum Handle();
}

یه Enum با مقادیر زیر

public enum FailoverReadingTypeEnum
    {
        None = 0,
        Primary = 1,
        Secondary = 2
}

روش کار به این صورت هست که ما برای هر کدوم از حالت هایی که نیاز داریم یه کلاس جدید درست میکنیم که از State ارث بری کرده باشه
به این صورت:

public class PrimaryState : State
    {
        public override FailoverReadingTypeEnum Handle()
        {
            Logger.WriteInformationLog($&quotFailoverReadingTypeEnum change to : {FailoverReadingTypeEnum.Primary.ToString()}&quot);
            
            return FailoverReadingTypeEnum.Primary;
        }
}

برای استفاده هم کافی هست کلاس Context رو تو قسمتی که میخوایم New کنیم و در زمان هایی که لازم داریم State اون رو تغییر بدیم یا State فعلی رو بگیریم

public FailOverService(ProcessorType processorType)
{
            _context = new Context();
}

به این صورت میتونیم حالت رو تغییر بدیم:

_context.TransitionTo(new PrimaryState());

که ورودی همون کلاس هایی هستن که از State ارث بری کردن
برای دریافت حالت هم کافیه بصورت زیر عمل کنیم

public static FailoverReadingTypeEnum GetReadingType()
{
            return _context.GetState();
}

با این کار در درجه اول یکی از اون boolean ها برای دونستن حالت حذف شد و فقط دو مقدار IsPrimary و IsFirstTime باقی موندن
اون متد که توش از switch , case استفاده شده بود هم بصورت زیر در اومد

private static int GetLastDispatchedMessageIdTypeCode()
{
            return (int) FailOverService.GetReadingType();
}

لینک مطلب در وبلاگ اصلی :

دیزاین پترن State در عمل - روزمرگی های یک برنامه نویس (mhkarami97.ir)