Prywatna klasa?

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:

private class

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:

private class errorMoż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
{
}
}
view raw OuterClass.cs hosted with ❤ by GitHub

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?

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

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?

Szkolenie C# i .NET 5

Szkolenie C# i .NET 5

Zainteresował Ciebie ten temat? A może chcesz więcej? Jak tak to zapraszam na moje autorskie szkolenie o C# oraz .NET.

3 thoughts on “Prywatna klasa?

  • Pingback: dotnetomaniak.pl
  • 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ę.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.