Идея паттерна проектирования Состояние (State)

Паттерн проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте подробнее рассмотрим один из наиболее часто используемых поведенческих паттернов — Состояние (State).

Существует три вида паттернов проектирования:

  • Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
  • Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
  • Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.

Состояние (State) — это поведенческий паттерн, который предоставляет возможность экземпляру класса самостоятельно регулировать свое поведение, ориентируясь на его текущее внутреннем статусе. То есть, при изменении каких-либо внутренних значений класс может кардинально изменять свое поведение.

Архитектура паттерна Состояние (State)

Давайте рассмотрим UML диаграмму данного поведенческого паттерна

Состояние (State)

Состояние (State)

  • Widget — основной объект, поведение которого изменяется в зависимости от его текущего состояния;
  • IState — базовый интерфейс для всех возможных состояний объекта;
  • State1, State2 — конкретные состояния объекта, влияющие на его поведение.

Логика работы паттерна Состояние (State)

Сразу перейдем к рассмотрению примера из одной из возможных предметных областей, где применение данного паттерна можно считать уместным. Возьмем обычную кредитную банковскую карту. На ней хранятся два тип средств, это собственные средства, которые были зачислены пользователю его работодателем, и кредитные средства, которые принадлежат банку, но при необходимости пользователь может ими воспользоваться, при условии возврата. Исходя из этого, мы можем выделить три состояния кредитной карты: блокировка (денег нет совсем), расходование кредитных средств, расходование собственных средств. При этом, если на карточке в первую очередь расходуются собственные средства, а пополняются в первую очередь кредитные. Так же мы можем выделить две операции, это пополнение, и расходование средств.

Теперь, когда мы выделили возможные состояния и операции, нам необходимо определить возможные направления перехода между состояниями. Это проще всего сделать с помощью схемы.

Как можно понять из схемы, если на карте имеются собственные средства, то при их исчерпании начинают расходоваться кредитные. Если заканчиваются и кредитные, то карточка блокируется до пополнения средств. При пополнении счета, накопление собственных средств не начинается до тех пор, пока ни будет полностью погашена кредитная задолженность. Если кредитные средства имеются в полном объеме, то при пополнении увеличивается запас собственных средств.

При этом в соответствии с логикой паттерна мы будем всегда вызывать одни и те же методы пополнения и снятия средств, а кредитная карта сама будет изменять свое поведение в зависимости от текущего состояния счета.

Реализация паттерна проектирования Состояние (State) на языке C#

Для начала определим интерфейс состояний. Любое состояние должно реализовывать две операции: снятие и пополнение счета.

IState.cs
namespace State
{
    /// <summary>
    /// Интерфейс для состояний счета.
    /// </summary>
    public interface IState
    {
        /// <summary>
        /// Пополнить счет.
        /// </summary>
        /// <param name="card"> Пополняемый счет. </param>
        /// <param name="money"> Сумма пополнения. </param>
        void Deposit(Card card, decimal money);

        /// <summary>
        /// Расходование со счета.
        /// </summary>
        /// <param name="card"> Счет списания. </param>
        /// <param name="price"> Стоимость покупки. </param>
        /// <returns> Успешность выполнения операции. </returns>
        bool Spend(Card card, decimal price);
    }
}

Теперь реализуем сами конкретные состояния кредитной карты:

Blocked.cs
using System;

namespace State
{
    /// <summary>
    /// Заблокированное состояние счета.
    /// </summary>
    public class Blocked : IState
    {
        /// <summary>
        /// Пополнить счет.
        /// </summary>
        /// <param name="card"> Пополняемый счет. </param>
        /// <param name="money"> Сумма пополнения. </param>
        public void Deposit(Card card, decimal money)
        {
            // Проверяем входные аргументы на корректность.
            if (card == null)
            {
                throw new ArgumentNullException(nameof(card));
            }

            if (money <= 0)
            {
                throw new ArgumentException("Вносимая сумма должна быть больше нуля.", nameof(money));
            }

            // Вычисляем сумму сверхлимитной задолженности.
            var overdraft = card.CreditLimit - card.Credit;

            // Вычисляем насколько сумма пополнения перекрывает задолженность.
            var difference =  money - overdraft;

            if (difference < 0)
            {
                // Если сумма пополнения не перекрывает задолженность,
                // то просто уменьшаем сумму задолженности.
                card.Credit += money;

                // Вычисляем процент оставшейся суммы на счете.
                var limit = card.Credit / card.CreditLimit * 100;
                if (limit < 10)
                {
                    // Если после пополнения на счете все еще меньше десяти процентов от лимита,
                    // то просто сообщаем об этом пользователю.
                    Console.WriteLine($"Ваш счет пополнен на сумму {money}. " +
                        $"Сумма на вашем счете все еще составляет менее 10%. Ваш счет остался заблокирован. Пополните счет на большую сумму.  {card.ToString()}");
                }
                else if (limit >= 10 && limit < 100)
                {
                    // Если задолженность перекрыта не полностью, то переводим в состояние расходования кредитных средств.
                    card.State = new UsingCreditFunds();

                    Console.WriteLine($"Ваш счет пополнен на сумму {money}. Задолженность частично погашена. " +
                        $"Погасите задолженность в размере {Math.Abs(difference)} рублей. {card.ToString()}");
                }
                else
                {
                    // Иначе задолженность полностью погашена, переводим в состояние расходования собственных средств.
                    card.State = new UsingOwnFunds();

                    Console.WriteLine($"Ваш счет пополнен на {money} рублей. Задолженность полностью погашена. {card.ToString()}");
                }
            }
            else
            {
                // Иначе закрываем задолженность, а оставшиеся средства переводим в собственные средства.
                card.Credit = card.CreditLimit;
                card.Debit = difference;

                // Переводим карту в состояние использования собственных средств.
                card.State = new UsingOwnFunds();

                Console.WriteLine($"Ваш счет пополнен на {money} рублей. " +
                    $"Кредитная задолженность погашена. {card.ToString()}");
            }
        }

        /// <summary>
        /// Расходование со счета.
        /// </summary>
        /// <param name="card"> Счет списания. </param>
        /// <param name="price"> Стоимость покупки. </param>
        /// <returns> Успешность выполнения операции.</returns>
        public bool Spend(Card card, decimal price)
        {
            // Отказываем в операции.
            Console.WriteLine($"Ваш счет заблокирован. Пополните счет.  {card.ToString()}");
            return false;
        }
    }
}
UsingCreditFunds.cs
using System;

namespace State
{
    // Состояние счета, при котором используются кредитные средства.
    public class UsingCreditFunds : IState
    {
        /// <summary>
        /// Пополнить счет.
        /// </summary>
        /// <param name="card"> Пополняемый счет. </param>
        /// <param name="money"> Сумма пополнения. </param>
        public void Deposit(Card card, decimal money)
        {
            // Проверяем входные аргументы на корректность.
            if (card == null)
            {
                throw new ArgumentNullException(nameof(card));
            }

            if (money <= 0)
            {
                throw new ArgumentException("Вносимая сумма должна быть больше нуля.", nameof(money));
            }

            // Вычисляем сумму сверхлимитной задолженности.
            var overdraft = card.CreditLimit - card.Credit;

            // Вычисляем насколько сумма пополнения перекрывает задолженность.
            var difference = money - overdraft;

            if(difference < 0)
            {
                // Если сумма пополнения не перекрывает задолженность,
                // то просто уменьшаем сумму задолженности.
                card.Credit += money;

                Console.WriteLine($"Ваш счет пополнен на сумму {money}. " +
                    $" Погасите задолженность в размере {difference} рублей. {card.ToString()}");
            }
            else
            {
                // Иначе закрываем задолженность, а оставшиеся средства переводим в собственные средства.
                card.Credit = card.CreditLimit;
                card.Debit = difference;

                // Переводим карту в состояние использования собственных средств.
                card.State = new UsingOwnFunds();

                Console.WriteLine($"Ваш счет пополнен на {money} рублей. " +
                    $"Кредитная задолженность погашена. {card.ToString()}");
            }
        }

        /// <summary>
        /// Расходование со счета.
        /// </summary>
        /// <param name="card"> Счет списания. </param>
        /// <param name="price"> Стоимость покупки. </param>
        /// <returns> Успешность выполнения операции. </returns>
        public bool Spend(Card card, decimal price)
        {
            // Проверяем входные аргументы на корректность.
            if (card == null)
            {
                throw new ArgumentNullException(nameof(card));
            }

            if (price <= 0)
            {
                throw new ArgumentException("Цена должна быть больше нуля.", nameof(price));
            }

            if(price > card.Credit)
            {
                // Если сумма покупки больше, чем средства на счету,
                // от отказываем в операции.
                Console.WriteLine($"Операция не выполнена. Недостаточно средств на счете. {card.ToString()}");
                return false;
            }
            else
            {
                // Иначе расходуем кредитные средства.
                card.Credit -= price;

                // Вычисляем текущую задолженность.
                var overdraft = card.CreditLimit - card.Credit;

                Console.WriteLine($"Выполнена операция за счет кредитных средств на сумму {price}. " +
                    $"Погасите задолженность в размере {overdraft} рублей.  {card.ToString()}");

                // Вычисляем процент оставшейся суммы на счете.
                var limit = card.Credit / card.CreditLimit * 100;
                if(limit < 10)
                {
                    // Если оставшаяся сумма менее десяти процентов от кредитного лимита, то блокируем карту.
                    card.State = new Blocked();
                    Console.WriteLine($"Сумма на вашем счете составляет менее 10%. Ваш счет заблокирован. Пополните счет.  {card.ToString()}");
                }

                return true;
            }

        }
    }
}
UsingOwnFunds.cs
using System;

namespace State
{
    /// <summary>
    /// Состояние счета, при котором используются собственные средства.
    /// </summary>
    public class UsingOwnFunds : IState
    {
        /// <summary>
        /// Пополнить счет.
        /// </summary>
        /// <param name="card"> Пополняемый счет. </param>
        /// <param name="money"> Сумма пополнения. </param>
        public void Deposit(Card card, decimal money)
        {
            // Проверяем входные аргументы на корректность.
            if(card == null)
            {
                throw new ArgumentNullException(nameof(card));
            }

            if(money <= 0)
            {
                throw new ArgumentException("Вносимая сумма должна быть больше нуля.", nameof(money));
            }

            // Увеличиваем остаток собственных средств.
            card.Debit += money;

            Console.WriteLine($"Ваш счет пополнен на {money} рублей. {card.ToString()}");
        }

        /// <summary>
        /// Расходование со счета.
        /// </summary>
        /// <param name="card"> Счет списания. </param>
        /// <param name="price"> Стоимость покупки. </param>
        /// <returns> Успешность выполнения операции. </returns>
        public bool Spend(Card card, decimal price)
        {
            // Проверяем входные аргументы на корректность.
            if (card == null)
            {
                throw new ArgumentNullException(nameof(card));
            }

            if (price <= 0)
            {
                throw new ArgumentException("Цена должна быть больше нуля.", nameof(price));
            }

            if(price <= card.Debit)
            {
                // Если сумма покупки меньше количества собственных средств,
                // то просто уменьшаем сумму собственных средств.
                card.Debit -= price;

                // Сообщаем пользователю.
                Console.WriteLine($"Выполнена операция за счет собственных средств на сумму {price}. {card.ToString()}");
                return true;
            }
            else if(price > card.All)
            {
                // Если сумма покупки больше, чем все средства на счету,
                // от отказываем в операции.
                Console.WriteLine($"Операция не выполнена. Недостаточно средств на счете. {card.ToString()}");
                return false;
            }
            else
            {
                // Иначе частично расходуем кредитные средства.
                // Вычисляем сумму необходимых кредитных средств.
                var overdraft = price - card.Debit;

                // Расходуем средства со счетов.
                card.Credit -= overdraft;
                card.Debit = 0;

                // Переводим карту в состояние расходования кредитных средств.
                card.State = new UsingCreditFunds();

                // Сообщаем пользователю.
                Console.WriteLine($"Выполнена операция за счет собственных и кредитных средств на сумму {price}. " +
                    $"Погасите задолженность в размере {overdraft} рублей.  {card.ToString()}");

                return true;
            }
        }
    }
}

Теперь реализуем сам класс кредитной карты.

Card.cs
using System;

namespace State
{
    /// <summary>
    /// Кредитная карта.
    /// </summary>
    public class Card
    {
        /// <summary>
        /// Кредитные средства на карте.
        /// </summary>
        public decimal Credit { get; set; }

        /// <summary>
        /// Собственные средства на карте.
        /// </summary>
        public decimal Debit { get; set; }

        /// <summary>
        /// Все средства на карте.
        /// </summary>
        public decimal All => Credit + Debit;

        /// <summary>
        /// Состояние карты.
        /// </summary>
        public IState State { get; set; }

        /// <summary>
        /// Кредитный лимит на карте.
        /// </summary>
        public decimal CreditLimit { get; private set; }

        /// <summary>
        /// Создать новый экземпляр кредитной карты. 
        /// </summary>
        /// <param name="creditLimit"> Кредитный лимит. </param>
        public Card(decimal creditLimit)
        {
            // Проверяем входные данные на корректность.
            if(creditLimit <= 0)
            {
                throw new ArgumentException("Кредитный лимит должен быть больше нуля.", nameof(creditLimit));
            }

            // Устанавливаем значения.
            CreditLimit = creditLimit;
            Credit = creditLimit;
            State = new UsingOwnFunds();
            Debit = 0;
        }

        /// <summary>
        /// Пополнить карту.
        /// </summary>
        /// <param name="money"> Сумма пополнения. </param>
        public void Deposit(decimal money)
        {
            // Передаем управление пополнением текущему состоянию объекта.
            State.Deposit(this, money);
        }

        /// <summary>
        /// потратить деньги с карты.
        /// </summary>
        /// <param name="price"> Сумма покупки. </param>
        /// <returns> Успешность выполнения операции. </returns>
        public bool Spend(decimal price)
        {
            // Передаем управление расходом средств текущему состоянию объекта.
            return State.Spend(this, price);
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns> Состояние счета. </returns>
        public override string ToString()
        {
            return $"Состояние счета {All}, в том числе кредитные средства {Credit}, собственные средства {Debit}.";
        }
    }
}

Ну и наконец начнем работать с нашей кредитной картой.

Program.cs
using System;
using System.Text;

namespace State
{
    class Program
    {
        static void Main(string[] args)
        {
            // Используем эту команду, если Windows на английском 
            // и выводит вместо кириллицы ???????? ????? ????
            Console.OutputEncoding = Encoding.Unicode;

            // Создаем новую карту.
            var card = new Card(10);

            // Выполняем операции с картой.
            card.Deposit(1);    // 11
            card.Deposit(2);    // 13
            card.Spend(1);      // 12
            card.Spend(5);      // 7
            card.Deposit(7);    // 14
            card.Spend(10);     // 4
            card.Spend(1);      // 3
            card.Spend(5);      // 3
            card.Spend(2.5M);   // 0.5      
            card.Deposit(7);    // 7.5
            card.Spend(7);      // 0.5
            card.Deposit(0.1M); // 0.6
            card.Deposit(20);   // 20.6

            Console.ReadLine();
        }
    }
}

В результате получаем следующий вывод на консоль

Исходный код доступен в репозитории github.

Советую также изучить статью Паттерн проектирования Стратегия (Strategy) на языке C#.

P.S. Присоединяйся в любой удобной для тебя социальной сети. Для меня очень важно оставаться с тобой на связи, ведь у меня есть еще много полезной информации о программировании для тебя, которой я хочу с тобой поделиться.

Вконтакте
Facebook
Telegram
Twitter
Одноклассники
Дзен
Google+
 
×
%d такие блоггеры, как: