Czy dobrą praktyką jest korzystanie z Listy Wyliczeń?

32

Obecnie pracuję w systemie, w którym są Użytkownicy, a każdy użytkownik ma jedną lub wiele ról. Czy dobrą praktyką jest używanie wartości Lista Enum na Użytkowniku? Nie mogę wymyślić nic lepszego, ale to nie jest w porządku.

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

class User{
   ...
   List<Role> Roles {get;set;}
}
Dexie
źródło
6
Wygląda mi to dobrze, chciałbym zobaczyć komentarze innych przeciwnie.
David Scholefield
9
@MatthewRock to całkiem szerokie uogólnienie. Lista <T> jest dość powszechna w świecie .NET.
Graham
7
@MatthewRock .NET List jest listą tablic, która ma te same właściwości dla wspomnianych algorytmów.
jestem tak zdezorientowany
18
@MatthewRock - nie, mówisz o listach POŁĄCZONYCH, gdy pytanie i wszyscy inni mówią o ogólnym interfejsie list.
Davor Ždralo 21.01.16
5
Auto właściwości są charakterystyczną cechą C # (get; set; składnia). Także nazewnictwo klas List.
jaypb

Odpowiedzi:

37

TL; DR: Zwykle złym pomysłem jest stosowanie zbioru wyliczeń, ponieważ często prowadzi to do złego projektu. Zbiór wyliczeń zwykle wymaga odrębnych elementów systemowych o określonej logice.

Konieczne jest rozróżnienie kilku przypadków użycia wyliczenia. Ta lista jest tylko z góry mojej głowy, więc może być więcej przypadków ...

Wszystkie przykłady są w języku C #, domyślam się, że twój wybrany język będzie miał podobne konstrukcje lub byłoby możliwe, abyś sam je zaimplementował.

1. Poprawna jest tylko jedna wartość

W takim przypadku wartości są wyłączne, np

public enum WorkStates
{
    Init,
    Pending,
    Done
}

Nieprawidłowe jest mieć pracę, która jest jednocześnie Pendingi Done. Dlatego tylko jedna z tych wartości jest poprawna. Jest to dobry przypadek użycia wyliczenia.

2. Poprawna jest kombinacja wartości.
Przypadek ten nazywa się także flagami, C # zapewnia [Flags]atrybut enum do pracy z nimi. Pomysł można modelować jako zbiór bools lub bits, z których każdy odpowiada jednemu członowi wyliczającemu. Każdy członek powinien mieć wartość potęgi dwóch. Kombinacje można tworzyć za pomocą operatorów bitowych:

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

Korzystanie ze zbioru elementów wyliczających jest w takim przypadku przesadą, ponieważ każdy element wyliczający reprezentuje tylko jeden bit, który jest albo ustawiony, albo nieuzbrojony. Chyba większość języków obsługuje takie konstrukcje. W przeciwnym razie możesz je utworzyć (np. Użyć bool[]i rozwiązać przez (1 << (int)YourEnum.SomeMember) - 1).

a) Wszystkie kombinacje są ważne

Chociaż są one w porządku w niektórych prostych przypadkach, kolekcja obiektów może być bardziej odpowiednia, ponieważ często potrzebujesz dodatkowych informacji lub zachowania w zależności od typu.

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

(uwaga: zakłada się, że naprawdę zależy ci tylko na smakach lodów - że nie musisz modelować lodów jako kolekcji gałek i stożka)

b) Niektóre kombinacje wartości są prawidłowe, a niektóre nie

To częsty scenariusz. Często może się zdarzyć, że umieścisz dwie różne rzeczy w jednym wyliczeniu. Przykład:

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

Teraz, mimo że jest w pełni poprawny zarówno dla, jak Vehiclei Buildingdla Doorsi i Windows, nie jest to zbyt często dla Buildings i dla nich Wheel.

W takim przypadku lepiej byłoby rozbić wyliczenie na części i / lub zmodyfikować hierarchię obiektów, aby osiągnąć przypadek nr 1 lub nr 2a.

Uwagi dotyczące projektu

W jakiś sposób wyliczenia zwykle nie są elementami napędzającymi w OO, ponieważ typ bytu można uznać za podobny do informacji, które zwykle dostarcza wyliczenie.

Weźmy np. IceCreamPróbkę z nr 2, IceCreamjednostka zamiast flag miałaby kolekcję Scoopobiektów.

Mniej purystycznym podejściem byłoby Scoopposiadanie Flavorwłasności. Podejście purist byłoby dla Scoopbyć abstrakcyjną klasą bazową dla VanillaScoop, ChocolateScoop... zamiast klas.

Najważniejsze jest to, że:
1. Nie wszystko, co jest „rodzajem czegoś”, musi być wyliczone.
2. Jeśli jakiś element wyliczeniowy nie jest prawidłową flagą w niektórych scenariuszach, rozważ podzielenie wyliczenia na wiele różnych wyliczeń.

Teraz twój przykład (nieco zmieniony):

public enum Role
{
    User,
    Admin
}

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

Myślę, że ten dokładny przypadek powinien zostać zamodelowany jako (uwaga: nie do rozszerzenia!):

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

Innymi słowy - domyślnie jest, że Userjest to User, dodatkowe informacje to, czy on jest Admin.

Jeśli masz mieć wiele ról, które nie wykluczają (np Usermoże być Admin, Moderator, VIP, ... w tym samym czasie), to byłby dobry czas, aby użyć jednej flagi ENUM lub abtract klasa bazowa lub interfejs.

Wykorzystanie klasy do przedstawienia Roleprowadzi do lepszego rozdzielenia obowiązków, w których osoba Rolemoże być odpowiedzialna za podjęcie decyzji, czy może wykonać dane działanie.

Dzięki wyliczeniu musisz mieć logikę w jednym miejscu dla wszystkich ról. Który pokonuje cel OO i przywraca cię do imperatywu.

Wyobraź sobie, że a Moderatorma prawa do edycji oraz Adminma zarówno prawo do edycji , jak i do usuwania.

Podejście Enum (wywoływane, Permissionsaby nie mieszać ról i uprawnień):

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

Podejście klasowe (jest to dalekie od ideału, idealnie, chciałbyś mieć IActionwzór dla odwiedzających, ale ten post jest już ogromny ...):

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

Korzystanie z wyliczenia może być jednak akceptowalnym podejściem (np. Wydajność). Decyzja zależy od wymagań modelowanego systemu.

Zdeněk Jelínek
źródło
3
Nie sądzę, że [Flags]implikuje to, że wszystkie kombinacje są ważne tak samo jak int oznacza, że ​​wszystkie cztery miliardy wartości tego typu są ważne. Oznacza to po prostu, że można je łączyć w jednym polu - wszelkie ograniczenia kombinacji należą do logiki wyższego poziomu.
Random832
Ostrzeżenie: podczas używania [Flags]należy ustawić wartości na potęgę dwóch, w przeciwnym razie nie będzie działać zgodnie z oczekiwaniami.
Arturo Torres Sánchez
@ Random832 Nigdy nie miałem zamiaru tego mówić, ale zredagowałem odpowiedź - mam nadzieję, że teraz jest bardziej przejrzysta.
Zdeněk Jelínek
1
@ ArturoTorresSánchez Dziękuję za wkład, naprawiłem odpowiedź, a także dodałem notatkę wyjaśniającą na ten temat.
Zdeněk Jelínek
Świetne wyjaśnienie! W rzeczywistości flagi bardzo dobrze pasują do wymagań systemowych, jednak korzystanie z HashSets będzie łatwiejsze ze względu na istniejącą implementację usługi.
Dexie,
79

Dlaczego nie użyć zestawu? Jeśli używasz listy:

  1. Łatwo jest dwukrotnie dodać tę samą rolę
  2. Naiwne porównywanie list nie będzie tu działać poprawnie: [Użytkownik, Administrator] to nie to samo, co [Administrator, Użytkownik]
  3. Złożone operacje, takie jak przecinanie i scalanie, nie są łatwe do wdrożenia

Jeśli EnumSetobawiasz się o wydajność, to na przykład w Javie jest taka, która jest implementowana jako tablica boolean o stałej długości (i-ty element boolowski odpowiada na pytanie, czy i-ta wartość Enum jest obecna w tym zestawie, czy nie). Na przykład EnumSet<Role>. Zobacz także EnumMap. Podejrzewam, że C # ma coś podobnego.

gudok
źródło
35
W rzeczywistości w Javie EnumSetsą implementowane jako pola bitowe.
biziclop
4
Powiązane pytanie SO na temat HashSet<MyEnum>enums vs. flags: stackoverflow.com/q/9077487/87698
Heinzi
3
.NET ma FlagsAttribute, co pozwala używać operatorów bitowych do konkatowania wyliczeń. Brzmi podobnie do Java EnumSet. msdn.microsoft.com/en-US/LIBRARY/system.flagsattribute EDYCJA: Powinienem był spojrzeć w dół na jedną odpowiedź! ma tego dobry przykład.
ps2goat
4

Napisz swoje wyliczenie, aby je połączyć. Używając wykładniczej podstawy 2, możesz połączyć je wszystkie w jednym wyliczeniu, mieć 1 właściwość i możesz je sprawdzić. Wasze wyliczenie powinno to znieść

enum MyEnum
{ 
    FIRST_CHOICE = 2,
    SECOND_CHOICE = 4,
    THIRD_CHOICE = 8
}
Rémi
źródło
3
Jest jakiś powód, dla którego nie używasz 1?
Robbie Dee
1
@RobbieDee nie efektywnie mogłem użyć 1, 2, 4, 8 itd. Masz rację
Rémi
5
Cóż, jeśli używasz Javy, już ma ona EnumSetimplementującą to dla enums. Wyodrębniono już całą arytmetykę logiczną, a także kilka innych metod ułatwiających jej użycie, a także niewielką pamięć i dobrą wydajność.
13
2
@ fr13d Jestem facetem # i ponieważ pytanie nie ma tagu Java, myślę, że ta odpowiedź dotyczy w szerszym zakresie
Rémi
1
Racja, @ Rémi. C # zapewnia [flags]atrybut, który @ Zdeněk Jelínek bada w swoim poście .
fr13d
4

Czy dobrą praktyką jest używanie wartości Lista Enum na Użytkowniku?


Krótka odpowiedź : tak


Lepsza krótka odpowiedź : tak, wyliczenie definiuje coś w dziedzinie.


Odpowiedź w czasie projektowania : Twórz i używaj klas, struktur itp., Które modelują domenę pod względem samej domeny.


Odpowiedź czasu kodowania : oto jak wyliczać procy ...


Wnioskowane pytania :

  • Czy powinienem używać ciągów znaków?

    • odpowiedź: nie
  • Czy powinienem użyć innej klasy?

    • Oprócz tak. Zamiast nie
  • Czy Rolelista wyliczeń jest wystarczająca do użycia w User?

    • Nie mam pojęcia. Jest wysoce zależny od innych szczegółów projektu, które nie są widoczne.

WTF Bob?

  • To pytanie projektowe jest ujęte w szczegóły techniczne języka programowania.

    • Jest enumto dobry sposób na zdefiniowanie „Roli” w twoim modelu. Tak więc jedna Listz ról jest dobrą rzeczą.
  • enum jest znacznie lepszy od łańcuchów.

    • Role jest deklaracją WSZYSTKICH ról, które istnieją w domenie.
    • Ciąg „Admin” jest ciągiem z literami "A" "d" "m" "i" "n"w tej kolejności. Jest to nic, jeśli chodzi o domenę.
  • Wszelkie klasy, które zaprojektujesz, aby nadać koncepcji Rolepewnej funkcji życiowej - mogą dobrze wykorzystać Rolewyliczenie.

    • Na przykład, zamiast podklasy dla każdego rodzaju roli, należy mieć Rolewłaściwość, która mówi, jaką rolę pełni ta instancja klasy.
    • User.RolesLista jest uzasadnione, gdy nie trzeba, albo nie chcą, instancja obiektów ról.
    • A kiedy musimy utworzyć rolę, wyliczenie jest niejednoznacznym, bezpiecznym typem sposobu komunikowania go RoleFactoryklasie; i nie bez znaczenia dla czytnika kodów.
radarbob
źródło
3

Nie widziałem tego, ale nie widzę powodu, dla którego to by nie działało. Możesz jednak użyć posortowanej listy, która chroni ją jednak przed duplikatami.

Bardziej powszechnym podejściem jest poziom pojedynczego użytkownika, np. System, administrator, użytkownik zaawansowany, użytkownik itp. System może zrobić wszystko, administrować większością rzeczy i tak dalej.

Jeśli miałbyś ustawić wartości ról jako potęgi dwóch, możesz zapisać rolę jako int i wyprowadzić role, jeśli naprawdę chciałbyś przechowywać je w jednym polu, ale może to nie odpowiadać każdemu.

Więc możesz mieć:

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

Więc jeśli użytkownik miałby rolę o wartości 6, wówczas miałby role Front office i Warehouse.

Robbie Dee
źródło
2

Użyj HashSet, ponieważ zapobiega duplikatom i ma więcej metod porównywania

W przeciwnym razie dobre użycie Enum

Dodaj metodę Boolean IsInRole (Role R)

i pomijałem zestaw

List<Role> roles = new List<Role>();
Public List<Role> Roles { get {return roles;} }
paparazzo
źródło
1

Podany uproszczony przykład jest w porządku, ale zazwyczaj jest bardziej skomplikowany. Na przykład, jeśli zamierzasz utrwalić użytkowników i role w bazie danych, powinieneś zdefiniować role w tabeli bazy danych zamiast używać wyliczeń. To zapewnia integralność referencyjną.

Moher
źródło
0

Jeśli system - może to być oprogramowanie, system operacyjny lub organizacja - zajmuje się rolami i uprawnieniami, przychodzi czas, kiedy przydatne jest dodawanie ról i zarządzanie dla nich uprawnieniami.
Jeśli można to zarchiwizować poprzez zmianę kodu źródłowego, może być dobrze trzymać się wyliczeń. Ale w pewnym momencie użytkownik systemu chce mieć możliwość zarządzania rolami i uprawnieniami. Niż enum stają się bezużyteczne.
Zamiast tego będziesz chciał mieć konfigurowalną strukturę danych. tj. klasa UserRole, która również posiada zestaw uprawnień przypisanych do roli.
Klasa użytkownika miałaby zestaw ról. Jeśli zachodzi potrzeba sprawdzenia uprawnień użytkownika, tworzony jest łączny zestaw wszystkich uprawnień do uprawnień i sprawdzany, czy zawiera dane uprawnienie.

VikingoS mówi: Przywróć Monikę
źródło