Modifikator dostępu Elvis w C#: jak zaimplementować kontrolowany dostęp do prywatnych członków bez naruszenia inkapsulacji
Elvis – modifikator dostępu to niestandardowy system atrybutów i analizatorów Roslyn dla języka C#, który umożliwia deklaratywnie udostępnianie dostępu do private i internal członków klas tylko wybranym, "przyjaznym" typom. W przeciwieństwie do friend w C++, Elvis nie wymaga modyfikacji klasy docelowej podczas dodawania nowych zaufanych użytkowników i zapewnia surową weryfikację na etapie kompilacji. Rozwiązanie jest skierowane do programistów średniego i wysokiego poziomu, pracujących nad projektami o dużym obciążeniu, wielowątkowymi lub zależnymi od frameworków, gdzie zasady niezmienności (immutable-by-default) i lokalizacji zmienności są kluczowe dla poprawności działania.
Jak działa Elvis: architektura i kluczowe komponenty
Elvis składa się z trzech powiązanych warstw:
- Atrybuty czasu kompilacji –
[Elvis],[OnlyType],[OnlyNs],[ExcludeType],[ExcludeNs]. Annotują one członki (właściwości, metody, pola), wskazując, które typy lub przestrzenie nazw mają prawo do dostępu. - Analizator Roslyn – wykonuje semantyczną weryfikację w IDE i podczas kompilacji: analizuje wywołania, określa kontekst dostępu („atraktor”), porównuje go z zasadami atrybutów i generuje komunikaty diagnostyczne w przypadku naruszeń.
- Rozszerzenie VSIX i pakiet NuGet – zapewniają integrację z procesem pracy: automatyczne uzupełnianie atrybutów, szybkie stosowanie szablonów oraz wizualizację zezwolonych relacji w drzewie decyzji.
Ważne: Elvis nie zmienia kodu IL, nie korzysta z refleksji w czasie wykonania ani nie opiera się na InternalsVisibleTo. Cała weryfikacja odbywa się statycznie – podczas kompilacji. To gwarantuje, że niedozwolony dostęp nie przejdzie nawet w wersji debug.
Przykłady zastosowania: od podstawowego przypadku do zaawansowanych scenariuszy
Rozważmy podstawowe zadanie: klasa Me zawiera private decimal Money, ale musi umożliwić innemu typowi, MyFriend, bezpieczne modyfikowanie jego wartości, bez otwierania dostępu całemu kodowi w zestawie.
[Elvis(typeof(MyFriend))]
class Me
{
private decimal _money = 100;
public decimal Money => _money;
public void SetMoney(decimal value) => _money = value;
}
class MyFriend
{
public void TakeHalf(Me me)
{
var half = me.Money / 2;
me.SetMoney(me.Money - half); // ✅ Dopuszczalne: MyFriend został wskazany w [Elvis]
// me._money = 0; // ❌ Błąd kompilacji: bezpośredni dostęp do private pola jest zabroniony
}
}
Nowe możliwości wprowadzone w wersjach 2026-03 i 2026-04:
- Typy przez nazwy ciągów:
[Elvis("MyNamespace.MyFriend")]– przydatne w przypadku łączności między zestawami, gdy bezpośrednie odwołanie do typu nie jest możliwe. - Wyrażenia regularne:
[Elvis(@"^MyNamespace\.Tests\.*$")]– zezwala na dostęp wszystkim klasom testowym w danym namespace’u. - Atrybuty wykluczające:
[ExcludeType(typeof(ObsoleteHelper))]– blokuje konkretny typ, nawet jeśli wpada pod ogólne zasady[OnlyNs]. - Przestrzenie nazw:
[OnlyNs("MyCompany.Core")]– ogranicza dostęp tylko do typów z podanej przestrzeni nazw.
Kombinatoryka i zachowanie w hierarchii
Elvis obsługuje złożone reguły. Jeśli na jednym członku występuje kilka atrybutów, domyślnie stosowana jest logika AND, jednak z priorytetem wyjątków:
- Jeśli podano
[OnlyNs("A")]i[ExcludeNs("A.Sub")], to dostęp jest zezwalany dlaA., z wyjątkiemA.Sub.. - W przypadku dziedziczenia: jeśli klasa bazowa jest oznaczona
[Elvis(typeof(BaseFriend))], a pochodna –[Elvis(typeof(DerivedFriend))], to oba typy otrzymują dostęp do członków klasy bazowej. Jednak klasa pochodna nie nabywa uprawnień do własnychprivateczłonków – te należy anotować oddzielnie. - W przypadku metod z overloadem, atrybuty stosowane są indywidualnie do każdej sygnatury.
Cecha szczególna: Elvis nie wpływa na protected członki – pozostają one dostępne zgodnie ze standardowymi zasadami dziedziczenia. Atrybuty działają wyłącznie na private i internal.
Co jest ważne
- Elvis nie jest zastępstwem DI ani interfejsów, lecz dodatkowym poziomem kontroli nad inkapsulacją tam, gdzie interfejsy są niekomfortowe (np. przy pracy z DTO, obiektami konfiguracyjnymi czy wewnętrznymi mechanizmami frameworków).
- Wszystkie zasady są sprawdzane statycznie – zero nakładu w czasie wykonania, pełna kompatybilność z kompilacją AOT i trimmingiem.
- Obsługa wyrażeń regularnych i nazw typów w formie ciągów czyni rozwiązanie użytecznym w scenariuszach, gdzie typy nie są dostępne na etapie kompilacji (architektury plug-inowe, generacja kodu).
- Atrybuty wykluczające (
[Exclude*]) umożliwiają tworzenie „białych list z wyjątkami”, co jest kluczowe podczas migracji dużych baz kodu. - Elvis nie łamie SOLID: nie osłabia DIP, lecz go uzupełnia, pozwalając formalizować umowy między komponentami bez tworzenia sztucznych interfejsów-owijaków.
Praktyczne ograniczenia i rekomendacje
Elvis nie rozwiązuje wszystkich problemów z dostępem. Powinien być stosowany, gdy:
- Potrzebny jest punktowy, przewidywalny dostęp do
privatestanu bez ujawniania go przezpublic/internalAPI. - Architektura zakłada ścisłą izolację stref odpowiedzialności (np. jądro frameworku i jego rozszerzenia).
- Wykorzystywane są wzorce, w których klasy są logicznie ściśle powiązane, ale powinny pozostać niezależne pod względem kompilacji (np.
BuilderiBuilderResult).
Nie warto stosować Elvisa:
- Zamiast refaktoringu źle zaprojektowanych zależności – jeśli potrzeba 15 klas
frienddla jednegoprivatepola, najprawdopodobniej logika powinna zostać przeniesiona do osobnego serwisu. - W projektach bez CI/CD, gdzie analizatory nie są włączone do pipeline’a – wtedy zasady będą ignorowane podczas kompilacji poza IDE.
- Do
publicczłonków – atrybuty są po prostu ignorowane, ponieważ dostęp jest już otwarty.
Podczas wdrażania w istniejący projekt zaleca się zaczynać od private pól w klasach DTO i wewnętrznych fabrykach, gdzie ryzyko naruszenia inwariantów jest największe, a koszt refaktoringu minimalny.
— Editorial Team
Brak komentarzy.