[PL] Challenge everything
[read English version here]
Rzucam wyzwanie standardom kodowania. Postanowiłem po raz kolejny, na bazie nagromadzonych w ciągi ostatnich miesięcy doświadczeń, zrewidować swój sposób pisania kodu. Oto, co wymyśliłem.
Var
Jakiś czas temu przy okazji cyklu dotyczącego czytelności kodu pozwoliłem sobie dosyć mocno skrytykować słowo kluczowe var. Zgadnijcie co się stało? Zmieniłem zdanie. var już mnie nie straszy. Oswoiłem się z nim. Przekonała mnie możliwość stosowania dłuższych i bardziej wyrazistych nazw zmiennych przy zachowaniu tej samej długości linii (staram się unikać linii dłuższych niż 100 znaków).
Regiony
Nadal mają u mnie przerąbane. Stosuję je tylko w wyjątkowych sytuacjach, kiedy inaczej po prostu się nie da. Przykładem jest kod obiektów ValueObject w DDDSample.Net (tu dostępne są źródła),. gdzie nie chcę rozpraszać czytelnika kwestiami infrastrukturalnymi, takimi jak implementacja operatorów.
Implementacje interfejsów
Kiedyś zwykłem (zgodnie z resztą z domyślnymi ustawieniami R#) zamykać implementacje interfejsów w regiony. Ostatnio jednak porzuciłem jednak ten nawyk. Po pierwsze, bardzo rzadko piszę klasy, które implementują więcej niż jeden interfejs nietechniczny. Po drugie, jeśli już jakiś implementują, bardzo rzadko mają metody publiczne nie należące do tego interfejsu. Nie ma więc potrzeby w jakikolwiek sposób wyróżniać tych, które do niego należą. Jest jeszcze jeden argument przeciwko wyróżnianiu i izolowaniu metod implementacji interfejsu: takie postępowanie uniemożliwia odpowiednie pionowe rozmieszczenie metod. Dlaczego?
Funkcje publiczne, funkcje prywatne
Uncle Bob w swojej genialnej książce Clean Code przekonał mnie, iż metody niższego poziomu wykorzystywane przez daną metodę wyższego poziomu powinny być zlokalizowane blisko niej. Jest to praktyczne: nie trzeba przewijać ekranu podczas czytania kodu. Jest to także logiczne: relacja “wywołuje” znacznie bardziej wiąże ze sobą metody, niż posiadanie takiego samego kwalifikatora dostępu (private, public). Aby nie być gołosłownym — przykład. Zamiast wcześniejszego:
public Class1()
{
public void MethodA() {
}
public void MethodB() {
}
private void MethodA_1(){}
private void MethodA_2(){}
private void MethodB_1() {}
private void MethodB_2() {}
}
teraz stosuję:
public Class2()
{
public void MethodA() {}
private void MethodA_1(){}
private void MethodA_2(){}
public void MethodB() {}
private void MethodB_1() {}
private void MethodB_2() {}
}
Widać także, że o ile stary styl kopiowania pozwalał na zawijanie implementacji interfejsów w regiony (np. MehodA i MethodB), o tyle nowy jest zupełnie niekompatybilny z tą ideą — trzeba by było zawinąć całą klasę.
Pola
Czytając kod NServiceBus nauczyłem się, że dogmat o kolejności memberów w klasie (najpierw pola, potem metody) może być złamany. W NSB spotkałem się z wzorcem: najpierw metody, potem konstruktor, na końcu pola. Z początku byłem dosyć sceptyczny, jednak po jakimś czasie sam zacząłem go stosować. Nie wiem jaka była oryginalnie motywacja Udiego, aby tak pisać, ale dla mnie plusem tego podejścia jest to, że otwieram klasę i widzę to co mnie interesuje — jej publiczny interfejs. Dzięki możliwości współczesnych IDE jest duża szansa, że podczas czytania kodu w ogóle nie będę potrzebował patrzeć na pola.
Konstruktor
Klasy, które piszę, są albo pełnoprawnymi klasami, albo strukturami danych kompletnie pozbawionymi zachowania. Instancje tych drugich tworzone są zwykle klasycznie, za pomocą operatora new, więc sygnatura konstruktora ma znaczenie. W takich wypadkach umieszczam konstruktor wysoko, zaraz po polach, a przed właściwościami. Można powiedzieć, że stosuję mniej-więcej klasyczny układ memberów.
Zupełnie inaczej jest, jeśli chodzi o “prawdziwe” klasy. Ich instancje zwykle tworzone są za pośrednictwem frameworku DI, więc kompletnie nie interesuje mnie jak zostaną stworzone. Konstruktor ląduje więc na dole klasy, pomiędzy metodami, a polami. Zaglądam do niego tylko wtedy, kiedy muszę dodać jakąś nową zależność do klasy.
Efekt końcowy
Tak wygląda efekt końcowy:
public MyNewClass()
{
public void DoSomething()
{
HelperMethod1();
HelperMethod2();
}
private void HelperMethod1()
{
}
private void HelperMethod2()
{
}
public void DoSomethingElse()
{
}
public void MyNewClass(SomeOtherClass dependency, YetAnotherClass anotherDependency)
{
_dependency = dependency;
_anotherDependency = anotherDependency;
}
private readonly SomeOtherClass _dependency;
private readonly YetAnotherClass _anotherDependency;
}
Co o tym sądzicie? Na wypadek, gdyby ktoś polubił ten styl pisania, tutaj dostępny jest dokument XML w formacie R# ustawiający automatyczne formatowanie zgodnie z tym, co opisałem. Dodatkowo, jeśli w klasie znajduje się pole o nazwie “_logger”, jest umieszczane na samym dole.




about 1 year ago
Cały post można podsumować cytatem: “Think for yourself. Question authority.” I moim zdaniem jest to świetny kierunek. Kolejność deklaracji w klasie, stosowanie regionów itd. nie powinny być podyktowane narzuconymi przez kogoś regułami, tylko wypracowane na podstawie własnych (i zespołu) doświadczeń. Mi zdarza się nazwać klasę czasownikiem, zdarza się zastosować region, zdarza się złamać jakieś “ogólnie przyjęte zasady”. Ważne aby kod robił co ma robić, a z eksplorowania nowych sposobów na wyrażenie swoich intencji zawsze można się wycofać.
Dzięki za zwrócenie uwagi na deklaracje pól i konstruktorów w klasach tworzonych przez zewnętrzny mechanizm, faktycznie nikomu do niczego nie jest potrzebne gapienie się na to zaraz po otwarciu pliku:).
A teraz jeszcze do poszczególnych punktów:
var – moim zdaniem granica sensownego wykorzystania tego słówka to okolice wartości zwracanych przez metody… tam bym var nie umieszczał;
regiony – także staram się z nich korzystać z umiarem i zamykanie “z automatu” różnych części klasy w regiony jest niebezpieczne… jak wszystko robione “z automatu”
grupowanie metod – popieram, metody powiązane powinny być blisko siebie, żeby czytający nie musiał skakać bezsensownie po pliku
I to tyle:).
about 1 year ago
@Procent
“Mi zdarza się nazwać klasę czasownikiem”: dobrze, że zwróciłeś na to moją uwagę. Zdarzyło mi się ostatnio coś takiego i chciałem się z tego wyspowiadać. Pisałem prostego “utila” do walidacji pewnych danych. Nazwałem więc moją klasę rzeczownikeim “Validator”. Wywołania wyglądały mniej-więcej tak:
if (Validator.IsValidCurrency(“PLN”)) …
Zaraz, jak to napisałem uderzyła mnie bezsensowność tego fragmentu: dwa rady mówię, że chcę coś zwalidować. Ostatecznie, zmieniłem nazwę klasy na “IsValid”. Oto co wyszło:
if (IsValid.BicCurrency(“PLN”)…
Prawda że czytelniejsze? Plain Old English:-)
about 1 year ago
Co do grupowania metod nie jestem zbytnio przekonany. Prywatne metody w klasie są zazwyczaj używane przez więcej niż jedną inna metodę. Wówczas bliżej której metody powinna leżeć ta prywatna? Jeżeli logika jakiejś metody publicznej jest na tyle skomplikowana że chciałoby się dla samego porządku rozbić ją na kilka metod prywatnych to może to oznacza ze ta logika powinna być zastąpiona klasą ? Zgodnie z SRP
Regiony są chyba dobre do początkujących programistów którzy całą logikę (prezentacje, usługi, dostep do danych) pisza w jednej klasie Program.cs