Это хорошая практика, чтобы использовать список перечислений?

32

В настоящее время я работаю в системе, где есть пользователи, и у каждого пользователя есть одна или несколько ролей. Является ли хорошей практикой использование списка значений Enum для пользователя? Я не могу придумать ничего лучшего, но с этим не все в порядке.

enum Role{
  Admin = 1,
  User = 2,
}

class User{
   ...
   List<Role> Roles {get;set;}
}
Dexie
источник
6
Выглядит хорошо для меня, мне было бы интересно увидеть чьи-либо комментарии об обратном.
Дэвид Шолфилд
9
@MatthewRock - это довольно широкое обобщение. Список <T> довольно распространен в мире .NET.
Грэм
7
@MatthewRock .NET List - это массив, который имеет те же свойства для алгоритмов, которые вы упомянули.
Я так растерялся,
18
@ MatthewRock - нет, вы говорите о связных списках, когда вопрос, как и все остальные, говорит об общем интерфейсе списков.
Давор Адрало
5
Авто свойства являются отличительной чертой C # (get; set; синтаксис). Также список именования классов.
Jaypb

Ответы:

37

TL; DR: Обычно плохая идея использовать коллекцию перечислений, так как это часто приводит к плохому дизайну. Коллекция перечислений обычно требует отдельных системных объектов с определенной логикой.

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

Все примеры написаны на C #, я думаю, у вашего языка будут схожие конструкции, или вы сможете реализовать их самостоятельно.

1. Допустимо только одно значение

В этом случае значения являются исключительными, например,

public enum WorkStates
{
    Init,
    Pending,
    Done
}

Недопустимо иметь какую-то работу, которая одновременно Pendingи Done. Поэтому только одно из этих значений является действительным. Это хороший пример использования enum.

2. Допустима комбинация значений.
Этот случай также называется flags, C # предоставляет [Flags]атрибут enum для работы с ними. Идея может быть смоделирована как набор bools или bits, каждый из которых соответствует одному члену перечисления. Каждый член должен иметь значение силы два. Комбинации могут быть созданы с использованием побитовых операторов:

[Flags]
public enum Flags
{
    None = 0,
    Flag0 = 1, // 0x01, 1 << 0
    Flag1 = 2, // 0x02, 1 << 1
    Flag2 = 4, // 0x04, 1 << 2
    Flag3 = 8, // 0x08, 1 << 3
    Flag4 = 16, // 0x10, 1 << 4

    AFrequentlyUsedMask = Flag1 | Flag2 | Flag4,
    All = ~0 // bitwise negation of zero is all ones
}

Использование коллекции членов перечисления является излишним в таком случае, поскольку каждый член перечисления представляет только один бит, который установлен или не установлен. Я думаю, что большинство языков поддерживают такие конструкции. В противном случае вы можете создать его (например, использовать bool[]и адресовать его (1 << (int)YourEnum.SomeMember) - 1).

а) Все комбинации действительны

Хотя в некоторых простых случаях это нормально, коллекция объектов может оказаться более подходящей, поскольку вам часто требуется дополнительная информация или поведение в зависимости от типа.

[Flags]
public enum Flavors
{
    Strawberry = 1,
    Vanilla = 2,
    Chocolate = 4
}

public class IceCream
{
    private Flavors _scoopFlavors;

    public IceCream(Flavors scoopFlavors)
    {
        _scoopFlavors = scoopFlavors
    }

    public bool HasFlavor(Flavors flavor)
    {
        return _scoopFlavors.HasFlag(flavor);
    }
}

(примечание: это предполагает, что вы действительно заботитесь только о вкусах мороженого - вам не нужно моделировать мороженое как набор шариков и конуса)

б) Некоторые комбинации значений действительны, а некоторые нет

Это частый сценарий. Часто дело в том, что вы помещаете две разные вещи в одно перечисление. Пример:

[Flags]
public enum Parts
{
    Wheel = 1,
    Window = 2,
    Door = 4,
}

public class Building
{
    public Parts parts { get; set; }
}

public class Vehicle
{
    public Parts parts { get; set; }
}

Теперь, несмотря на то, что для s и s вполне допустимо Vehicleи Buildingдля Doors, и для Windows не очень обычно BuildingиметьWheel с.

В этом случае было бы лучше разбить перечисление на части и / или изменить иерархию объектов, чтобы получить вариант № 1 или № 2а).

Особенности дизайна

Каким-то образом перечисления, как правило, не являются движущими элементами в ОО, поскольку тип сущности может считаться аналогичным информации, которую обычно предоставляет перечисление.

Возьмите, например, IceCreamобразец из # 2, у IceCreamобъекта вместо флагов будет коллекцияScoop объектов.

Менее пуристом подход был бы для Scoopиметь Flavorсобственность. Пуристом подход был бы для Scoopбыть абстрактным базовым классом для VanillaScoop,ChocolateScoop , ... классов вместо этого.

Суть в том, что:
1. Не все, что является "типом чего-то", должно быть перечислением
2. Когда какой-либо член перечисления не является допустимым флагом в некотором сценарии, рассмотрите возможность разделения перечисления на несколько различных перечислений.

Теперь для вашего примера (немного изменено):

public enum Role
{
    User,
    Admin
}

public class User
{
    public List<Role> Roles { get; set; }
}

Я думаю, что этот точный случай должен быть смоделирован как (примечание: не очень расширяемый!):

public class User
{
    public bool IsAdmin { get; set; }
}

Другими словами - подразумевается, что Userэто User, дополнительная информация, является ли онAdmin .

Если вы иметь несколько ролей , которые не являются взаимоисключающими (например , Userможет быть Admin, Moderator, VIP, ... в то же время), что будет время хорошо использовать либо флаги ENUM или Abtract базовый класс или интерфейс.

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

С перечислением вам нужно было бы иметь логику в одном месте для всех ролей. Который побеждает цель ОО и возвращает вас к необходимости.

Представьте, что у вас Moderatorесть права на редактирование иAdmin редактирование, а также права на редактирование и удаление.

Подход Enum (вызывается Permissionsдля того, чтобы не смешивать роли и разрешения):

[Flags]
public enum Permissions
{
    None = 0
    CanEdit = 1,
    CanDelete = 2,

    ModeratorPermissions = CanEdit,
    AdminPermissions = ModeratorPermissions | CanDelete
}

public class User
{
    private Permissions _permissions;

    public bool CanExecute(IAction action)
    {
        if (action.Type == ActionType.Edit && _permissions.HasFlag(Permissions.CanEdit))
        {
            return true;
        }

        if (action.Type == ActionType.Delete && _permissions.HasFlag(Permissions.CanDelete))
        {
            return true;
        }

        return false;
    }
}

Классовый подход (это далеко не идеально, в идеале, вы хотели бы, чтобы IActionв шаблоне посетителя, но этот пост уже огромен ...):

public interface IRole
{
    bool CanExecute(IAction action);
}

public class ModeratorRole : IRole
{
    public virtual bool CanExecute(IAction action)
    {
         return action.Type == ActionType.Edit;
    }
}

public class AdminRole : ModeratorRole
{
     public override bool CanExecute(IAction action)
     {
         return base.CanExecute(action) || action.Type == ActionType.Delete;
     }
}

public class User
{
    private List<IRole> _roles;

    public bool CanExecute(IAction action)
    {
        _roles.Any(x => x.CanExecute(action));
    }
}

Использование перечисления может быть приемлемым подходом (например, производительность). Решение здесь зависит от требований моделируемой системы.

Зденек Елинек
источник
3
Я не думаю, что [Flags]подразумевается, что все комбинации действительны больше, чем int подразумевает, что все четыре миллиарда значений этого типа являются действительными. Это просто означает, что они могут быть объединены в одном поле - любые ограничения на комбинации принадлежат логике более высокого уровня.
Random832
Предупреждение: при использовании [Flags]вы должны установить значения в степени двух, иначе это не будет работать, как ожидалось.
Артуро Торрес Санчес
@ Random832 У меня никогда не было намерения сказать это, но я отредактировал ответ - надеюсь, теперь он понятнее.
Зденек Елинек
1
@ ArturoTorresSánchez Спасибо за вклад, я исправил ответ, а также добавил пояснительную записку об этом.
Зденек Елинек
Отличное объяснение! Фактически флаги очень хорошо соответствуют системным требованиям, однако использовать HashSets будет проще благодаря существующей реализации сервиса.
Dexie
79

Почему бы не использовать Set? При использовании списка:

  1. Легко добавить одну и ту же роль дважды
  2. Наивное сравнение списков здесь не будет работать должным образом: [Пользователь, Администратор] не совпадает с [Администратор, Пользователь]
  3. Сложные операции, такие как пересечение и слияние, не так просто реализовать

Если вы беспокоитесь о производительности, то, например, в Java есть EnumSetреализация, которая реализована как массив логических значений фиксированной длины (i-й логический элемент отвечает на вопрос, присутствует ли значение i-го Enum в этом наборе или нет). Например, EnumSet<Role>. Также смотрите EnumMap. Я подозреваю, что в C # есть нечто подобное.

Гудок
источник
35
На самом деле в Java EnumSets реализованы как битовые поля.
biziclop
4
Соответствующий вопрос SO по HashSet<MyEnum>сравнению с перечислениями flags: stackoverflow.com/q/9077487/87698
Heinzi
3
.NET имеет FlagsAttribute, что позволяет использовать побитовые операторы для объединения перечислений. Похоже на Java EnumSet. msdn.microsoft.com/en-US/LIBRARY/system.flagsattribute РЕДАКТИРОВАТЬ: Я должен был посмотреть вниз один ответ! у этого есть хороший пример этого.
ps2goat
4

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

enum MyEnum
{ 
    FIRST_CHOICE = 2,
    SECOND_CHOICE = 4,
    THIRD_CHOICE = 8
}
Рэй
источник
3
Любая причина, почему вы не используете 1?
Робби Ди
1
@RobbieDee нет, я мог бы использовать 1, 2, 4, 8 и т. Д. Вы правы
Rémi
5
Ну, если вы используете Java, он уже имеет, EnumSetкоторый реализует это для enums. У вас уже есть вся логическая арифметика, а также некоторые другие методы, облегчающие использование, плюс небольшая память и хорошая производительность.
13
2
@ fr13d Я парень с #, и поскольку у вопроса нет java-тега, я думаю, что этот ответ более применим
Rémi
1
Правильно, @ Реми. C # предоставляет [flags]атрибут, который @ Zdeněk Jelínek исследует в своем посте .
13
4

Является ли хорошей практикой использование списка значений Enum для пользователя?


Краткий ответ : да


Лучший ответ : да, enum определяет что-то в домене.


Ответ на этапе разработки: создание и использование классов, структур и т. Д., Которые моделируют домен с точки зрения самого домена.


Ответ на кодирование : вот как перебирать перечисления ...


Предполагаемые вопросы :

  • Должен ли я использовать строки?

    • Ответ: Нет.
  • Должен ли я использовать какой-то другой класс?

    • В дополнение к да. Вместо нет.
  • RoleДостаточно ли перечисления для использования вUser ?

    • Не имею представления. Это сильно зависит от других деталей конструкции, которые отсутствуют.

WTF Боб?

  • Это вопрос дизайна, который сформулирован в технических деталях языка программирования.

    • Это enumхороший способ определить «роли» в вашей модели. Так что Listроль - это хорошо.
  • enum намного превосходит струны.

    • Role является декларацией ВСЕХ ролей, которые существуют в домене.
    • Строка «Admin» - это строка с буквами "A" "d" "m" "i" "n"в указанном порядке. Это ничего, что касается домена.
  • Любые классы, которые вы могли бы разработать, чтобы дать концепцию Roleнекоторой жизни - функциональность - могут эффективно использоватьRole перечисление.

    • Например, вместо подкласса для каждого вида роли, есть Role свойство, которое сообщает, какую роль выполняет этот экземпляр класса.
    • User.RolesСписок является разумным , когда нам не нужно, или не хотят, конкретизированные объектов ролей.
    • И когда нам нужно создать экземпляр роли, enum - это недвусмысленный, безопасный для типов способ сообщить об этом RoleFactoryклассу; и, что немаловажно, читателю кода.
radarbob
источник
3

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

Более распространенным подходом является однопользовательский уровень, например система, администратор, опытный пользователь, пользователь и т. Д. Система может делать все, администрировать большинство вещей и так далее.

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

Таким образом, вы могли бы иметь:

Back office role 1
Front office role 2
Warehouse role 4
Systems role 8

Таким образом, если бы у пользователя было значение роли 6, тогда у него были бы роли Front office и Warehouse.

Робби Ди
источник
2

Используйте HashSet поскольку он предотвращает дублирование и имеет больше методов сравнения

В противном случае хорошее использование Enum

Добавить метод Boolean IsInRole (Role R)

и я бы пропустил набор

List<Role> roles = new List<Role>();
Public List<Role> Roles { get {return roles;} }
папараццо
источник
1

Упрощенный пример, который вы приводите, хорош, но обычно он более сложный, чем этот. Например, если вы собираетесь сохранить пользователей и роли в базе данных, вам следует определить роли в таблице базы данных, а не использовать перечисления. Это дает вам ссылочную целостность.

Мохер
источник
0

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

VikingoS говорит восстановить Монику
источник