Enum И Switch, И Что С Ними Не Так



Enum и switch, и что с ними не так

Вы когда-нибудь добавляли новое значение в перечисление, а затем тратили часы, пытаясь найти все места, где оно используется, а затем добавляли новый случай, чтобы избежать получения исключения ArgumentOutOfRangeException во время выполнения?



Идея

Если проблема заключается только в операторе переключения и отслеживании новых типов, то давайте избавимся от них! Идея состоит в том, чтобы заменить использование переключателя шаблоном посетителя.



Пример 1

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

Давайте определим файл ТипДокумента.

cs :

  
  
  
  
  
  
  
  
  
  
   

public enum DocumentType { Invoice, PrepaymentAccount } public interface IDocumentVisitor<out T> { T VisitInvoice(); T VisitPrepaymentAccount(); } public static class DocumentTypeExt { public static T Accept<T>(this DocumentType self, IDocumentVisitor<T> visitor) { switch (self) { case DocumentType.Invoice: return visitor.VisitInvoice(); case DocumentType.PrepaymentAccount: return visitor.VisitPrepaymentAccount(); default: throw new ArgumentOutOfRangeException(nameof(self), self, null); } } }

И да, я предлагаю определять все связанные типы в одном файле, что не является идиомой для .

Net-разработчика.

Но иногда это очень ухудшается делает код более понятным.

Опишем посетителя, который будет искать в базе данных нужный документ. База данныхSearchVisitor.cs :

public class DatabaseSearchVisitor : IDocumentVisitor<IDocument> { private ApiId _id; private Database _db; public DatabaseSearchVisitor(ApiId id, Database db) { _id = id; _db = db; } public IDocument VisitInvoice() => _db.SearchInvoice(_id); public IDocument VisitPrepaymentAccount() => _db.SearchPrepaymentAccount(_id); }

И затем его использование:

public void UpdateStatus(ApiDoc doc) { var searchVisitor = new DatabaseSearchVisitor(doc.Id, _db); var databaseDocument = doc.Type.Accept(searchVisitor); databaseDocument.Status = doc.Status; _db.SaveChanges(); }



Пример 2

У нас есть такие события:

public enum PurseEventType { Increase, Decrease, Block, Unlock } public sealed class PurseEvent { public PurseEventType Type { get; } public string Json { get; } public PurseEvent(PurseEventType type, string json) { Type = type; Json = json; } }

Мы хотим отправлять пользователю уведомления о событиях определенного типа.

Затем мы реализуем посетителя:

public interface IPurseEventTypeVisitor<out T> { T VisitIncrease(); T VisitDecrease(); T VisitBlock(); T VisitUnlock(); } public sealed class PurseEventTypeNotificationVisitor : IPurseEventTypeVisitor<Missing> { private readonly INotificationManager _notificationManager; private readonly PurseEventParser _eventParser; private readonly PurseEvent _event; public PurseEventTypeNotificationVisitor(PurseEvent @event, PurseEventParser eventParser, INotificationManager notificationManager) { _notificationManager = notificationManager; _event = @event; _eventParser = eventParser; } public Missing VisitIncrease() => Missing.Value; public Missing VisitDecrease() => Missing.Value; public Missing VisitBlock() { var blockEvent = _eventParser.ParseBlock(_event); _notificationManager.NotifyBlockPurseEvent(blockEvent); return Missing.Value; } public Missing VisitUnlock() { var blockEvent = _eventParser.ParseUnlock(_event); _notificationManager.NotifyUnlockPurseEvent(blockEvent); return Missing.Value; } }

Ради этого примера мы ничего не будем возвращать.

Для этого вы можете использовать тип Missing из System.Reflection или написать тип Unit. В реальном проекте Result будет возвращен, например, с информацией об ошибке, если таковая имеется.

И пример использования:

public void SendNotification(PurseEvent @event) { var notificationVisitor = new PurseEventTypeNotificationVisitor(@event, _eventParser, _notificationManager); @event.Type.Accept(notificationVisitor); }



Добавление



Если вам нужно это быстрее

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

Тогда код изменится следующим образом.

Метод расширения:

public static T Accept<TVisitor, T>(this DocumentType self, in TVisitor visitor) where TVisitor : IDocumentVisitor<T> { switch (self) { case DocumentType.Invoice: return visitor.VisitInvoice(); case DocumentType.PrepaymentAccount: return visitor.VisitPrepaymentAccount(); default: throw new ArgumentOutOfRangeException(nameof(self), self, null); } }

Сам посетитель остается прежним, мы просто меняем класс на struct. Да и сам код обновления документа выглядит не так удобно, но работает быстро:

public void UpdateStatus(ApiDoc doc) { var searchVisitor = new DatabaseSearchVisitor(doc.Id, _db); var databaseDocument = doc.Type.Accept<DatabaseSearchVisitor, IDocument>(searchVisitor); databaseDocument.Status = doc.Status; _db.SaveChanges(); }

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



Читабельность и реализация на месте

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

Поэтому есть альтернативное решение соответствовать .

Просто пример со структурой:

public static T Match<T>(this DocumentType self, Func<T> invoiceCase, Func<T> prepaymentAccountCase) { var visitor = new FuncVisitor<T>(invoiceCase, prepaymentCase); return self.Accept<FuncVisitor<T>, T>(visitor); }

Сам FuncVisitor :

public readonly struct FuncVisitor<T> : IDocumentVisitor<T> { private readonly Func<T> _invoiceCase; private readonly Func<T> _prepaymentAccountCase; public FuncVisitor(Func<T> invoiceCase, Func<T> prepaymentAccountCase) { _invoiceCase = invoiceCase; _prepaymentAccountCase = prepaymentAccountCase; } public T VisitInvoice() => _invoiceCase(); public T VisitPrepaymentAccount() => _prepaymentAccountCase(); }

Применение соответствовать :

public void UpdateStatus(ApiDoc doc) { var databaseDocument = doc.Type.Match( () => _db.SearchInvoice(doc.Id), () => _db.SearchPrepaymentAccount(doc.Id) ); databaseDocument.Status = doc.Status; _db.SaveChanges(); }



Нижняя граница

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

  2. Добавьте его использование в метод расширения.

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

Это все еще не панацея, но может оказаться большим подспорьем при работе с перечислениями.



Ссылки

Теги: #программирование #.

NET #шаблон проектирования #enum #программирование #расширение #посетитель

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.