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 Pending
i 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 bool
s lub bit
s, 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 Vehicle
i Building
dla Door
si i Window
s, nie jest to zbyt często dla Building
s 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. IceCream
Próbkę z nr 2, IceCream
jednostka zamiast flag miałaby kolekcję Scoop
obiektów.
Mniej purystycznym podejściem byłoby Scoop
posiadanie Flavor
własności. Podejście purist byłoby dla Scoop
być 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 User
jest to User
, dodatkowe informacje to, czy on jest Admin
.
Jeśli masz mieć wiele ról, które nie wykluczają (np User
moż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 Role
prowadzi do lepszego rozdzielenia obowiązków, w których osoba Role
moż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 Moderator
ma prawa do edycji oraz Admin
ma zarówno prawo do edycji , jak i do usuwania.
Podejście Enum (wywoływane, Permissions
aby 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ć IAction
wzó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.