Prywatna klasa?
Dzisiejszy post będzie nieco inny niż większość. Temat, który poruszę, nie jest może jakoś mocno praktyczny i nie wykorzystasz go każdego dnia. Z drugiej strony może jednak posłużyć jako ciekawy pomysł na pytanie rekrutacyjne, dlatego warto się nim zainteresować. 🙂
Na początku zastanówmy się, czy klasa faktycznie może być prywatna. Chwila zastanowienia i prawdopodobnie myślisz sobie, że chyba nie. Po co w ogóle coś takiego byłoby potrzebne? Odpalasz Visual Studio i próbujesz skompilować coś takiego:
Od razu widzisz, że coś jest nie tak. Visual Studio podkreśla słowo kluczowe private oraz samą nazwę klasy. Próba kompilacji powoduje błąd:
Można by wysnuć wniosek, że klasa nie może być prywatna, skoro Visual Studio nie chce skompilować takiego kodu. Ale gdy wczytamy się głębiej w komunikat błędu, to zauważymy, że nie ma tam nic na temat tego, że klasa nie może być prywatna. Jest jedynie informacja, że element zdefiniowany w namespace nie może być między innymi prywatny. Skoro tak, to może zdołamy w jakiś inny sposób utworzyć klasę, która nie będzie bezpośrednio w namespace? Po chwili zapewne przypomnisz sobie, że mamy w C# coś takiego jak klasa zagnieżdżona.
Klasa zagnieżdżona
Klasa zagnieżdżona to taka klasa, która jest zdefiniowana w jakimś innym elemencie (np. innej klasie). Czyli możemy utworzyć jedną klasę i do jej środka dodać drugą. Coś na kształt tego:
public class OuterClass | |
{ | |
private class InnerClass | |
{ | |
} | |
} |
Jak widać, klasa OuterClass zawiera tylko jeden element, który również jest klasą (InnerClass). W tym przypadku klasa zagnieżdżona może już być prywatna, bo nie znajduje się bezpośrednio w namespace.
A co oznacza w tym przypadku to, że klasa jest prywatna? Klasy tej możemy używać tylko wewnątrz klasy zewnętrznej – podobnie jak innych elementów prywatnych klasy, takich jak pola, właściwości, metody.
OK, ale czy to ma jakiś praktyczny sens?
Praktycznie użycie prywatnej klasy
Klas zagnieżdżonych używa się głównie w sytuacji, gdy dana klasa nie ma sensu istnieć bez głównej klasy, w której się znajduje. Prawda jest taka, że tych klas używa się bardzo rzadko (użyłeś/użyłaś jej kiedyś?). W moim przypadku używam ich głównie w jednym scenariuszu. A są nimi testy jednostkowe.
Czasami zdarza się, że aby coś przetestować, musimy utworzyć nową klasę, która dziedziczy po już istniejącej. I tak było w jednym z ostatnich przypadków, w którym używałem prywatnej klasy zagnieżdżonej.
W systemie, który rozwijam na co dzień, używamy Identity Servera do uwierzytelniania użytkowników. W tokenie, który jest przekazywany między mikroserwisami, znajdują się podstawowe informacje o użytkowniku. Między innymi znajduje się tam ID użytkownika. Dla łatwiejszego wyciągania tych danych stworzyliśmy prosty extension method, który to ułatwia. Wygląda on tak:
public static class ControllerExtensions | |
{ | |
public static string GetUserClaim(this ApiController controller, string claimName) | |
{ | |
var principal = controller.User as ClaimsPrincipal; | |
if (principal == null) | |
{ | |
return null; | |
} | |
var claim = principal.Claims.FirstOrDefault(c => c.Type == claimName); | |
if (claim == null) | |
{ | |
return null; | |
} | |
return claim.Value; | |
} | |
public static T GetUserClaim<T>(this ApiController controller, string claimName) | |
{ | |
var val = controller.GetUserClaim(claimName); | |
return (T)Convert.ChangeType( | |
val, | |
typeof(T) | |
); | |
} | |
} |
Nie ma tutaj nic skomplikowanego. Rozszerzamy ApiController, do którego dodajemy metodę GetUserClaim w dwóch wersjach.
W tym momencie pojawia się pytanie: jak przetestować te dwie metody? Gdy sprawdzimy definicję klasy ApiController, to okaże się, że klasa ta jest klasą abstrakcyjną, czyli nie możemy utworzyć jej instancji. Dlatego potrzebujemy jakiejś innej klasy.
Z jednej strony możemy użyć jakiegoś już istniejącego w aplikacji kontrolera. Nie jest to jednak najlepsze wyjście. Po pierwsze: jak zdecydować, który kontroler będzie do tego najlepszy? A po drugie: co się stanie, gdy z jakiegoś powodu kiedyś usuniemy ten kontroler?
W tym wypadku najlepszym rozwiązaniem jest utworzenie dedykowanego kontrolera dla testów. I tutaj właśnie przydaje się klasa prywatna zagnieżdżona. Możemy taki testowy kontroler utworzyć w klasie z testami. Możemy również ustawić go jako prywatny, dzięki czemu poza klasą z testami nie będzie widoczny.
Najlepiej widać to na przykładzie:
public class ControllerExtensionsTest | |
{ | |
private string _claimName = "userId"; | |
private int _claimValue = 123; | |
private TestController Create() | |
{ | |
var controller = new TestController(); | |
controller.User = new ClaimsPrincipal(new ClaimsIdentity(new [] {new Claim(_claimName, _claimValue.ToString())})); | |
return controller; | |
} | |
[Fact] | |
public void GetUserClaim_Return_Null_When_User_Is_Null() | |
{ | |
var controller = Create(); | |
controller.User = null; | |
var claim = controller.GetUserClaim("claimName"); | |
claim.Should().BeNull(); | |
} | |
[Fact] | |
public void GetUserClaim_Return_Null_When_Claim_Does_Not_Exist() | |
{ | |
var controller = Create(); | |
var claim = controller.GetUserClaim("claimName"); | |
claim.Should().BeNull(); | |
} | |
[Fact] | |
public void GetUserClaim_Return_Claim() | |
{ | |
var controller = Create(); | |
var claim = controller.GetUserClaim(_claimName); | |
claim.Should().NotBeNull(); | |
claim.Should().Be(_claimValue.ToString()); | |
} | |
[Fact] | |
public void Generic_GetUserClaim_Return_Claim() | |
{ | |
var controller = Create(); | |
var claim = controller.GetUserClaim<int>(_claimName); | |
claim.Should().Be(_claimValue); | |
} | |
private class TestController : ApiController | |
{ | |
} | |
} |
TestController jest prywatny i znajduje się na końcu klasy z testami. Wszystkie testy wykorzystują go poprzez metodę Create.
Co ciekawe, takie rozwiązanie spowoduje, że nikt poza klasą z testami nie użyje tego kontrolera, dzięki czemu nie musimy się martwić, że jakieś zmiany w tych testach wpłyną na inne testy.
Czy klasy prywatne są potrzebne?
Oczywiście skorzystanie w przykładzie z klasy prywatnej nie było wymagane. Praktycznie ten sam efekt uzyskalibyśmy, gdybyśmy TestController utworzyli w normalny sposób. Niestety według mnie na dłuższą metę takie rozwiązanie mogłoby powodować problemy.
Nieraz widziałem, jak programiści, zamiast utworzyć swoją własną klasę na potrzeby testów, korzystali już z jakieś gotowej z innych testów. To niestety często później powodowało, że zmiany wpływały na dużo większą liczbę testów, niż pierwotnie mogłoby się wydać. W efekcie powoduje to znacznie większe koszty utrzymania takich testów.
Dlatego właśnie w takich przypadkach bardzo chętnie korzystam z klas prywatnych. 🙂
Przykład
Na githubie (https://github.com/danielplawgo/PrivateClass) znajduje się przykład, na którym możesz sprawdzić działanie klas prywatnych. Do uruchomienia przykładu nie jest potrzebna żadna zmiana w projekcie.
Zachęcam do pobrania i pobawienia się. 🙂
Podsumowanie
Mam nadzieję, że po tym wpisie – gdy ktoś zada Ci pytanie, czy w C# możemy utworzyć klasę prywatną – będziesz wiedział, co odpowiedzieć. Do tego podasz realny przykład, gdzie taka klasa może się przydać.
A może będziesz miał ciekawe pytanie rekrutacyjne? 😉
Użyłeś kiedyś klasy prywatnej w swoim kodzie?
Ok. Trochę mi to rozjaśniło podejście do prywatnych klas. Myślę że w normalnym kodzie gdzie mamy wewnętrze bardziej rozbudowane zmienne lepiej użyć wewnętrznej klasy niż tysiąca ifów 🙂
Prywatne klasy mają zastosowanie również w przypadku, gdy chcemy zaaplikować jakiegoś customowego distincta lub ordera dodając odpowiednio implementacje interfejsów IEqualityComparer lub IComparer. Jeżeli robimy to w obrębie danej klasy, to prywatne klasy są wtedy najlepszym rozwiązaniem bo zachowujemy enkapsulację.