
Krok 3 | Wielopokoleniowy kod – 5 kroków do zagłady
Do tej pory operowaliśmy na poziomie funkcji. Ale funkcje to przecież nie jest istota programowania obiektowego. Tutaj liczą się klasy! I to klasy odgrywają najważniejszą rolę w DUŻYCH i POWAŻNYCH projektach!
A skoro klasy to nie może zabraknąć najważniejszego elementu programowania obiektowego czyli dziedziczenia. Wasz zespół na szczęście składa się z samych specjalistów i takie podstawy jak zasady programowania obiektowego to Wy macie w małym palcu.
Wpis jest częścią cyklu „5 kroków do zagłady”. W tym momencie dostępne wpisy to:
Seria dostępna jest też w formie wideo na moim kanale w serwisie Youtube.
Tak więc nie ma o czym tutaj mówić. Dziedziczenie jakie jest każdy widzi. I jest to nieodłączny element każdego poważnego projektu jaki tworzycie. To jest fundament paradygmatu, w którym pracujemy od wielu lat! Gdyby nie dziedziczenie to ile byśmy się musieli kodu nakopiować. Ile to trzeba by zmieniać chcąc zmienić zachowanie jakiejś funkcji w klasie. Toż to zbawienie dla programistów, którego programiści języków funkcyjnych albo ci z poprzedniej epoki, piszący strukturalnie, mogą nam zazdrościć!
Dziedziczenie jest naturalnym rozwiązaniem. Ale Wy idziecie dalej. Wasz projekt jest tak dobrze skonstruowany, że jesteście gotowi na każdą sytuację bo klasy dodatkowo są doskonale przygotowane pod przyszłe dziedziczenie:
abstract class Handler<T> { protected DbContext dbContext = new DbContext(); public abstract void Handle(T command); } class NewOrderHandler : Handler<NewOrderCommand> { public void Handle(NewOrderCommand command) { var order = MapCommand(command); dbContext.Orders.Add(order); dbContext.SaveChanges(); } protected NewOrder MapCommand(NewOrderCommand command) { return new NewOrder(command.CustomerId, command.Items, command.DeliveryDate, command.PaymentType); } }
Z handlera korzystacie w kilku miejscach bo i aplikacja mobilna i strona internetowa dostarczają dane. Ale musicie zrobić walidację użytkowników przychodzących z mobilek bo mogą działać offline i potem wywala się wszystko jak zapiszecie nieistniejącego klienta. Ale co to za problem w Waszym kodzie! Wystarczy rozszerzyć działanie handlera poprzez dziedziczenie i nową klasę:
class NewOrderHandlerWithValidation : NewOrderHandler { public override void Handle(NewOrderCommand commad) { if (dbContext.Customers.All(x => x.Id != command.CustomerId)) { throw new ArgumentException("Nie ma takiego klienta!!!!"); } base.Handle(command); } }
Teraz tylko wstawiacie ją zamiast dotychczasowej i wszystko śmiga.
Jedyny problem jaki jest to to, że jakiś pacan wymyślił, że da się dziedziczyć tylko z jednej klasy… W C++ jakoś to działało, ale nie! Musieli zepsuć.
Problem
Problem w tym wypadku jest tak naprawdę inny. I fakt, że da się dziedziczyć tylko z jednej klasy trochę ratuje Ci tyłek bo nie możesz napsuć bardziej. Doceń to.
Wodospad zmian
Podejście pokazane powyżej ma prawo działać. Ba, nawet są pewnie jakieś wzorce projektowe, które z takiej konstrukcji korzystają. Jednak problem zaczyna się kiedy przejdziemy do rozszerzania projektu i dopasowywania go do nowych wymagań biznesowych.
W takim wielopokoleniowym kodzie, gdzie w dodatku robisz różne warianty wszystkiego klasy są ze sobą tak silnie związane, że wręcz tworzą całość. W powyższym przykładzie można te klasy traktować wręcz jako jedną. Pomyśl sobie – Twój projekt składa się z jednej wielkiej klasy porozrzucanej na wiele plików (swoją drogą w C# dosłownie na to pozwalają, ale nie powiem jakie słowo kluczowe jest za tą tragedię odpowiedzialne).
W przypadku tak silnych zależności zmiana na jednym poziomie niesie za sobą zmiany we wszystkich poziomach zależnych. Dopóki jest to jeden poziom dziedziczenia to sprawa jest jeszcze do ogarnięcia. Ale wyobraź sobie, że np. w klasie bazowej w dzisiejszym kodzie zmieniasz funkcję na wirtualną i dodajesz rzucanie wyjątku w razie nieistnienia klienta w bazie. A jednocześnie na drugim poziomie dziedziczenia funkcja Handle() wysyła maila do działu sprzedaży żeby sprawdzili czy klient, który nie istnieje w bazie był w niej i jakoś zareagowali biznesowo na tę sytuację.
W takim przypadku opcje są dwie.
- Macie testy i widzicie błąd. Wtedy trzeba przejrzeć zmiany w klasie, która źle działa. Potem w klasie po której ona dziedziczy. Następnie w klasie z której dziedziczny klasa, z której dziedziczy nasza klasa itd.
- Nie macie testów i po pół roku szef organizuje spotkanie na temat tego, że przez Waszą aplikację stracili wielu klientów. Którym można było coś zaproponować w procesie tworzenia zamówienia.
Utrata elastyczności
Dodatkowo takie namiętne stosowanie dziedziczenia jest paradoksalnie coraz mocniejszym zmniejszaniem elastyczności kodu. Łączy się to z punktem poprzednim. Bo kiedy w powyższym przypadku chciałbyś wprowadzić jakieś warunkowe zapisywanie danych (np. w zależności od typu zamówienie idzie do bazy albo leci na kolejkę bo inny system je przejmie) to nie możesz myśleć tylko w kontekście tej jednej klasy. Musisz też myśleć w kontekście wszystkich klas, które z niej dziedziczą. Nawet jak sprawdzisz je wszystkie to nigdy nie masz pewności, że będą gotowe na Twoją zmianę. Więc możesz napisać osobną klasę, która będzie się tym zajmować.
Ale wtedy tracisz chociażby możliwość łatwego podmieniania obiektów różnych klas w aplikacji. Bo albo Twoja klasa dziedziczy z tej korzystającej z domyślnego kontekstu bazy danych albo z tego zmienionego. Ewentualnie zaczynasz robić jakieś dziwne struktury żeby ten „problem” obejść i wtedy wracamy do punktu pierwszego. Bo tutaj pojęcie spagetti kodu będzie już tak pasujące, że zostanie Ci tylko szukanie sosu pomidorowego i parmezanu żeby ten kod przyprawić.
Przyczyna
Przyczyna tutaj jest wg mnie prosta. Praktycznie każda książka o podstawach języka obiektowego i programowaniu obiektowym poświęca masę miejsca dziedziczeniu. Roztaczane są przed nami świetlane wizje świata, w którym kolejne pokolenia kodu żyją między sobą w zgodzie. Do tego wzorce i poradniki wykorzystują to dziedziczenie. A my chłoniemy tą wizję i bezmyślnie uznajemy, że dziedziczenie jest istotą programowania obiektowego. Tutaj dodam, że jak zadasz programistom pytanie co jest istotne w programowaniu obiektowym to spora grupa odpowie, że dziedziczenie klas.
Sytuację pogarsza też brak testów w wielu projektach. Bo bez testów nie widzimy jak powoli tracimy wspomnianą wcześniej elastyczność i prostotę w operowaniu kodem, który nadużywa dziedziczenia.
Rozwiązanie
Nowsze języki programowania starają się utrudnić nam możliwość zepsucia świata poprzez nadużywanie dziedziczenia. Robią to np. poprzez ograniczenie ilości klas, z których możemy dziedziczyć jednorazowo. Jednak nie mogą tego dziedziczenia wyeliminować w ogóle bo jednak jest ono przydatne i potrzebne w określonych sytuacjach. Ale naprawdę są to sytuacje konkretne i poprzedzone analizą innych możliwości rozwiązania zadania.
To co mogę Ci poradzić w tej kwestii to zastąpienie dziedziczenia kompozycją. Przeczytaj czym jest kompozycja. Do tego zapoznaj się z tematyką interfejsów (jeżeli Twój język programowania je wspiera) i wstrzykiwaniem zależności.
Niech kompozycja stanie się Twoim domyślnym sposobem radzenia sobie z rozszerzaniem kodu. Najłatwiej to przyjdzie kiedy zaczniesz myśleć o klasach i obiektach jak o klockach albo jakichś elementach, z których coś składasz. Bo kiedy chcesz mieć w komputerze wentylatory z LEDami zamiast takich bez podświetlenia to wymieniasz te właśnie elementy, które mają konkretny interfejs czyli rozmiar i wtyczkę zasilania. Nie stawiasz obok drugiego komputera, który w większości podłączony jest kablami do części z tego pierwszego tylko wentylator i obudowę ma swoją.
Tak więc zapamiętaj, że Twój kod to klocki, z których składasz całą maszynę i w taki sposób powinieneś o nim myśleć – składanie kloców w całość i interfejsy, które umożliwiają w razie potrzeby wymianę tych klocków właśnie.
Leave a Comment