Шаблон Состояние (State): Управление состоянием объекта

Просто о NET | создано: 18.08.2016 | опубликовано: 18.08.2016 | обновлено: 13.01.2024 | просмотров: 6271

Очень часто в своей работе мне приходилось использовать перечисления (Enum) в качестве информации о состоянии объекта. И всё бы вроде как хорошо, но есть некоторое неудобство, при таком подходе логика по проверке состояния (validation) объекта при смене статуса "размазывалась" по всей системе. И часто получалось, что отследить все правила перехода от одного состояния к другому практически непосильная задача, особенно если проект разрабатывает группа программистов.

Объект в “Состоянии”

Итак, паттерн “Состояние” (State). Предположу что вы уже знакомы с паттерном, если же нет, прочитать о нем можно в статье википедии. Примером сущности для пилотного проекта я возьму понятие Документ (Document). На самом деле не важно какой это документ, но а в нашем случает пусть это будет документ, который может и должен быть утвержден, после чего вступает в силу. Пример такого документа – документ на внесения изменений в какую-либо систему (например, Штатное расписание). В общем, документ в моем примере будет иметь следующие состояния:

Черновик (Draft) – новый документ, который только что появился в системе имеет именно этот статус.

На проверке (In validation) – документ был заполнен и отправлен на утверждение начальству. В документе заполнены все необходимые реквизиты и он готов к утверждению.

Утверждён (Complete) – документ проверен начальником и/или соответствующей инстанцией, подписан и вступил (вступит) в силу в момента наступления даты указанной в документе.

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

Правила при смене статуса

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

1. Статус Черновик требует заполненное свойство Автор

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

2. Статус На проверке требует заполненное свойство Регистрационный номер

После того, как специальная служба в нашей организации проверит документ, его отнесут на подпись нашему директору, директор подписывает. После этого документ приобретает статус Утвержден. Итак, третье правило:

3. Статус Утвержден требует подписи (цифровой) директора

На данный момент обрисована идеальная ситуация, когда документ проходит все состояния без каких-либо помех. Но нужно быть реалистом и иметь в виду, что Директор или специальная служба может вернуть документ в статус Черновик, потому что необходимые реквизиты были заполнены неправильно (или еще какие-нибудь заморочки).

Переходы по статусам

Давайте опишем направления перехода, которые, в свою очередь, тоже являются своего рода правилами.

  1. Статус Черновик может переходить в статус На проверке
  2. Статус На проверке может переходить в статус Утвержден
  3. Статус На проверке может переходить в статус Черновик

Пусть правила слишком надуманны, но суть не в том, чтобы рассказать как строить правила, а в том, как эти правила соблюдать, храня их и управляя ими централизованно. Вот наверное и всё по правилам. Давайте перейдем к реализации.

Класс Document

Мой класс для простоты будет содержать только те свойства, которые требуются для показа в демонстрации. В реальном мире этот класс не имеет смысла, а для пример идеально подходит по своей простоте.

namespace Calabonga.StateProcessor.Demo
{
    /// <summary>
    /// Entiti with states
    /// </summary>
    public class Document
    {
        /// <summary>
        /// Unique indentificator
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Author of the document
        /// </summary>
        public string Author { get; set; }

        /// <summary>
        /// Registration number
        /// </summary>
        public string Number { get; set; }
    }
}

StateProcessor

Скажу сразу, что я буду использовать свой nuget-пакет StatusProcessor, который создан мной как реализация паттерна "Состояние". Вы можете написать свою реализацию, а можете использовать мою. Итак, для чего нужен StateProcessor. StateProcessor добавит к вашей сущности, которая должна иметь состояния, всего одно свойство - это идентификатор состояния типа Guid. Также StateProcessor даст возможность создать объекты состояния и правила перехода между ними. Плюс ко всему, вы получите вспомогательные методы и функции для управления этими состояними. Давайте начнем с установки  nuget-пакета.

Для установки пакета в Package Managment Console надо выполнить команду установки. Вот результат выполнения моей команды:

PM> Install-Package Calabonga.StatusProcessor

Attempting to gather dependency information for package 'Calabonga.StatusProcessor.1.6.0'
with respect to project 'Calabonga.StateProcessor.Demo', targeting '.NETFramework,Version=v4.5.2'
Gathering dependency information took 1,24 sec
Attempting to resolve dependencies for package 'Calabonga.StatusProcessor.1.6.0' with DependencyBehavior 'Lowest'
Resolving dependency information took 0 ms
Resolving actions to install package 'Calabonga.StatusProcessor.1.6.0'
Resolved actions to install package 'Calabonga.StatusProcessor.1.6.0'
Retrieving package 'Calabonga.StatusProcessor 1.6.0' from 'nuget.org'.
  GET https://api.nuget.org/packages/calabonga.statusprocessor.1.6.0.nupkg
  OK https://api.nuget.org/packages/calabonga.statusprocessor.1.6.0.nupkg 608ms
Installing Calabonga.StatusProcessor 1.6.0.
Adding package 'Calabonga.StatusProcessor.1.6.0' to folder 'D:\Dev\_TEMP\Calabonga.StateProcessor\packages'
Added package 'Calabonga.StatusProcessor.1.6.0' to folder 'D:\Dev\_TEMP\Calabonga.StateProcessor\packages'
Added package 'Calabonga.StatusProcessor.1.6.0' to 'packages.config'
Successfully installed 'Calabonga.StatusProcessor 1.6.0' to Calabonga.StateProcessor.Demo
Executing nuget actions took 1,77 sec
Прошло времени: 00:00:04.7423125
PM> 

DraftStatus - первый статус "черновик"

Класс будет иметь именно такое название, и будет унаследован от EntityStatus. В классе переопределены абстрактные методы и один виртуальный. Я создать статическое свойство типа Guid как идентификтор, который будет доступен всегда и везде. А также создать русскоязычное названия статуса для отчетов и для UI-отображения. Вот как выглядит класс:

namespace Calabonga.StateProcessor.Demo {

    /// <summary>
    /// Draft Status
    /// </summary>
    public class DraftStatus : EntityStatus
    {
        public static Guid Guid => Guid.Parse("49446068-0CCE-4168-B7AC-CD36E032F2BC");

        public static string StatusDisplayName = "Черновик";

        public override string GetDisplayName()
        {
            return StatusDisplayName;
        }

        protected override string StatusName() {
            return DocumentStatus.Draft.ToString();
        }

        protected override Guid GetUniqueInentifier() {
            return Guid;
        }
    }
}

Остальные статусы создаю подобным образом, изменив название для DisplayName и Guid. Идентификатор статусов в таком случае будут уникальны и статичны. Обратите внимание, что метод StatusName возвращает название статуса на английском языке. Я создал перечисление, которое будет использоваться в реализации.

namespace Calabonga.StateProcessor.Demo {

    /// <summary>
    /// Ducument statuses as enum
    /// </summary>
    public enum DocumentStatus {
        Draft,
        InValidation,
        Complete
    }
}

После того, как созданы все статусы, можно приступать к созданию правил. Но перед этим надо сделать так, чтобы сущность Document имела реализацию IEntity. Другими словами, надо унаследовать Document от IEntity:

namespace Calabonga.StateProcessor.Demo
{
    /// <summary>
    /// Entiti with states
    /// </summary>
    public class Document : IEntity
    {
        /// <summary>
        /// Unique indentificator
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Author of the document
        /// </summary>
        public string Author { get; set; }

        /// <summary>
        /// Registration number
        /// </summary>
        public string Number { get; set; }

        /// <summary>
        /// Status (IEntity)
        /// </summary>
        public Guid EntityActiveStatus { get; set; }
    }
}

Обратите внимание на 6 и 26 строки. Вот теперь можно создавать первое правило DraftRule:

namespace Calabonga.StateProcessor.Demo.Rules {

    /// <summary>
    /// Rules validation for Draft status
    /// </summary>
    public class DraftRule : StatusRule<Document, EntityStatus> {

        public DraftRule(IEnumerable<EntityStatus> statuses) : base(statuses) { }

        public override bool CanEnter(RuleContext<Document, EntityStatus> context) {
            return true;
        }

        public override bool CanLeave(RuleContext<Document, EntityStatus> context) {
            return true;
        }

        protected override EntityStatus ValidationForStatus(IEnumerable<EntityStatus> statuses) {
            return statuses.SingleOrDefault(x => x.Name.Equals(DocumentStatus.Draft.ToString()));
        }
    }
}

В строке 6 указан наследник StatusRule, его абстрактные методы проверки CanEnter и CanLeave означающие "может ли сущность войти в статус Draft" и "может ли сущность выйти из статуса Draft" соответственно, на данный момент возвращаю true, что разрешает смену статуса.

В строке 18 мы указываем, к какому значения перечисления DocumentStatus принадлежит это правило.

Усложнение задачи

Давайте остановимся подробнее на строке 18, или если точнее сказать, на сигнатуре метода. Коллекция статусов системы (наследников от EntityStatus), которая указана в сигнатуре должны быть где-то зарегистрирована. Заглядывая вперед скажу, что будет создан DocumentRuleProcessor, при помощи которого и будут управляться статусы сущности Document. Так вот, как раз в конструктор этого объекта и требуется набор правил и статусов. Можно создать свойство AvaliableStatuses по такому примеру:

namespace Calabonga.StateProcessor.Demo {
    class Program {
        static void Main(string[] args) {

            var holder = new StatusHolder();
            foreach (var status in holder.AvaliableStatuses)
            {
                Console.WriteLine($"{status.Name} - {status.DisplayName} - {status.Id}");
            }
        }
    }

    public class StatusHolder
    {
        public IEnumerable<EntityStatus> AvaliableStatuses
        {
            get
            {
                yield return new DraftStatus();
                yield return new InValidationStatus();
                yield return new CompleteStatus();
            }
        }
    }
}

Или сделать представленный класс реализующим паттерн Singleton.

Результатом этой программы будет такой скриншот:

В общем, вы в праве поступать так, как вам хочется. Но мне нравится всё регистрировать в DI-контейнере (Depedency Injection container). Мой выбор - Autofac. Давайте усложним поставленную задачу и подключим DI-контейнер, чтобы пример получился максимально практичным. И для пушей практичности можно не только подключить конейнер, но и создать БД на EntityFramework, чтобы документы хранить в базе данных.

Установка EntityFramework и Autofac

Я установил nuget-пакеты, и вот логи выполненых команд, чтобы вы смогли отследить версии установленных мной пакетов:

PM> Install-Package Autofac

Attempting to gather dependency information for package 'Autofac.4.0.0' with
 respect to project 'Calabonga.StateProcessor.Demo', targeting '.NETFramework,Version=v4.5.2'
Gathering dependency information took 1,23 sec
Attempting to resolve dependencies for package 'Autofac.4.0.0' with DependencyBehavior 'Lowest'
Resolving dependency information took 0 ms
Resolving actions to install package 'Autofac.4.0.0'
Resolved actions to install package 'Autofac.4.0.0'
Retrieving package 'Autofac 4.0.0' from 'nuget.org'.
Adding package 'Autofac.4.0.0' to folder 'D:\Dev\_TEMP\Calabonga.StateProcessor\packages'
Added package 'Autofac.4.0.0' to folder 'D:\Dev\_TEMP\Calabonga.StateProcessor\packages'
Added package 'Autofac.4.0.0' to 'packages.config'
Successfully installed 'Autofac 4.0.0' to Calabonga.StateProcessor.Demo
Executing nuget actions took 1,15 sec
Прошло времени: 00:00:04.1491785
PM> Install-Package EntityFramework


Attempting to gather dependency information for package 'EntityFramework.6.1.3'
with respect to project 'Calabonga.StateProcessor.Demo', targeting '.NETFramework,Version=v4.5.2'
Gathering dependency information took 21,16 ms
Attempting to resolve dependencies for package 'EntityFramework.6.1.3' with DependencyBehavior 'Lowest'
Resolving dependency information took 0 ms
Resolving actions to install package 'EntityFramework.6.1.3'
Resolved actions to install package 'EntityFramework.6.1.3'
Retrieving package 'EntityFramework 6.1.3' from 'nuget.org'.
Adding package 'EntityFramework.6.1.3' to folder 'D:\Dev\_TEMP\Calabonga.StateProcessor\packages'
Added package 'EntityFramework.6.1.3' to folder 'D:\Dev\_TEMP\Calabonga.StateProcessor\packages'
Added package 'EntityFramework.6.1.3' to 'packages.config'
Выполнение файла скрипта "D:\Dev\_TEMP\Calabonga.StateProcessor\packages\EntityFramework.6.1.3\tools\init.ps1"
Выполнение файла скрипта "D:\Dev\_TEMP\Calabonga.StateProcessor\packages
\EntityFramework.6.1.3\tools\install.ps1"

Type 'get-help EntityFramework' to see all available Entity Framework commands.
Successfully installed 'EntityFramework 6.1.3' to Calabonga.StateProcessor.Demo
Executing nuget actions took 3,48 sec
Прошло времени: 00:00:04.0479574
PM> 

Теперь всё необходимое установлено и можно приступать к созданию контейнера и базы данных. Для начала создадим ApplicationDbContext.cs, который унаследован от DbContext, то ест, это будет Code First база данных.

namespace Calabonga.StateProcessor.Demo.Data {

    /// <summary>
    /// EntityFramework Code Fisrt Database class
    /// </summary>
    public class ApplicationDbContext : DbContext {

        public ApplicationDbContext()
            : base("DocumentDbConnection") {

        }

        public DbSet<Document> Documents { get; set; }
    }
}

Так как для примера я использую ConsoleApplication, то информацию для DbContext я прописал в файле конфигурации App.config:

<connectionStrings>
  <add
      name="DocumentDbConnection"
      connectionString="Data Source=SQL2014;Initial Catalog=RuleProcessorTest; Integrated Security=true"
      providerName="System.Data.SqlClient"/>
</connectionStrings>

Обратите внимание на названия "DocumentDbConnection". Теперь можно подключить DB-миграции, хотя это делать и не обязательно, потому что БД при первом к ней обращении будет создана автоматически.  Но я буду создавать базу данных "вручную" при помощи команды Update-database в Package Manager Console, а для этого надо подключить миграции Code First, выполнив команду:

PM> Enable-Migrations
Checking if the context targets an existing database...
Code First Migrations enabled for project Calabonga.StateProcessor.Demo.
PM> 

Для начала надо в файле настроек миграций Configuration, который появился в новой папке Migrations после выполнения команды, установить разрешения на перегенерацию базы данных:

namespace Calabonga.StateProcessor.Demo.Migrations
{
    using System.Data.Entity.Migrations;

    internal sealed class Configuration : DbMigrationsConfiguration<Data.ApplicationDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(Data.ApplicationDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }
}

Чтобы база создавалась с перезаписью (или сбросом) уже текщих настроек Code Fisrt, параметры 9 и 10 строки надо устрановить в True. После этого можно создать базу данных, выполнив команду в Package Manager. Я не стал создавать конкретную миграцию, а положился на автоматически созданную Code Fisrt:

PM> Update-Database
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
No pending explicit migrations.
Applying automatic migration: 201608190950399_AutomaticMigration.
Running Seed method.
PM> 

Если вы обратили внимание, то в файле настроек название каталога RuleProcessorTest, и именно с таким именем Code First и создал мне базу данных.

В общем, пришло время создавать контейнер для DI (dependency injection). Я создал новый класс со статичным методом:

namespace Calabonga.StateProcessor.Demo
{
    public class DependencyContainer {

        internal static IContainer Initialize() {

            // Создаем построитель для контейнера
            var builder = new ContainerBuilder();

            // Регистрируем правила для сущности Document
            builder.RegisterType<DraftRule>().As<IStatusRule<Document, EntityStatus>>();
            builder.RegisterType<InValidationRule>().As<IStatusRule<Document, EntityStatus>>();
            builder.RegisterType<CompleteRule>().As<IStatusRule<Document, EntityStatus>>();

            // Регистрируем статусы для сущности Document
            builder.RegisterType<DraftStatus>().As<EntityStatus>();
            builder.RegisterType<InValidationStatus>().As<EntityStatus>();
            builder.RegisterType<CompleteStatus>().As<EntityStatus>();

            // Возвращаем готовый контейнер
            return builder.Build();
        }
    }
}

В комментариях указал, что для чего и к чему: сначала создаем builder, который является неотемлемой частью Autofac, далее регистрируем правила и статусы.

RuleProcessor для сущности Document

Собственно говоря, пришло время создать то, ради чего всё затевалось. Создаем DocumentRuleProcessor, который призван управлять статусами документа (Document) на основании правил созданных нами правил.

namespace Calabonga.StateProcessor.Demo {

    /// <summary>
    /// Document RuleProcessor
    /// </summary>
    public class DocumentRuleProcessor : RuleProcessor<Document, EntityStatus> {
        public DocumentRuleProcessor(IEnumerable<IStatusRule<Document, EntityStatus>> rules, IEnumerable<EntityStatus> statuses)
            : base(rules, statuses) {
        }

        /// <summary>
        /// Returns initial status for entity Document
        /// </summary>
        /// <returns></returns>
        protected override IEntityStatus InitialStatus() {
            return Statuses.FirstOrDefault(x => x.Name.Equals(DocumentStatus.Draft.ToString()));
        }
    }
}

Класс является наследником RuleProcessor, обобщенными параметрами для которого является сущность Document и EntityStatus. Обратите внимание на конструктор этого класса. При создании DI-контейнера мы регистрируем в нем требуемый набор правил и статусов. Процессор правил будет управлять нашими статусами используя наши правила. Таким образом, достаточно просто зарегистрировать наш новый DocumentRuleProcessor в контейнере, и контейнер сам наполнит нужными зависимостями класс. Теперь класс регистрации контейнера выглядит так:

namespace Calabonga.StateProcessor.Demo
{
    public class DependencyContainer {

        internal static IContainer Initialize() {

            // Создаем построитель для контейнера
            var builder = new ContainerBuilder();

            // Регистрируем процессор обработки правил для Document
            builder.RegisterType<DocumentRuleProcessor>().AsSelf();

            // Регистрируем правила для сущности Document
            builder.RegisterType<DraftRule>().As<IStatusRule<Document, EntityStatus>>();
            builder.RegisterType<InValidationRule>().As<IStatusRule<Document, EntityStatus>>();
            builder.RegisterType<CompleteRule>().As<IStatusRule<Document, EntityStatus>>();

            // Регистрируем статусы для сущности Document
            builder.RegisterType<DraftStatus>().As<EntityStatus>();
            builder.RegisterType<InValidationStatus>().As<EntityStatus>();
            builder.RegisterType<CompleteStatus>().As<EntityStatus>();

            // Возвращаем готовый контейнер
            return builder.Build();
        }
    }
}

Пример использования процессора правил

Пришло время переключить статусы у какого-нибудь документа. Мне хочется показать всё и сразу, но давайте постепенно.

var processor = container.Resolve<DocumentRuleProcessor>();

Первым делом я получаю из DI-контейнера processor, который будет осуществлять все манипуляции с сущностями. Далее хочу продемонстрировать некоторые полезные утилиты. В процессоре есть метод-конвертор, который принимает несколько перегрузок. Этот метод является вспомогательной утилитой.

// Получение Enum из Guid
var result1 = processor.ToEnum<DocumentStatus>("Draft");
if (result1.Ok) {
    DocumentStatus status1 = result1.Result;
    Console.WriteLine($"Test 1 converted : {status1.ToString()}");
}

// Получение Enum из строки (String) по имени статуса
var result2 = processor.ToEnum<DocumentStatus>(Guid.Parse("49446068-0CCE-4168-B7AC-CD36E032F2BC"));
if (result2.Ok) {
    DocumentStatus status2 = result2.Result;
    Console.WriteLine($"Test 2 converted : {status2.ToString()}");
}

// Получение Enum из EntityStatus
var statusInProcessor = processor.Statuses.SingleOrDefault(x => x.Id == Guid.Parse("49446068-0CCE-4168-B7AC-CD36E032F2BC"));
var result3 = processor.ToEnum<DocumentStatus>(statusInProcessor);
if (result3.Ok) {
    DocumentStatus status3 = result3.Result;
    Console.WriteLine($"Test 3 converted : {status3.ToString()}");
}

Теперь наверное следует показать обновленное правило для статуса Draft, которое я адаптировал под оговоренные выше требования.

namespace Calabonga.StateProcessor.Demo.Rules {

    /// <summary>
    /// Rules validation for Draft status
    /// </summary>
    public class DraftRule : StatusRule<Document, EntityStatus> {

        public DraftRule(IEnumerable<EntityStatus> statuses) : base(statuses) { }

        public override bool CanEnter(RuleContext<Document, EntityStatus> context) {
            var hasAuthor =  !string.IsNullOrEmpty(context.Processor.Entity.Author);
            var hasNoStatus = context.Processor.Entity.EntityActiveStatus == Guid.Empty;
            return hasAuthor && hasNoStatus;
        }

        public override bool CanLeave(RuleContext<Document, EntityStatus> context) {
            return true;
        }

        protected override EntityStatus ValidationForStatus(IEnumerable<EntityStatus> statuses) {
            return statuses.SingleOrDefault(x => x.Name.Equals(DocumentStatus.Draft.ToString()));
        }
    }
}

В строке 11 проверяется наличие автора, а в строке 12 проверяется, что документ не имел никакого статута. Следовательно, если я использую вторую утилиту и создам документ

// Метод Create создает экземпляр класса Document с установленным по умолчанию статусом.
// Статус по умолчанию задается в DocumentRuleProcessor при переопределении метода InitialStatus
// При вызове метода Create будет проведена проверка правил
var document = processor.Create();

У меня нечего не получится, потому что метод Create создает экземплар и применяет правила при установки статуса по умолчанию. То есть, метод Create создаст объект Document  и попытается его перевести в статутс Draft, но так как у меня во вновь созданном документе нет автора, я получу пустой экземпляр класса. Наверняка, ваши правила и статусы сущности позволят вам создать экземпляр без проблем, а мне не повезло, буйство фантазии привело к такому стечению обстоятельств.

С моим набором правил и сущностью Document я могу создать экземпляр таким образом:

// Создаем экземпляр класса с указанием автора
var document2 = new Document {
    Author = "Director"
};

Теперь получаем статус черновика из списка статусов зарегистрированных в процессоре:

// Список всех статусов всегда доступен в процессоре
var statusDraft = processor.Statuses.Single(x => x.Name.Equals(DocumentStatus.Draft.ToString()));

Напоминаю, что процессор проверит соответствующие правила перед применением статуса.

// Устанавливаем статус документу
processor.UpdateStatus(document2, statusDraft);

Вот  как это выглядит вместе с сохранением данных в базу и чтеним данных из нее:

using (var db = new ApplicationDbContext()) {

    // сохраним созданый выше документ в базу данных
    db.Documents.Add(document2);
    db.SaveChanges();

    PrintStatus(document2, processor);

    // загрузим снова документ базы данных
    var documentInDatabase = db.Documents.SingleOrDefault(x => x.Id == document2.Id);
    if (documentInDatabase != null) {

        // Выберем новый статус для сущности. А для этого выбираем статус из всех доступных
        var entityStatus = processor.Statuses.SingleOrDefault(x => x.Name.Equals(DocumentStatus.InValidation.ToString()));

        document2.Number = "Номер строгой отчетности";

        // и обновляем его у сущности
        processor.UpdateStatus(documentInDatabase, entityStatus);

        PrintStatus(documentInDatabase, processor);

        // сохраним изменения
        db.SaveChanges();
    }
}

Заключение

В результате использования шаблона проектирования "Состояние" (Status) и, в частности RuleProcessor, мы получили следующие плюсы:

  • Фиксированный (но расширяемый) набор статусов для определенной сущности;
  • Фиксированный (но расширяемый) набор правил переключения статуса с проверкой "входа в статус" и "выхода из статуса";
  • Сущность (Document), которая не претерпела никаких изменений, за исключением только одно свойства типа Guid, а вся информация по статусам и правилам расположена в коде и сосредоточена в одном месте;
  • Правила и статусы могут быть загружены с любого сервера, из XML-файла, из любой сборки, а также могут варьироваться в зависимости от каких-либо параметров;
  • Unit-тестирование бизнес-логики на основе правил и статусов, определяющих четкое поведение в конкретных случаях;
  • Вспомогательные методы по работе со статусами.

но также не обошлось и без минусов:

  • Усложнение кода.

На этом позвольте закончить. Единственное что, хотелось бы еще предложить, так это Github на скачивание демонстрационного проекта. Пишите комментарии если будут вопросы.