¿Es una buena práctica usar List of Enums?

32

Actualmente estoy trabajando en un sistema donde hay usuarios, y cada usuario tiene uno o varios roles. ¿Es una buena práctica usar la Lista de valores de Enum en el Usuario? No puedo pensar en nada mejor, pero esto no se siente bien.

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

class User{
   ...
   List<Role> Roles {get;set;}
}
Dexie
fuente
66
Me parece bien, me interesaría ver los comentarios de alguien más en sentido contrario.
David Scholefield
9
@MatthewRock es una generalización bastante amplia. La lista <T> es bastante común en el mundo .NET.
Graham
77
@MatthewRock .NET List es una lista de arrays, que tiene las mismas propiedades para los algoritmos que menciona.
Estoy tan confundido el
18
@MatthewRock: no, estás hablando de listas VINCULADAS, cuando la pregunta, y todos los demás, están hablando de la interfaz de Lista genérica.
Davor Ždralo
55
Las propiedades automáticas son una característica distintiva de C # (get; set; syntax). También el nombre de la clase Lista.
jaypb

Respuestas:

37

TL; DR: Por lo general, es una mala idea usar una colección de enumeraciones, ya que a menudo conduce a un mal diseño. Una colección de enumeraciones generalmente requiere entidades del sistema distintas con lógica específica.

Es necesario distinguir entre algunos casos de uso de enum. Esta lista es solo la parte superior de mi cabeza, por lo que podría haber más casos ...

Todos los ejemplos están en C #, supongo que su lenguaje de elección tendrá construcciones similares o podría implementarlas usted mismo.

1. Solo un valor único es válido

En este caso, los valores son exclusivos, p. Ej.

public enum WorkStates
{
    Init,
    Pending,
    Done
}

No es válido tener un trabajo que sea a la vez Pendingy Done. Por lo tanto, solo uno de estos valores es válido. Este es un buen caso de uso de enum.

2. Una combinación de valores es válida
Este caso también se llama banderas, C # proporciona un [Flags]atributo de enumeración para trabajar con estos. La idea se puede modelar como un conjunto debool so bits con cada uno correspondiente a un miembro enum. Cada miembro debe tener un valor de poder de dos. Se pueden crear combinaciones usando operadores bit a bit:

[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
}

El uso de una colección de miembros de enumeración es una exageración en tal caso, ya que cada miembro de enumeración solo representa un bit que está activado o desactivado. Supongo que la mayoría de los idiomas admiten construcciones como esta. De lo contrario, puede crear uno (por ejemplo, usarlo bool[]y abordarlo (1 << (int)YourEnum.SomeMember) - 1).

a) Todas las combinaciones son válidas.

Si bien estos están bien en algunos casos simples, una colección de objetos puede ser más apropiada ya que a menudo necesita información adicional o comportamiento basado en el tipo.

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

(nota: esto supone que solo te importan realmente los sabores del helado, que no necesitas modelar el helado como una colección de bolas y un cono)

b) Algunas combinaciones de valores son válidas y otras no

Este es un escenario frecuente. El caso a menudo podría ser que estás poniendo dos cosas diferentes en una enumeración. Ejemplo:

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

Ahora, si bien es completamente válido para ambos Vehicley Buildingpara tener Doors y Windows, no es muy común que Buildings tengaWheel s.

En este caso, sería mejor dividir la enumeración en partes y / o modificar la jerarquía de objetos para lograr el caso # 1 o # 2a).

Consideraciones de diseño

De alguna manera, las enumeraciones tienden a no ser los elementos impulsores en OO ya que el tipo de entidad puede considerarse similar a la información que generalmente proporciona una enumeración.

Tome, por ejemplo, la IceCreammuestra del n. ° 2, la IceCreamentidad en lugar de banderas tendría una colecciónScoop objetos.

El enfoque menos purista sería Scooptener una Flavorpropiedad. El enfoque purista sería que el Scoopser una clase base abstracta para VanillaScoop, ChocolateScoop, ... clases en su lugar.

La conclusión es que:
1. No todo lo que es un "tipo de algo" debe ser enumeración
2. Cuando algún miembro de enumeración no es un indicador válido en algún escenario, considere dividir la enumeración en varias enumeraciones distintas.

Ahora para su ejemplo (ligeramente cambiado):

public enum Role
{
    User,
    Admin
}

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

Creo que este caso exacto debería modelarse como (nota: ¡no es realmente extensible!):

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

En otras palabras, está implícito que el Useres un User, la información adicional es si él es unAdmin .

Si se llega a tener múltiples roles que no son exclusivos (por ejemplo, Userpuede haber Admin, Moderator, VIP, ... al mismo tiempo), que sería un tiempo bueno para usar cualquiera de las banderas de enumeración o una clase base abtract o interfaz.

El uso de una clase para representar a Roleconduce a una mejor separación de responsabilidades donde un Rolepuede tener la responsabilidad de decidir si puede realizar una acción determinada.

Con una enumeración, necesitaría tener la lógica en un solo lugar para todos los roles. Lo cual derrota el propósito de OO y lo lleva de vuelta al imperativo.

Imagine que a Moderatortiene derechos de edición y Admintiene derechos de edición y eliminación.

Enum enfoque (llamado Permissionspara no mezclar roles y permisos):

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

Enfoque de clase (esto está lejos de ser perfecto, idealmente, desearía IActionun patrón de visitante pero esta publicación ya es enorme ...):

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

Sin embargo, el uso de una enumeración puede ser un enfoque aceptable (por ejemplo, rendimiento). La decisión aquí depende de los requisitos del sistema modelado.

Zdeněk Jelínek
fuente
3
No creo que eso [Flags]implique que todas las combinaciones sean válidas más de lo que un int implica que los cuatro mil millones de valores de ese tipo son válidos. Simplemente significa que se pueden combinar dentro de un solo campo: cualquier restricción en las combinaciones pertenece a la lógica de nivel superior.
Random832
Advertencia: cuando se usa [Flags], debe establecer los valores en potencias de dos, de lo contrario no funcionará como se esperaba.
Arturo Torres Sánchez
@ Random832 Nunca tuve la intención de decir eso, pero edité la respuesta, espero que ahora esté más claro.
Zdeněk Jelínek
1
@ ArturoTorresSánchez Gracias por el aporte, arreglé la respuesta y también agregué una nota explicativa al respecto.
Zdeněk Jelínek
¡Gran explicación! En realidad, los indicadores se ajustan muy bien a los requisitos del sistema, sin embargo, será más fácil usar HashSets debido a la implementación del servicio existente.
Dexie
79

¿Por qué no usar Set? Si usa List:

  1. Es fácil agregar el mismo rol dos veces
  2. La comparación ingenua de listas no funcionará aquí correctamente: [Usuario, Administrador] no es lo mismo que [Administrador, Usuario]
  3. Las operaciones complejas como la intersección y la fusión no son fáciles de implementar.

Si le preocupa el rendimiento, entonces, por ejemplo, en Java, existe el EnumSetque se implementa como una matriz de longitud fija de booleanos (el elemento booleano responde a la pregunta de si el valor Enum está presente en este conjunto o no). Por ejemplo, EnumSet<Role>. Además, ver EnumMap. Sospecho que C # tiene algo similar.

gudok
fuente
35
En realidad, en Java EnumSets se implementan como campos de bits.
biziclop
44
Pregunta SO relacionada en HashSet<MyEnum>enumeraciones vs. banderas: stackoverflow.com/q/9077487/87698
Heinzi
3
.NET tiene FlagsAttribute, que le permite usar operadores bit a bit para concat enumeraciones. Suena similar al Java EnumSet. msdn.microsoft.com/en-US/LIBRARY/system.flagsattribute EDIT: ¡Debería haber buscado una respuesta! Tiene un buen ejemplo de esto.
ps2goat
4

Escriba su enumeración para que pueda combinarlos. Al usar la base 2 exponencial, puede combinarlos todos en una enumeración, tener 1 propiedad y puede verificarlos. Tu enumeración debería ser lile esto

enum MyEnum
{ 
    FIRST_CHOICE = 2,
    SECOND_CHOICE = 4,
    THIRD_CHOICE = 8
}
Rémi
fuente
3
¿Alguna razón por la que no estás usando 1?
Robbie Dee
1
@RobbieDee sin efecto, podría haber usado 1, 2, 4, 8, etc. tienes razón
Rémi
55
Bueno, si usas Java, ya tiene lo EnumSetque implementa esto para enums. Tiene toda la aritmética booleana ya abstraída, además de algunos otros métodos para que sea más fácil de usar, además de memoria pequeña y buen rendimiento.
viernes 13 de
2
@ fr13d Soy un chico # ac y como la pregunta no tiene etiqueta java, creo que esta respuesta se aplica más en general
Rémi
1
Correcto, @ Rémi. C # proporciona el [flags]atributo, que @ Zdeněk Jelínek explora en su publicación .
fr13d
4

¿Es una buena práctica usar la Lista de valores de Enum en el Usuario?


Respuesta corta : sí


Mejor respuesta corta : Sí, la enumeración define algo en el dominio.


Respuesta en tiempo de diseño : crea y usa clases, estructuras, etc. que modelen el dominio en términos del dominio mismo.


Respuesta de tiempo de codificación : aquí se explica cómo codificar las enumeraciones ...


Las preguntas inferidas :

  • ¿Debo usar cadenas?

    • respuesta: No.
  • ¿Debo usar alguna otra clase?

    • Además de sí. En lugar de no.
  • ¿La Rolelista enum es suficiente para usar User?

    • No tengo idea. Depende mucho de otros detalles de diseño que no están en evidencia.

WTF Bob?

  • Esta es una pregunta de diseño que se enmarca en los tecnicismos del lenguaje de programación.

    • Una enumes una buena manera de definir "Roles" en su modelo. Entonces, una Listde las funciones es algo bueno.
  • enum Es muy superior a las cuerdas.

    • Role es una declaración de TODOS los roles que existen en el dominio.
    • La cadena "Admin" es una cadena con las letras "A" "d" "m" "i" "n", en ese orden. No es nada en lo que respecta al dominio.
  • Cualquier clase que pueda diseñar para dar el concepto de Rolevida (funcionalidad) puede hacer un buen uso de la Roleenumeración.

    • Por ejemplo, en lugar de una subclase para cada tipo de rol, tenga una Rolepropiedad que indique qué tipo de rol es esta instancia de clase.
    • Una User.Roleslista es razonable cuando no necesitamos, o no queremos, objetos de roles instanciados.
    • Y cuando necesitamos crear una instancia de un rol, la enumeración es una forma segura y poco ambigua de comunicarlo a una RoleFactoryclase; y, no insignificantemente, para el lector de código.
radarbob
fuente
3

Eso no es algo que haya visto, pero no veo ninguna razón por la que no funcionaría. Sin embargo, es posible que desee usar una lista ordenada para que se defienda contra los engañados.

Un enfoque más común es un nivel de usuario único, por ejemplo, sistema, administrador, usuario avanzado, usuario, etc. El sistema puede hacer todo, administrar la mayoría de las cosas, etc.

Si estableciera los valores de los roles como poderes de dos, podría almacenar el rol como int y derivar los roles si realmente quisiera almacenarlos en un solo campo, pero eso podría no ser del gusto de todos.

Entonces podrías tener:

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

Entonces, si un usuario tiene un valor de rol de 6, entonces tendría los roles de Front office y Warehouse.

Robbie Dee
fuente
2

Use HashSet ya que evita duplicados y tiene más métodos de comparación

De lo contrario, buen uso de Enum

Agregar un método Boolean IsInRole (Rol R)

y me saltaría el set

List<Role> roles = new List<Role>();
Public List<Role> Roles { get {return roles;} }
paparazzo
fuente
1

El ejemplo simplista que das está bien, pero generalmente es más complicado que eso. Por ejemplo, si va a persistir los usuarios y roles en una base de datos, debe definir los roles en una tabla de base de datos en lugar de usar enumeraciones. Eso te da integridad referencial.

Mohair
fuente
0

Si un sistema, podría ser un software, un sistema operativo o una organización, trata con roles y permisos, llega un momento en que es útil agregar roles y administrar permisos para ellos.
Si esto puede archivarse alterando el código fuente, podría estar bien, seguir con las enumeraciones. Pero en algún momento el usuario del sistema quiere poder administrar roles y permisos. Que las enumeraciones se vuelven inutilizables.
En su lugar, querrá tener una estructura de datos configurable. es decir, una clase UserRole que también posee un conjunto de permisos asignados al rol.
Una clase de usuario tendría un conjunto de roles. Si es necesario verificar el permiso del usuario, se crea un conjunto de unión de todos los permisos de roles y se verifica si contiene el permiso en cuestión.

VikingoS dice reinstalar a Mónica
fuente