10 lat stażu. Tytuł Senior Developer na LinkedInie. Robi Ci code review, blokuje Twoje pull requesty i z pełnym przekonaniem tłumaczy, jak się "powinno" pisać kod. A teraz coś, co może zaboleć: jest spora szansa, że ta osoba nie przeszłaby dziś rekrutacji na stanowisko mida.
Nie piszę tego dla kontrowersji. Przez lata pracowałem z dziesiątkami seniorów. Część z nich to najlepsi inżynierowie, jakich znam. Ale część po prostu przetrwała w branży wystarczająco długo, żeby dostać tytuł. Z zewnątrz obie grupy wyglądają tak samo: te same lata w CV, ten sam tytuł w stopce maila. Różnicę widać dopiero w kodzie i w tym, jak ktoś o nim myśli.
W tym artykule zobaczysz, skąd się biorą "seniorzy z papieru", i poznasz 7 czerwonych flag (plus 1 bonusową), po których ich rozpoznasz - 5 z nich pokażę na konkretnym kodzie w C#, razem z poprawną wersją. Potem przejdziemy do 7 cech prawdziwego seniora, a na koniec dostaniesz 5 pytań, którymi sprawdzisz samego siebie. Uprzedzam: może zaboleć.
Staż to nie doświadczenie
Zacznijmy od prostego rozróżnienia, bez którego reszta tego tekstu nie ma sensu: staż to nie jest doświadczenie. To 2 różne rzeczy, choć w CV wyglądają identycznie.
Ktoś ma w CV 10 lat. Ale jeśli przez te 10 lat robił w kółko te same CRUD-y, tym samym sposobem, bez wychodzenia poza schemat, to nie jest 10 lat doświadczenia. To 1 rok doświadczenia, powtórzony 10 razy.
Liczba lat mówi, jak długo ktoś jest w branży. Nie mówi, czego się przez ten czas nauczył. I właśnie w tej szczelinie wyrastają seniorzy, którzy nie umieją programować.
Skąd się biorą "seniorzy z papieru"? 4 powody
Skąd się w ogóle biorą seniorzy, którzy nie umieją programować? Widzę 4 główne powody. Od razu zaznaczę: nie chodzi o szukanie jednego winnego. Wszystkie 4 są systemowe - tak po prostu działa branża.
Powód 1: inflacja tytułów. Rynek przez lata był rozgrzany. Firmy dawały "seniora" po 3 latach, bo jeśli one tego nie zrobią, zrobi to konkurencja - i programista odejdzie. Tytuł stał się kartą przetargową w negocjacjach, a nie miarą umiejętności.
Powód 2: awans za przetrwanie. W wielu firmach nie awansuje ten, kto najbardziej urósł, tylko ten, kto siedzi najdłużej. Dlaczego? Bo staż jest łatwo zmierzyć, a umiejętności - trudno. Więc mierzy się staż.
Powód 3: zero weryfikacji. Kiedy ostatni raz ktoś na poważnie sprawdził umiejętności Twojego seniora? Najpewniej na rozmowie rekrutacyjnej. 5, 8, może 10 lat temu. Od tamtej pory - nic.
Powód 4: duża ryba w małym stawie. Ktoś jest najlepszym programistą w firmie, w której nikt inny nie potrafi ocenić jego kodu. Brak punktu odniesienia przez lata robi swoje.
Efekt tych 4 rzeczy? Tytuł rośnie co roku, automatycznie. Umiejętności - już niekoniecznie.
Skoro tytuł niewiele mówi, trzeba patrzeć na coś innego: na kod. Koniec teorii - poniżej 7 czerwonych flag, po których rozpoznasz seniora z papieru. Wszystkie z życia, a pierwsze 5 pokażę na przykładach w C#.
Czerwona flaga #1: async/await wszędzie, bez zrozumienia
Używa async/await wszędzie, bo "tak się teraz pisze". Ale nie ma pojęcia, co się dzieje pod spodem. Najczęściej widać to w 2 miejscach:
public string GetUser(int id)
{
/* blokuje wątek, a bywa, że wiesza całą aplikację */
return _api.GetUserAsync(id).Result;
}
public async void Save()
{
/* nie wraca jako Task - łatwo zgubić obsługę błędów */
await _db.SaveChangesAsync();
}.Result, czyli synchroniczne czekanie na asynchroniczny kod. Klasyka: gdzieś nie można użyć await, więc dokleja się .Result. W starym ASP.NET (na .NET Framework) albo w aplikacji desktopowej to przepis na deadlock - aplikacja po prostu staje. Wątek czeka na wynik zadania, a dokończenie metody asynchronicznej czeka na powrót do tego samego kontekstu (w aplikacji desktopowej - do wątku UI), który właśnie jest zablokowany. Obie strony czekają na siebie w nieskończoność. W ASP.NET Core zwykle nie będzie klasycznego deadlocka, bo nie ma tam takiego kontekstu synchronizacji. Dalej jednak blokujesz wątek z puli, który mógłby w tym czasie obsługiwać inne żądania - i pod obciążeniem aplikacja zaczyna się dusić.
async void, czyli wyjątek z zaskoczenia. Druga klasyka. Metoda async void nie zwraca Taska, więc wywołujący nie może na nią poczekać ani złapać jej wyjątku. Wyjątek z takiej metody nie wraca w Tasku, tylko łatwo wypada poza normalną obsługę błędów. Potrafi polecieć prosto w aplikację i zrobić awarię tam, gdzie nikt się jej nie spodziewa. Jedyne rozsądne miejsce na async void to handlery zdarzeń (np. kliknięcie przycisku), bo tam sygnatura jest narzucona z góry.
Jak powinno być. Metoda asynchroniczna zwraca Task (albo Task<T>), a wywołujący na nią czeka - i tak w górę stosu wywołań, aż do kontrolera czy handlera ("async all the way"):
public async Task<string> GetUserAsync(int id)
{
return await _api.GetUserAsync(id);
}
public async Task SaveAsync()
{
await _db.SaveChangesAsync();
}I jeszcze zdanie, które seniorzy z papieru lubią powtarzać: "Dodałem asynca, będzie szybciej." Nie będzie. Async nie przyspiesza pojedynczej operacji. Pozwala zwolnić wątek, kiedy kod czeka na bazę, sieć czy dysk, dzięki czemu aplikacja obsłuży więcej żądań naraz. To różnica między szybkością a przepustowością - i dobry test na to, czy ktoś rozumie, czego używa.
Czerwona flaga #2: IEnumerable czy IQueryable? "A jaka to różnica"
Nie odróżnia IEnumerable od IQueryable. "A jaka to różnica, jedno i drugie się iteruje." Różnica jest ogromna, choć widać ją w kodzie, który wygląda niemal identycznie. Wersja seniora z papieru:
var active = _db.Users
.ToList() /* ściąga CAŁĄ tabelę */
.Where(u => u.IsActive) /* filtruje w pamięci */
.Take(10);ToList() wywołany za wcześnie ściąga z bazy całą tabelę, a filtrowanie i Take dzieją się już w pamięci aplikacji. Tak powinno być:
var active = await _db.Users
.Where(u => u.IsActive) /* filtruje baza */
.Take(10)
.ToListAsync(); /* pobiera 10 rekordów */Tutaj warunki lecą do SQL-a i baza zwraca 10 rekordów. Przy tabeli z 500 000 użytkowników różnica to 500 000 rekordów w pamięci kontra 10. A kod wygląda prawie tak samo.
Skąd ta różnica? Dopóki pracujesz na IQueryable, EF Core tylko buduje zapytanie, a Where i Take zostaną przetłumaczone na SQL. Do bazy nic nie leci aż do materializacji: ToList(), ToListAsync(), First(), Count() albo pętli foreach. Od tego momentu masz zwykłą kolekcję w pamięci i każdy kolejny operator LINQ działa na obiektach w RAM-ie, a nie w bazie.
Ta sama pułapka czai się w metodzie repozytorium, która zwraca IEnumerable<User> zamiast IQueryable<User>. Każde Where dopisane później przez wywołującego wykona się w pamięci - już po pobraniu całej tabeli.
Czerwona flaga #3: wyjątki zamiecione pod dywan
Wyjątki? Zamiata pod dywan. W praktyce wygląda to tak:
try
{
ProcessPayment(order);
}
catch (Exception)
{
/* TODO: obsłużyć kiedyś */
}Pusty catch. Płatność się wywaliła, klient nie dostał produktu, a w logach - cisza. System "działa", tylko nikt nie wie, że nie działa. Płatność padła. Klient wie. Logi - nie.
Bonus z tej samej półki: throw ex.
catch (Exception ex) { throw ex; } /* ucina stack trace */throw ex zamiast samego throw ucina oryginalny stack trace. Ślad stosu zaczyna się wtedy w miejscu ponownego rzucenia, a nie tam, gdzie błąd naprawdę wystąpił - i debugowanie zamienia się w zgadywanie. Samo throw; rzuca dalej ten sam wyjątek z nienaruszonym śladem.
Jak to naprawić? Łap wyjątek tylko wtedy, gdy wiesz, co z nim zrobić. Jeśli nie wiesz - zaloguj go z kontekstem i rzuć dalej (albo nie łap wcale i zostaw to globalnej obsłudze błędów):
try
{
ProcessPayment(order);
}
catch (Exception ex)
{
_logger.LogError(ex, "Płatność dla zamówienia {OrderId} nie powiodła się", order.Id);
throw; /* oryginalny stack trace zostaje */
}Płatność dalej może paść, ale przynajmniej dowiesz się o tym z logów, a nie od klienta.
Czerwona flaga #4: rejestracja w DI "na czuja"
Rejestruje serwisy w kontenerze DI na czuja. Singleton, Scoped, Transient? "E tam, jakie to ma znaczenie."
/* UserService dostaje w konstruktorze DbContext */
builder.Services.AddSingleton<UserService>();
/* często: błąd przy starcie albo */
/* DbContext trzymany dłużej, niż powinien */Ma znaczenie. DbContext zarejestrowany przez AddDbContext ma domyślnie czas życia Scoped, czyli 1 instancja na request. Jeśli singleton dostaje taką zależność, to przy włączonej walidacji zakresów (ASP.NET Core włącza ją domyślnie w środowisku Development) aplikacja wywali się już przy starcie. I to jest ten dobry scenariusz.
Gorzej, jeśli walidacja jest wyłączona albo ktoś ją obejdzie. Wtedy singleton trzyma kontekst dłużej, niż powinien - w praktyce przez całe życie aplikacji - i potrafi współdzielić go między requestami. A DbContext nie jest thread-safe. Efekt: losowe wyjątki na produkcji, których "nie da się odtworzyć". I diagnoza seniora: "Entity Framework jest niestabilny".
Jak to naprawić? Zasada jest jedna: serwis nie może żyć dłużej niż jego zależności (złamanie tej zasady to tzw. captive dependency). Jeśli UserService korzysta z DbContextu, rejestrujesz go jako Scoped:
builder.Services.AddScoped<UserService>();A jeśli serwis naprawdę musi być singletonem (np. usługa działająca w tle), nie wstrzykujesz mu DbContextu, tylko fabrykę, i tworzysz krótko żyjący kontekst na każdą operację:
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString));
/* w singletonie wstrzykujesz IDbContextFactory<AppDbContext> */
using var db = _dbFactory.CreateDbContext();Każda operacja dostaje wtedy własny, świeży kontekst, a problem współdzielenia między requestami znika.
Czerwona flaga #5: nie wie, jakie SQL generuje jego LINQ
Nie ma pojęcia, jakie SQL generuje jego LINQ. Nigdy nawet nie zajrzał. Efekt wygląda na przykład tak:
/* OrderReport.cs - klasyczny problem N+1 */
var orders = await _db.Orders.ToListAsync(); /* 1 zapytanie */
foreach (var order in orders)
{
var client = await _db.Customers /* +1 za każdym obrotem */
.FirstAsync(c => c.Id == order.CustomerId);
}
/* 1000 zamówień = 1001 zapytań do bazy */Pobiera zamówienia - 1 zapytanie. A potem w pętli, dla każdego zamówienia osobno, dociąga klienta. 1000 zamówień? 1001 zapytań do bazy. To klasyczny problem N+1. Aplikacja muli, a diagnoza "seniora" brzmi: "Baza jest za wolna, dołóżmy cache". Nie. Wystarczyło jedno Include albo join:
var orders = await _db.Orders
.Include(o => o.Customer) /* klienci w tym samym zapytaniu (JOIN) */
.ToListAsync(); /* 1 zapytanie zamiast 1001 */(Zakładam tu, że encja Order ma właściwość nawigacyjną Customer - w typowym modelu EF Core tak właśnie jest.)
Najważniejsze jest jednak to, że problemu N+1 nie widać w samym kodzie C#. Widać go dopiero w SQL-u, który leci do bazy. Dlatego senior zagląda w zapytania, zanim zacznie obwiniać bazę. Jak to zrobić najprościej, zobaczysz w teście na końcu artykułu.
Czerwona flaga #6: architektura z konferencji, nie z potrzeby
Prosty CRUD na 5 tabel, a w odpowiedzi propozycja: "Zróbmy mikroserwisy, CQRS i event sourcing". Kopiowanie rozwiązań Netflixa do aplikacji, której używa 40 osób.
Każdy z tych wzorców ma sens - w odpowiedniej skali i przy odpowiednich problemach. Ale wzięty z prezentacji konferencyjnej, a nie z wymagań projektu, dokłada tylko kolejne warstwy, które ktoś musi zrozumieć, utrzymać i debugować. Złożoność to koszt. Tylko że senior z papieru płaci go cudzymi pieniędzmi.
Czerwona flaga #7: boi się własnego kodu
Poznasz to po 3 zdaniach, które wracają jak mantra:
• "Nie ruszaj, bo działa."
• "Zawsze tak robiliśmy."
• "U mnie działa."
Strach przed dotknięciem kodu to prawie zawsze objaw jednego: braku zrozumienia, co ten kod właściwie robi. Kto rozumie swój kod, ten wie, co może się zepsuć po zmianie, i potrafi to sprawdzić.
Flaga bonusowa: nigdy nie mówi "nie wiem"
Moja ulubiona. Na każde pytanie ma gotową, pewną siebie odpowiedź. Często błędną. Problem w tym, że pewność siebie i kompetencja z zewnątrz wyglądają identycznie - dopóki ktoś nie sprawdzi.
Ważne zastrzeżenie. Zanim zaczniesz liczyć flagi u kolegów z zespołu: 1 czy 2 flagi zdarzają się każdemu. Mnie też. Ale 5 z 7? To już nie jest przypadek. To wzorzec.
Po czym poznasz prawdziwego seniora? 7 cech
No dobrze - a po czym poznać seniora prawdziwego? Oto 7 cech. Zauważ, że żadna z nich nie dotyczy stażu.
Cecha 1: mówi "nie wiem, sprawdzę". To jedna z najlepszych oznak dojrzałości w tej branży. Im więcej ktoś naprawdę umie, tym częściej przyznaje, że czegoś nie wie. Bo wie, ile jeszcze jest do wiedzenia. To dokładne przeciwieństwo flagi bonusowej.
Cecha 2: upraszcza. Przeciętny programista komplikuje proste rzeczy. Senior upraszcza skomplikowane. Jeśli po jego wyjaśnieniach rozumiesz mniej niż przed - to nie jest dobry znak.
Cecha 3: rozumie, co się dzieje warstwę niżej. Nie musi znać każdego frameworka na pamięć. Musi wiedzieć, co framework robi za niego. Co się dzieje między requestem HTTP a kontrolerem. Co Entity Framework robi z jego LINQ. To właśnie ta cecha chroni przed flagami #1, #2, #4 i #5.
Cecha 4: myśli trade-offami. Nie pyta "co jest najlepsze?", tylko "co jest najlepsze tutaj?" - przy tym zespole, tym budżecie i tym terminie. Wie, że każda decyzja ma koszt, i umie ten koszt nazwać. Dlatego nie proponuje mikroserwisów do CRUD-a na 5 tabel.
Cecha 5: kasuje kod. Wie, że każda linia kodu to koszt utrzymania. Jeden z najlepszych pull requestów, jakie widziałem, był na sporym minusie. Mniej kodu, ta sama funkcjonalność - to jest inżynieria.
Cecha 6: odpowiada za produkcję. "U mnie działa" go nie interesuje. Interesuje go, czy działa u klienta. W piątek wieczorem też.
Cecha 7: podnosi poziom zespołu. Najważniejsza. Seniora nie poznajesz tylko po jego kodzie. Poznajesz go po tym, czy ludzie obok niego szybciej rosną.
Test: sprawdź siebie w 5 pytaniach
A teraz część najważniejsza. Bo najłatwiej jest oceniać innych. Trudniej - siebie. Sprawdźmy więc Ciebie.
Zasady. 5 pytań. Odpowiadasz na głos, prostym językiem, bez Google'a i bez przewijania artykułu w górę. Jeśli nie umiesz czegoś wyjaśnić prosto, to bardzo często znaczy, że masz tam lukę.
1. Co dokładnie dzieje się z wątkiem, kiedy kod trafia na await? I kto potem dokańcza tę metodę?
2. Czym różni się IQueryable od IEnumerable - i w którym dokładnie momencie LINQ faktycznie odpytuje bazę danych?
3. Singleton, Scoped, Transient - kiedy używasz którego? I co konkretnie może pójść nie tak, kiedy singleton dostanie zależność typu scoped?
4. Jakie SQL wygenerowało Twoje ostatnie zapytanie LINQ? Nie wiesz? OK - to jak byś to sprawdził?
5. Dlaczego throw ex; ucina oryginalny stack trace, a samo throw; - nie?
Podpowiedź do pytania 4. EF Core ma metodę ToQueryString(), która zwraca SQL wygenerowany dla zapytania, bez jego wykonywania. Jeśli słyszysz o niej pierwszy raz - zapisz sobie, przyda się:
var query = _db.Users
.Where(u => u.IsActive)
.Take(10);
Console.WriteLine(query.ToQueryString());Twój wynik. Jak poszło?
• 5/5 - masz solidne fundamenty. Serio, szacun.
• 3-4 - jest baza, ale są dziury do załatania.
• 0-2 - to nie wyrok. To po prostu Twoja lista zadań na najbliższe tygodnie.
Dobra wiadomość jest taka, że właśnie dokładnie wiesz, od czego zacząć. Pytania nie są przypadkowe: każde dotyczy jednej z pierwszych 5 czerwonych flag - await (#1), IQueryable (#2), throw (#3), czasu życia w DI (#4) i SQL-a z LINQ (#5). Jeśli któreś Cię zatrzymało, wróć do odpowiadającej mu flagi.
Bo widzisz - tytuł dostajesz od firmy. Umiejętności musisz wypracować sam.
Ściąga: senior z papieru vs prawdziwy senior
Wszystko w jednym miejscu:
| Obszar | Senior z papieru | Prawdziwy senior |
| Gdy czegoś nie wie | Pewna siebie odpowiedź, często błędna | "Nie wiem, sprawdzę" |
| Framework | async/await, LINQ i DI "na czuja" | Rozumie, co się dzieje warstwę niżej |
| Złożoność | Mikroserwisy, CQRS i event sourcing do CRUD-a na 5 tabel | Upraszcza i myśli trade-offami |
| Istniejący kod | "Nie ruszaj, bo działa" | Rozumie go i kasuje to, co zbędne |
| Błędy i produkcja | Pusty catch, throw ex, "u mnie działa" | Odpowiada za to, czy działa u klienta |
| Miara | Lata stażu i tytuł | Umiejętności i to, jak rosną ludzie obok niego |
Podsumowanie
Tytuł seniora mówi, jak długo ktoś jest w branży. Nie mówi, co umie. W skrócie:
• Staż to nie doświadczenie - 10 lat może być 1 rokiem powtórzonym 10 razy.
• Seniorzy z papieru to efekt systemu: inflacja tytułów, awans za przetrwanie, zero weryfikacji i duża ryba w małym stawie.
• 7 czerwonych flag: async/await bez zrozumienia, IEnumerable zamiast IQueryable, wyjątki pod dywanem, DI "na czuja", N+1 i ślepota na SQL, architektura z konferencji i strach przed własnym kodem. Plus bonus: nigdy nie mówi "nie wiem".
• 1-2 flagi zdarzają się każdemu, 5 i więcej to wzorzec.
• Prawdziwy senior mówi "nie wiem, sprawdzę", upraszcza, rozumie warstwę niżej, myśli trade-offami, kasuje kod, odpowiada za produkcję i podnosi poziom zespołu.
• Test na 5 pytań to Twoja lista zadań, jeśli któreś pytanie Cię zatrzymało.
Żeby było jasne: ten tekst nie jest po to, żeby hejtować seniorów. Znam wybitnych. Chodzi o jedno: przestań utożsamiać tytuł z umiejętnościami. U innych - i u siebie.
Jeśli chcesz te umiejętności budować systematycznie, dołącz do mojej społeczności. Jest nas już ponad 10 000 programistów .NET. Co tydzień wysyłam jednego konkretnego maila: najważniejsze newsy ze świata .NET i C#, bez lania wody, plus rabaty na moje kursy, zanim pojawią się gdziekolwiek indziej. Zero spamu - wypisujesz się jednym kliknięciem.
Wystarczy, że zostawisz swój adres tutaj: modestprogrammer.pl/vip. I rośnij szybciej niż Twój staż :)