Modifikátor přístupu Elvis v C#: jak implementovat kontrolovaný přístup k privátním členům bez porušení inkapsulace
Elvis-modifikátor přístupu je uživatelský systém atributů a Roslyn-analyzátorů pro C#, který umožňuje deklarativně povolit přístup k private a internal členům tříd pouze striktně omezeným „přátelským“ typům. Na rozdíl od C++ friend nevyžaduje Elvis změnu cílové třídy při přidávání nových důvěryhodných konzumentů a zajišťuje rigorózní kontrolu na fázi kompilace. Řešení je určeno pro middle/senior vývojáře pracující s vysoce zatíženými, multithreadovanými nebo frameworkem závislými projekty, kde jsou principy neměnnosti (immutable-by-default) a lokalizované měnitelnosti kritické pro správnost.
Jak funguje Elvis: architektura a klíčové komponenty
Elvis se skládá ze tří vzájemně propojených vrstev:
- Atributy časové kompilace —
[Elvis],[OnlyType],[OnlyNs],[ExcludeType],[ExcludeNs]. Tyto atributy anotují členy (vlastnosti, metody, pole), čímž určují, které typy nebo jmenné prostory mají právo na přístup. - Roslyn-analyzátor — provádí semantickou kontrolu v IDE i při buildu: analyzuje volání, určuje kontext přístupu („atraktor“), srovnává jej s pravidly atributů a vydává diagnostická hlášení při porušení.
- VSIX rozšíření a NuGet balíček — zajišťují integraci do pracovního procesu: automatické doplňování atributů, rychlé aplikování šablon, vizualizace povolených vazeb v rozhodovacím stromu.
Důležité: Elvis nezmění IL kód, nepoužívá reflexi v runtime ani se nespoléhá na InternalsVisibleTo. Všechna kontrola probíhá staticky — při kompilaci. To zaručuje, že zakázaný přístup neprojde ani v debug buildu.
Příklady použití: od základního scénáře po pokročilé situace
Vezměme si základní úlohu: třída Me obsahuje private decimal Money, ale má umožnit jiné třídě MyFriend bezpečně upravovat jeho hodnotu, aniž by se otevíral přístup celému kódu v sestavení.
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); // ✅ Povoleno: MyFriend je uveden v [Elvis]
// me._money = 0; // ❌ Chyba kompilace: přímý přístup k privátnímu poli je zakázán
}
}
Nové možnosti přidané ve verzích 2026-03 a 2026-04:
- Typy prostřednictvím řetězců:
[Elvis("MyNamespace.MyFriend")]— užitečné při cross-build spojení, kdy není možné odkázat na typ. - Pravidelné výrazy:
[Elvis(@"^MyNamespace\.Tests\.*$")]— povoluje přístup všem testovacím třídám v daném jmenném prostoru. - Vylučující atributy:
[ExcludeType(typeof(ObsoleteHelper))]— blokuje konkrétní typ, i když spadá pod obecné pravidlo[OnlyNs]. - Jménné prostory:
[OnlyNs("MyCompany.Core")]— omezuje přístup pouze typům z uvedeného jmenného prostoru.
Kombinatorika a chování v hierarchii
Elvis podporuje složená pravidla. Pokud je na jednom členovi několik atributů, standardně se používá logika AND, ale s prioritou výjimek:
- Pokud jsou uvedeny
[OnlyNs("A")]a[ExcludeNs("A.Sub")], pak je přístup povolen proA., kroměA.Sub.. - Při dědictví: pokud je základní třída označena
[Elvis(typeof(BaseFriend))], a odvozená[Elvis(typeof(DerivedFriend))], pak oba typy dostanou přístup k členům základní třídy. Ovšem odvozená třída ne zdědí práva na své vlastníprivatečleny — ty musí být anotovány zvlášť. - U přetížených metod se atributy aplikují na každou signaturu individuálně.
Zvláštnost: Elvis nemá žádný vliv na protected členy — ti zůstávají dostupní podle standardních pravidel dědictví. Atributy fungují pouze s private a internal.
Co je důležité
- Elvis není náhradou DI nebo rozhraní, ale další úroveň kontroly nad inkapsulací tam, kde rozhraní nejsou vhodná (například při práci s DTO, konfiguračními objekty nebo interními frameworkovými mechanismy).
- Všechna pravidla jsou kontrolována staticky — žádný overhead v runtime, plná kompatibilita s AOT kompilací a trimmingem.
- Podpora pravidelných výrazů a řetězcových jmen typů činí řešení použitelným i v scénářích, kde typy nejsou dostupné v době kompilace (pluginové architektury, generace kódu).
- Vylučující atributy (
[Exclude*]) umožňují vytvářet „bílé seznamy s výjimkami“, což je kritické při migraci velkých kódových základů. - Elvis nenarušuje SOLID: neoslabuje DIP, ale ho doplňuje, umožňuje formalizovat smlouvy mezi komponentami bez vytváření umělých interfeisů‑obalů.
Praktická omezení a doporučení
Elvis neřeší všechny problémy s přístupem. Jeho použití je vhodné, když:
- Je potřeba bodový, předvídatelný přístup k
privatestavu bez jeho odhalení prostřednictvímpublic/internalAPI. - Architektura předpokládá striktní izolaci zón odpovědnosti (například jádro frameworku a jeho rozšíření).
- Používají se vzorce, kde jsou třídy logicky těsně propojeny, ale mají zůstat nezávislé z hlediska kompilace (například
BuilderaBuilderResult).
Nepoužívejte Elvis:
- Místo refaktoringu špatně navržených závislostí — pokud potřebujete 15
friendtříd pro jednoprivatepole, nejspíš by logika měla být vytažena do samostatného servisu. - V projektech bez CI/CD, kde analyzátoři nejsou součástí pipeline — pak budou pravidla ignorována při buildu mimo IDE.
- Pro
publicčleny — atributy jsou prostě ignorovány, protože přístup je tak jako tak otevřen.
Při implementaci do existujícího projektu se doporučuje začít s private poli v DTO třídách a interních fabrikách, kde je riziko porušení invariantů nejvyšší a náklady na refaktoring minimální.
— Editorial Team
Zatím žádné komentáře.