Русский
Русский
English
Статистика
Реклама

Из песочницы Enum и switch, и что с ними не так

image


Часто ли у вас было такое, что вы добавляли новое значение в enum и потом тратили часы на то, чтобы найти все места его использования, а затем добавить новый case, чтобы не получить ArgumentOutOfRangeException во время исполнения?


Идея


Если проблема состоит только в switch операторе и отслеживании новых типов, тогда давайте избавимся от них!


Идея состоит в том, чтобы заменить использование switch паттерном visitor.


Пример 1


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


Определим файл DocumentType.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 разработчика. Но иногда это очень ухудшает упрощает понимание кода.


Опишем visitor который будет искать в базе нужный документ DatabaseSearchVisitor.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;    }}

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


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);}

Дополнение


Если нужно быстрее


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


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


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);        }    }

Сам visitor остаётся прежним, только меняем class на 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();}

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


Читабельность и in-place реализация


Если нужно реализовать логику только в одном месте, то часто visitor громоздко и не удобно. Поэтому есть альтернативное решение match.


Сразу пример со структурой:


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();}

Использование match:


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

Итог


При добавлении нового значения в enum необходимо:


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

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


Это все еще не серебряная пуля, но может здорово помочь в работе с enum.


Ссылки


Источник: habr.com
К списку статей
Опубликовано: 04.09.2020 16:11:02
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование

Net

Enum

Programming

Extension

Visitor

Design pattern

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2023, personeltest.ru