Porównanie kodu przed i po zastąpieniu ogólnych nazw nazwami opisującymi intencję.

Otwierasz metodę ProcessAsync. Przyjmuje id, dwa parametry typu bool i obiekt data. Żeby ustalić, czy tylko oblicza kwotę, czy również wysyła e-mail i zapisuje zmianę, musisz przeczytać jej implementację oraz kilka wywołań.

Dobra nazwa skraca tę drogę. Pozwala zrozumieć intencję w miejscu użycia i przewidzieć istotne zachowanie. Nie opisze całego algorytmu ani nie naprawi źle podzielonych odpowiedzialności, ale może ujawnić oba problemy wcześniej niż rozbudowany komentarz.

Nazwa to mały kontrakt z czytelnikiem

Wywołanie FindCustomerAsync tworzy pewne oczekiwanie: otrzymam dane klienta albo informację o jego braku. Jeśli metoda przy okazji rejestruje klienta, wysyła wiadomość powitalną i pobiera opłatę, jej nazwa ukrywa zachowanie ważne dla wywołującego.

Każda taka niespodzianka wymusza sprawdzanie implementacji. Podobny koszt mają nazwy Manager, Handle czy data2, gdy w ich otoczeniu brakuje informacji, czym zarządzają i co przechowują. Problem nasila się przy publicznych kontraktach oraz kodzie czytanym przez kilka zespołów.

Zacznij od pytania: „Co osoba czytająca ten fragment musi wiedzieć, żeby użyć go poprawnie?”. Nazwa powinna przenosić tę informację razem z typem, parametrami i kontekstem wywołania. Dokumentacja może wyjaśniać ograniczenia, lecz podstawowa intencja powinna być widoczna bez otwierania kolejnego pliku.

Trzy zasady, od których warto zacząć

  1. Nazywaj rozpoznawalne pojęcie lub operację. SendPaymentReminderAsync mówi, jaki efekt otrzyma użytkownik. ProcessAsync wymaga odgadnięcia tego efektu.
  2. Dobieraj szczegółowość do kontekstu. invoice wystarczy w krótkiej pętli. Parametr publicznej operacji zwykle potrzebuje dokładniejszej nazwy, na przykład overdueInvoiceId, jeśli właśnie taki warunek jest częścią kontraktu.
  3. Traktuj trudność z nazwaniem jako sygnał do rozmowy. Metoda może robić zbyt wiele, ale równie dobrze zespół może nie rozumieć jeszcze pojęcia biznesowego albo nie mieć uzgodnionego słownika.

Nie szukaj najdłuższej możliwej nazwy. Szukaj takiej, która usuwa istotną niejednoznaczność. Dopisanie Data, Object lub Info rzadko wyjaśnia różnicę między dwoma podobnymi typami.

Najpierw uzgodnij znaczenie słów

W jednym module ta sama osoba występuje jako User, w drugim jako Client, w trzecim jako Customer. Czy to trzy role, czy trzy określenia tego samego pojęcia? Tego nie rozstrzygnie reguła dotycząca wielkości liter.

Jeśli w module sprzedaży Customer oznacza podmiot kupujący, używaj tego terminu konsekwentnie w modelu, testach i rozmowach o funkcjonalności. Jeżeli User oznacza konto logowania, zachowaj rozróżnienie. Konto pracownika obsługującego zamówienie nie jest automatycznie klientem tego zamówienia.

Ten sam termin może mieć inne znaczenie w innym kontekście. „Zamówienie” w sprzedaży i „zlecenie” w magazynie mogą wymagać osobnych modeli, nawet gdy dotyczą tej samej transakcji handlowej. Nie wymuszaj jednego uniwersalnego typu tylko po to, żeby ujednolicić nazwy. Granice języka szerzej opisuje artykuł Context Map w praktyce.

Język kodu ustal z zespołem. Angielski zwykle ułatwia pracę z bibliotekami i dokumentacją, ale automatyczne tłumaczenie lokalnego pojęcia prawnego może zniekształcić jego sens. Przy trudnych terminach zapisz krótką definicję, wybrany odpowiednik oraz przykład. Mały słownik używany w pracy daje więcej niż długa lista synonimów w nieczytanym dokumencie.

Nazwa metody powinna ujawniać istotne zachowanie

Załóżmy, że metoda wysyła przypomnienie o zaległej fakturze. Jej implementacja i kontrakt pozostają bez zmian, poprawiamy tylko nazwę metody oraz parametru:

// Przed
Task ProcessAsync(Guid id, CancellationToken ct);

// Po
Task SendPaymentReminderAsync(
    Guid invoiceId,
    CancellationToken cancellationToken);

Nowa nazwa pozwala zauważyć zewnętrzny efekt. Gdyby metoda jedynie tworzyła treść wiadomości w pamięci, pasowałoby BuildPaymentReminder. Jeśli tylko zapisuje zlecenie późniejszej wysyłki, lepszym opisem może być SchedulePaymentReminderAsync. W systemie asynchronicznym różnica między zaplanowaniem i wykonaniem ma znaczenie dla użytkownika oraz obsługi błędów.

Nie musisz ujawniać wszystkich szczegółów technicznych. Nazwa SendPaymentReminderUsingSmtpAndWriteAuditLogAsync szybko stanie się nieaktualna po zmianie dostawcy. Istotny efekt biznesowy powinien być stabilny, a szczegóły realizacji należeć do implementacji lub do jasno nazwanych zależności.

Odróżniaj odczyt, zamiar i fakt

  • GetOrderSummaryAsync opisuje odczyt danych dla odbiorcy.
  • SubmitOrder opisuje zamiar zmiany stanu, który może zostać odrzucony.
  • OrderSubmitted opisuje zdarzenie: przyjęty już fakt.

Takie nazwy ułatwiają rozdzielenie odpowiedzialności, również bez rozbudowanej architektury. Przykłady znajdziesz w artykule CQRS bez Event Sourcingu. W kontrakcie zdarzenia doprecyzuj, co oznacza fakt: „złożone” nie musi oznaczać „opłacone” ani „wysłane”.

Przyrostek Async w metodach zwracających zadanie jest konwencją .NET opisaną w dokumentacji modelu TAP. Informuje o sposobie oczekiwania na zakończenie operacji. Nie obiecuje pracy w osobnym wątku ani tego, że zadanie będzie trwać po zakończeniu żądania HTTP.

Uważaj na parametry, których nie da się odczytać

Wywołanie z kilkoma flagami trudno zrozumieć bez podglądu sygnatury. Nazwane argumenty mogą od razu pomóc:

// To samo wywołanie, ta sama sygnatura.
await reminders.SendAsync(invoiceId, true, true, ct);

await reminders.SendAsync(
    invoiceId,
    sendEmail: true,
    previewOnly: true,
    cancellationToken: ct);

Teraz widać kolejne pytanie: czy sendEmail: true razem z previewOnly: true rzeczywiście niczego nie wyśle? Jeśli kombinacje flag wymagają tłumaczenia albo tworzą niedozwolone stany, sam rename nie wystarczy. Rozważ osobne operacje budowania podglądu i zlecania wysyłki lub typ opisujący dozwolone warianty. To już zmiana kontraktu, którą trzeba zaprojektować i sprawdzić osobno.

Typ powinien nazywać odpowiedzialność albo pojęcie

OrderManager może pobierać zamówienia, obliczać rabaty, wysyłać e-maile i eksportować raporty. Nadanie mu nazwy OrderService nie wyjaśni, gdzie kończy się jego odpowiedzialność. Nazwy takie jak DiscountPolicy, OrderSummaryQuery czy PaymentReminderSender mogą ujawnić odrębne role.

Nie wynika z tego zakaz słów Service, Handler albo Repository. Jeśli w projekcie mają ustalone znaczenie, pomagają rozpoznać rolę. SubmitOrderHandler jest czytelny, bo precyzuje obsługiwaną operację. Istotne jest to, czy pełna nazwa pozwala przewidzieć zakres zachowania.

Unikaj też jednego CustomerDto dla wszystkich ekranów i integracji. CustomerListItem, CustomerDetails oraz CustomerExportRow wskazują odbiorcę i powód zmiany. Jeśli typy naprawdę mają wspólny kontrakt, mogą go współdzielić; identyczne pola w danym momencie nie dowodzą jeszcze wspólnego przeznaczenia.

Nazwy parametrów nie chronią przed zamianą argumentów tego samego typu. Gdy pomylenie identyfikatorów jest częstym lub kosztownym błędem, osobne typy mogą dać silniejszą ochronę:

public readonly record struct CustomerId(Guid Value);
public readonly record struct OrderId(Guid Value);

Task CancelOrderAsync(
    OrderId orderId,
    CustomerId customerId,
    CancellationToken cancellationToken);

To uproszczone definicje: odróżniają rodzaje identyfikatorów na poziomie kompilatora, lecz nie sprawdzają pustego GUID-a, istnienia rekordu ani uprawnień. Dobieraj takie typy do ryzyka i znaczenia pojęcia. Tworzenie osobnej klasy dla każdej lokalnej wartości może tylko zwiększyć liczbę plików.

Jednostki, waluty i czas są częścią znaczenia

timeout = 30 nie mówi, czy chodzi o sekundy, minuty czy milisekundy. amount = 1299 nie wyjaśnia, czy to złote, grosze, czy kwota w innej walucie. Takie niejasności mogą zmieniać zachowanie systemu, mimo że kod poprawnie się kompiluje.

// Gdy kontrakt biblioteki wymaga liczby milisekund:
int retryDelayMilliseconds = 500;

// W kodzie aplikacji można przekazać jednostkę przez typ:
TimeSpan retryDelay = TimeSpan.FromMilliseconds(500);

decimal amountInMinorUnits = 1299m;
string currencyCode = "PLN";

DateTimeOffset expiresAtUtc =
    DateTimeOffset.UtcNow.AddMinutes(15);

Wartość pieniężna zwykle potrzebuje waluty i reguł precyzji. Nazwa amountInMinorUnits pomaga odczytać kontrakt, ale nie ustala liczby miejsc dziesiętnych dla wszystkich walut ani sposobu zaokrąglania. Jeśli te reguły powtarzają się w wielu miejscach, rozważ typ wartości reprezentujący pieniądze.

Przy czasie rozróżniaj moment, datę kalendarzową i czas trwania. CreatedAtUtc, InvoiceDate oraz RetryDelay opisują inne pojęcia. DateTimeOffset przenosi przesunięcie względem UTC, lecz nie zastępuje identyfikatora strefy czasowej dla przyszłych lokalnych terminów. Sama końcówka Utc wymaga konsekwentnego tworzenia i walidowania wartości.

Ile szczegółów potrzebuje nazwa

W OrderRepository metoda FindByIdAsync może być wystarczająco czytelna. Powtarzanie FindOrderByOrderIdAsync dodaje słowa, które wynikają już z typu i parametrów. Na poziomie publicznego serwisu obsługującego kilka rodzajów dokumentów nazwa FindOrderAsync może być potrzebna.

Podobnie customer.Address.City nie wymaga właściwości CustomerAddressCityName. Czytelnik widzi kontekst wyrażenia. Z kolei w płaskim eksporcie kolumny BillingCity i ShippingCity powinny ujawnić różnicę, bo nie ma już otaczających obiektów.

Skróty oceniaj według odbiorców. Id, Url i Http są powszechne, a dto bywa zrozumiałe w krótkim fragmencie mapowania. Lokalny skrót pmtRcncl wymaga nauki i utrudnia wyszukiwanie. W publicznym kontrakcie rozwinięcie PaymentReconciliation zwykle lepiej przekazuje znaczenie.

Krótka nazwa w małym zasięgu może być dobra. i w pętli indeksującej nie potrzebuje rozbudowanego opisu. Jeśli jednak indeks reprezentuje numer raty albo priorytet kolejki, znaczenie biznesowe może uzasadniać dokładniejszą nazwę.

Ustal konwencje, których można przestrzegać

Zespół powinien uzgodnić kilka powtarzalnych decyzji i zapisać je z przykładami:

  • Odczyt i brak wyniku. Możecie przyjąć, że Find zwraca brak, a Get wymaga istnienia rekordu i zgłasza błąd. To lokalna umowa; biblioteki zewnętrzne mogą używać tych słów inaczej.
  • Predykaty. IsEnabled, HasOverdueInvoices i CanBeCancelled powinny dawać się przeczytać jako pytania. Unikaj podwójnych zaprzeczeń takich jak !isNotDisabled.
  • Kolekcje i pojedyncze wartości. invoices sugeruje zbiór, invoice jeden element. Dodatek List ma znaczenie wtedy, gdy konkretny rodzaj kolekcji jest istotny dla kontraktu.
  • Styl języka. W C# nazwy publicznych typów i metod zwykle używają PascalCase, a parametry i zmienne camelCase. W innych językach trzymaj się ich idiomów oraz zasad repozytorium.

Konwencje .NET są opisane w Naming Guidelines. Użyj analyzera i formatowania do reguł, które da się sprawdzić automatycznie. Narzędzie wykryje niespójny zapis, ale nie ustali, czy biznes rozróżnia płatnika i odbiorcę faktury.

Jak bezpiecznie poprawiać nazwy

W kodzie prywatnym rename wspierany przez IDE bywa małą zmianą. Przy publicznym API ta sama operacja może wymagać migracji odbiorców. Zacznij od rozpoznania granicy kontraktu:

  1. Ustal znaczenie. Sprawdź implementację, wywołania i testy. Zapisz, co faktycznie robi operacja oraz czy proponowana nazwa tego nie zawęża ani nie rozszerza.
  2. Zmień symbol narzędziem rozumiejącym kod. Rename w IDE aktualizuje odwołania trafniej niż globalne zastępowanie tekstu. Sprawdź także napisy używane przez refleksję, konfigurację i szablony.
  3. Przejrzyj kontrakty zewnętrzne. Nazwy pól JSON, adresy HTTP, kolumny SQL, typy zdarzeń i argumenty skryptów mogą być używane poza repozytorium.
  4. Oddziel zmianę nazwy od zmiany zachowania. Jeśli operacja wymaga podziału lub nowej semantyki, pokaż to jako osobny krok z odpowiednią walidacją.
  5. Sprawdź wynik w miejscach użycia. Uruchom kompilację i właściwe testy, obejrzyj wygenerowane schematy oraz migracje. Przeczytaj kilka wywołań bez zaglądania do implementacji.

Zmiana nazwy właściwości w C# może zmienić pole JSON przy domyślnej serializacji albo wpłynąć na mapowanie ORM. Jeżeli kontrakt ma pozostać zgodny, użyj jawnego mapowania nazwy zewnętrznej i sprawdź je testem kontraktu. Dla publicznej biblioteki lub API może być potrzebny czasowo utrzymany stary punkt wejścia, oznaczenie wycofania oraz plan usunięcia.

Refaktoryzacja Change Function Declaration opisuje również etapowe przenoszenie wywołań do nowej deklaracji. Taki sposób pracy pomaga przy dużym zasięgu zmiany, ale każda warstwa zgodności powinna mieć uzasadnienie i właściciela. Nie trzeba dodawać jej do prywatnej metody mającej dwa wywołania w jednym pliku.

Co sprawdzić przy następnym code review

Zamiast dyskutować wyłącznie o tym, czy nazwa brzmi ładniej, sprawdź kilka konkretnych rzeczy:

  • Czy nazwa używa pojęć, które zespół rozumie w ten sam sposób?
  • Czy razem z typami i parametrami ujawnia istotny efekt, jednostkę oraz zakres danych?
  • Czy szczegóły wynikające już z kontekstu nie zasłaniają najważniejszej informacji?
  • Czy podobne nazwy mają rzeczywiście podobną semantykę?
  • Czy zmiana dotyka odbiorców spoza tego fragmentu kodu?

W komentarzu do review wskaż niejasność i jej konsekwencję: „Nie wiem, czy ta metoda wysyła wiadomość, czy tylko ją planuje” daje autorowi konkretny problem do rozwiązania. Sama propozycja innego synonimu łatwo przeradza się w spór o gust.

Popraw jedną nazwę tam, gdzie właśnie pracujesz, i sprawdź, czy łatwiej przeczytać jej użycia. Jeśli nadal potrzebujesz długiego wyjaśnienia, przyjrzyj się kontraktowi, typom i podziałowi odpowiedzialności. Dobre nazewnictwo rozwija się razem ze zrozumieniem systemu.